diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst index 9ccd8602bcb2c3..4bc92609f15d37 100644 --- a/Doc/library/ipaddress.rst +++ b/Doc/library/ipaddress.rst @@ -1007,13 +1007,16 @@ The module also provides the following module level functions: Return an iterator of the collapsed :class:`IPv4Network` or :class:`IPv6Network` objects. *addresses* is an :term:`iterable` of - :class:`IPv4Network` or :class:`IPv6Network` objects. A :exc:`TypeError` is - raised if *addresses* contains mixed version objects. + :class:`IPv4Address`, :class:`IPv6Address`, :class:`IPv4Network` or + :class:`IPv6Network` objects. + A :exc:`TypeError` is raised if *addresses* contains mixed version objects. - >>> [ipaddr for ipaddr in - ... ipaddress.collapse_addresses([ipaddress.IPv4Network('192.0.2.0/25'), - ... ipaddress.IPv4Network('192.0.2.128/25')])] + >>> list(ipaddress.collapse_addresses([ipaddress.IPv4Network('192.0.2.0/25'), + ... ipaddress.IPv4Network('192.0.2.128/25')])) [IPv4Network('192.0.2.0/24')] + >>> list(ipaddress.collapse_addresses([ipaddress.IPv4Address('192.0.2.0'), + ... ipaddress.IPv4Address('192.0.2.1'), ipaddress.IPv4Network('192.0.2.2/31')])) + [IPv4Network('192.0.2.0/30')] .. function:: get_mixed_type_key(obj) diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index f1062a8cd052a5..00e15d3f4804e3 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -299,7 +299,7 @@ def _collapse_addresses_internal(addresses): def collapse_addresses(addresses): - """Collapse a list of IP objects. + """Collapse an iterable of IP addresses or networks. Example: collapse_addresses([IPv4Network('192.0.2.0/25'), @@ -307,13 +307,14 @@ def collapse_addresses(addresses): [IPv4Network('192.0.2.0/24')] Args: - addresses: An iterable of IPv4Network or IPv6Network objects. + addresses: An iterable of IPv4Address, IPv6Address, IPv4Network or + IPv6Network objects. Returns: An iterator of the collapsed IPv(4|6)Network objects. Raises: - TypeError: If passed a list of mixed version objects. + TypeError: If passed an iterable of mixed version objects. """ addrs = [] @@ -322,24 +323,19 @@ def collapse_addresses(addresses): # split IP addresses and networks for ip in addresses: - if isinstance(ip, _BaseAddress): - if ips and ips[-1].version != ip.version: - raise TypeError("%s and %s are not of the same version" % ( - ip, ips[-1])) + if isinstance(ip, _BaseAddress) and not hasattr(ip, 'ip'): ips.append(ip) - elif ip._prefixlen == ip.max_prefixlen: - if ips and ips[-1].version != ip.version: - raise TypeError("%s and %s are not of the same version" % ( - ip, ips[-1])) - try: - ips.append(ip.ip) - except AttributeError: + elif isinstance(ip, _BaseNetwork): + if ip._prefixlen == ip.max_prefixlen: ips.append(ip.network_address) + else: + if nets and nets[-1].version != ip.version: + raise TypeError("%s and %s are not of the same version" % ( + ip, nets[-1])) + nets.append(ip) else: - if nets and nets[-1].version != ip.version: - raise TypeError("%s and %s are not of the same version" % ( - ip, nets[-1])) - nets.append(ip) + raise TypeError("expected an iterable of IP addresses or " + "networks, not %s" % type(ip).__name__) # sort and dedup ips = sorted(set(ips)) diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index 3f017b97dc28a3..271dfcab1d6f08 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -1845,6 +1845,9 @@ def testSlash0Constructor(self): '1.2.3.4/0') def testCollapsing(self): + collapsed = ipaddress.collapse_addresses([]) + self.assertEqual(list(collapsed), []) + # test only IP addresses including some duplicates ip1 = ipaddress.IPv4Address('1.1.1.0') ip2 = ipaddress.IPv4Address('1.1.1.1') @@ -1858,18 +1861,12 @@ def testCollapsing(self): self.assertEqual(list(collapsed), [ipaddress.IPv4Network('1.1.1.0/30'), ipaddress.IPv4Network('1.1.1.4/32')]) - - # test a mix of IP addresses and networks including some duplicates - ip1 = ipaddress.IPv4Address('1.1.1.0') - ip2 = ipaddress.IPv4Address('1.1.1.1') - ip3 = ipaddress.IPv4Address('1.1.1.2') - ip4 = ipaddress.IPv4Address('1.1.1.3') - #ip5 = ipaddress.IPv4Interface('1.1.1.4/30') - #ip6 = ipaddress.IPv4Interface('1.1.1.4/30') - # check that addresses are subsumed properly. - collapsed = ipaddress.collapse_addresses([ip1, ip2, ip3, ip4]) + collapsed = ipaddress.collapse_addresses([ip1]) self.assertEqual(list(collapsed), - [ipaddress.IPv4Network('1.1.1.0/30')]) + [ipaddress.IPv4Network('1.1.1.0/32')]) + # test same IP addresses + self.assertEqual(list(ipaddress.collapse_addresses([ip1, ip1, ip6])), + [ipaddress.ip_network('1.1.1.0/32')]) # test only IP networks ip1 = ipaddress.IPv4Network('1.1.0.0/24') @@ -1877,8 +1874,8 @@ def testCollapsing(self): ip3 = ipaddress.IPv4Network('1.1.2.0/24') ip4 = ipaddress.IPv4Network('1.1.3.0/24') ip5 = ipaddress.IPv4Network('1.1.4.0/24') - # stored in no particular order b/c we want CollapseAddr to call - # [].sort + # stored in no particular order b/c we want collapse_addresses() + # to call sorted() ip6 = ipaddress.IPv4Network('1.1.0.0/22') # check that addresses are subsumed properly. collapsed = ipaddress.collapse_addresses([ip1, ip2, ip3, ip4, ip5, @@ -1893,16 +1890,9 @@ def testCollapsing(self): [ipaddress.IPv4Network('1.1.0.0/23')]) # test same IP networks - ip_same1 = ip_same2 = ipaddress.IPv4Network('1.1.1.1/32') - self.assertEqual(list(ipaddress.collapse_addresses( - [ip_same1, ip_same2])), - [ip_same1]) + self.assertEqual(list(ipaddress.collapse_addresses([ip1, ip1])), + [ip1]) - # test same IP addresses - ip_same1 = ip_same2 = ipaddress.IPv4Address('1.1.1.1') - self.assertEqual(list(ipaddress.collapse_addresses( - [ip_same1, ip_same2])), - [ipaddress.ip_network('1.1.1.1/32')]) ip1 = ipaddress.IPv6Network('2001::/100') ip2 = ipaddress.IPv6Network('2001::/120') ip3 = ipaddress.IPv6Network('2001::/96') @@ -1917,6 +1907,21 @@ def testCollapsing(self): collapsed = ipaddress.collapse_addresses([ip1, ip2, ip3]) self.assertEqual(list(collapsed), [ip3]) + # test a mix of IP addresses and networks + ip1 = ipaddress.IPv4Address('1.1.1.0') + ip2 = ipaddress.IPv4Address('1.1.1.1') + ip3 = ipaddress.IPv4Network('1.1.1.2/31') + # check that addresses are subsumed properly. + collapsed = ipaddress.collapse_addresses([ip1, ip2, ip3]) + self.assertEqual(list(collapsed), + [ipaddress.IPv4Network('1.1.1.0/30')]) + + # unsupported types + self.assertRaises(TypeError, ipaddress.collapse_addresses, [42]) + self.assertRaises(TypeError, ipaddress.collapse_addresses, [None]) + self.assertRaises(TypeError, ipaddress.collapse_addresses, + [ipaddress.IPv4Interface('1.1.1.4/30')]) + # the toejam test addr_tuples = [ (ipaddress.ip_address('1.1.1.1'), @@ -1941,6 +1946,24 @@ def testCollapsing(self): for ip1, ip2 in addr_tuples: self.assertRaises(TypeError, ipaddress.collapse_addresses, [ip1, ip2]) + for ip1 in [ + ipaddress.ip_address('1.1.1.1'), + ipaddress.IPv4Network('1.1.0.0/24'), + ipaddress.IPv4Network('1.1.0.0/32'), + ]: + for ip2 in [ + ipaddress.ip_address('::1'), + ipaddress.IPv6Network('2001::/120'), + ipaddress.IPv6Network('2001::/128'), + ipaddress.ip_address('::1%scope'), + ipaddress.IPv6Network('2001::%scope/120'), + ipaddress.IPv6Network('2001::%scope/128'), + ]: + with self.subTest(ip1=ip1, ip2=ip2): + with self.assertRaises(TypeError): + list(ipaddress.collapse_addresses([ip1, ip2])) + with self.assertRaises(TypeError): + list(ipaddress.collapse_addresses([ip2, ip1])) def testSummarizing(self): #ip = ipaddress.ip_address diff --git a/Misc/NEWS.d/next/Library/2026-05-12-14-53-07.gh-issue-149722.L93mk7.rst b/Misc/NEWS.d/next/Library/2026-05-12-14-53-07.gh-issue-149722.L93mk7.rst new file mode 100644 index 00000000000000..920224388ea1b8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-12-14-53-07.gh-issue-149722.L93mk7.rst @@ -0,0 +1,3 @@ +Make :func:`ipaddress.collapse_addresses` always raising ``TypeError`` for +unsupported types, including IP interfaces. Document that IP addresses are +supported.