Skip to content

Commit e4ecd60

Browse files
gh-149722: Fix ipaddress.collapse_addresses()
* Explicitly forbid IP interfaces, support of which was undocumented and broken. * Document support for IP addresses. * Always raise TypeError for unsupported types. * Add more tests.
1 parent b546cc1 commit e4ecd60

4 files changed

Lines changed: 71 additions & 45 deletions

File tree

Doc/library/ipaddress.rst

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,13 +1007,16 @@ The module also provides the following module level functions:
10071007

10081008
Return an iterator of the collapsed :class:`IPv4Network` or
10091009
:class:`IPv6Network` objects. *addresses* is an :term:`iterable` of
1010-
:class:`IPv4Network` or :class:`IPv6Network` objects. A :exc:`TypeError` is
1011-
raised if *addresses* contains mixed version objects.
1010+
:class:`IPv4Address`, :class:`IPv6Address`, :class:`IPv4Network` or
1011+
:class:`IPv6Network` objects.
1012+
A :exc:`TypeError` is raised if *addresses* contains mixed version objects.
10121013

1013-
>>> [ipaddr for ipaddr in
1014-
... ipaddress.collapse_addresses([ipaddress.IPv4Network('192.0.2.0/25'),
1015-
... ipaddress.IPv4Network('192.0.2.128/25')])]
1014+
>>> list(ipaddress.collapse_addresses([ipaddress.IPv4Network('192.0.2.0/25'),
1015+
... ipaddress.IPv4Network('192.0.2.128/25')]))
10161016
[IPv4Network('192.0.2.0/24')]
1017+
>>> list(ipaddress.collapse_addresses([ipaddress.IPv4Address('192.0.2.0'),
1018+
... ipaddress.IPv4Address('192.0.2.1'), ipaddress.IPv4Network('192.0.2.2/31')]))
1019+
[IPv4Network('192.0.2.0/30')]
10171020

10181021

10191022
.. function:: get_mixed_type_key(obj)

Lib/ipaddress.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -299,21 +299,22 @@ def _collapse_addresses_internal(addresses):
299299

300300

301301
def collapse_addresses(addresses):
302-
"""Collapse a list of IP objects.
302+
"""Collapse an iterable of IP addresses or networks.
303303
304304
Example:
305305
collapse_addresses([IPv4Network('192.0.2.0/25'),
306306
IPv4Network('192.0.2.128/25')]) ->
307307
[IPv4Network('192.0.2.0/24')]
308308
309309
Args:
310-
addresses: An iterable of IPv4Network or IPv6Network objects.
310+
addresses: An iterable of IPv4Address, IPv6Address, IPv4Network or
311+
IPv6Network objects.
311312
312313
Returns:
313314
An iterator of the collapsed IPv(4|6)Network objects.
314315
315316
Raises:
316-
TypeError: If passed a list of mixed version objects.
317+
TypeError: If passed an iterable of mixed version objects.
317318
318319
"""
319320
addrs = []
@@ -322,24 +323,20 @@ def collapse_addresses(addresses):
322323

323324
# split IP addresses and networks
324325
for ip in addresses:
325-
if isinstance(ip, _BaseAddress):
326-
if ips and ips[-1].version != ip.version:
327-
raise TypeError("%s and %s are not of the same version" % (
328-
ip, ips[-1]))
326+
if isinstance(ip, _BaseAddress) and not hasattr(ip, 'ip'):
329327
ips.append(ip)
330-
elif ip._prefixlen == ip.max_prefixlen:
331-
if ips and ips[-1].version != ip.version:
332-
raise TypeError("%s and %s are not of the same version" % (
333-
ip, ips[-1]))
334-
try:
335-
ips.append(ip.ip)
336-
except AttributeError:
328+
elif isinstance(ip, _BaseNetwork):
329+
if ip._prefixlen == ip.max_prefixlen:
337330
ips.append(ip.network_address)
331+
else:
332+
if nets and nets[-1].version != ip.version:
333+
raise TypeError("%s and %s are not of the same version" % (
334+
ip, nets[-1]))
335+
nets.append(ip)
338336
else:
339-
if nets and nets[-1].version != ip.version:
340-
raise TypeError("%s and %s are not of the same version" % (
341-
ip, nets[-1]))
342-
nets.append(ip)
337+
raise TypeError("expected an iterable of IP addresses or "
338+
"networks, not %s" % type(ip).__name__)
339+
last_ip = ip
343340

344341
# sort and dedup
345342
ips = sorted(set(ips))

Lib/test/test_ipaddress.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1845,6 +1845,9 @@ def testSlash0Constructor(self):
18451845
'1.2.3.4/0')
18461846

18471847
def testCollapsing(self):
1848+
collapsed = ipaddress.collapse_addresses([])
1849+
self.assertEqual(list(collapsed), [])
1850+
18481851
# test only IP addresses including some duplicates
18491852
ip1 = ipaddress.IPv4Address('1.1.1.0')
18501853
ip2 = ipaddress.IPv4Address('1.1.1.1')
@@ -1858,27 +1861,21 @@ def testCollapsing(self):
18581861
self.assertEqual(list(collapsed),
18591862
[ipaddress.IPv4Network('1.1.1.0/30'),
18601863
ipaddress.IPv4Network('1.1.1.4/32')])
1861-
1862-
# test a mix of IP addresses and networks including some duplicates
1863-
ip1 = ipaddress.IPv4Address('1.1.1.0')
1864-
ip2 = ipaddress.IPv4Address('1.1.1.1')
1865-
ip3 = ipaddress.IPv4Address('1.1.1.2')
1866-
ip4 = ipaddress.IPv4Address('1.1.1.3')
1867-
#ip5 = ipaddress.IPv4Interface('1.1.1.4/30')
1868-
#ip6 = ipaddress.IPv4Interface('1.1.1.4/30')
1869-
# check that addresses are subsumed properly.
1870-
collapsed = ipaddress.collapse_addresses([ip1, ip2, ip3, ip4])
1864+
collapsed = ipaddress.collapse_addresses([ip1])
18711865
self.assertEqual(list(collapsed),
1872-
[ipaddress.IPv4Network('1.1.1.0/30')])
1866+
[ipaddress.IPv4Network('1.1.1.0/32')])
1867+
# test same IP addresses
1868+
self.assertEqual(list(ipaddress.collapse_addresses([ip1, ip1, ip6])),
1869+
[ipaddress.ip_network('1.1.1.0/32')])
18731870

18741871
# test only IP networks
18751872
ip1 = ipaddress.IPv4Network('1.1.0.0/24')
18761873
ip2 = ipaddress.IPv4Network('1.1.1.0/24')
18771874
ip3 = ipaddress.IPv4Network('1.1.2.0/24')
18781875
ip4 = ipaddress.IPv4Network('1.1.3.0/24')
18791876
ip5 = ipaddress.IPv4Network('1.1.4.0/24')
1880-
# stored in no particular order b/c we want CollapseAddr to call
1881-
# [].sort
1877+
# stored in no particular order b/c we want collapse_addresses()
1878+
# to call sorted()
18821879
ip6 = ipaddress.IPv4Network('1.1.0.0/22')
18831880
# check that addresses are subsumed properly.
18841881
collapsed = ipaddress.collapse_addresses([ip1, ip2, ip3, ip4, ip5,
@@ -1893,16 +1890,9 @@ def testCollapsing(self):
18931890
[ipaddress.IPv4Network('1.1.0.0/23')])
18941891

18951892
# test same IP networks
1896-
ip_same1 = ip_same2 = ipaddress.IPv4Network('1.1.1.1/32')
1897-
self.assertEqual(list(ipaddress.collapse_addresses(
1898-
[ip_same1, ip_same2])),
1899-
[ip_same1])
1893+
self.assertEqual(list(ipaddress.collapse_addresses([ip1, ip1])),
1894+
[ip1])
19001895

1901-
# test same IP addresses
1902-
ip_same1 = ip_same2 = ipaddress.IPv4Address('1.1.1.1')
1903-
self.assertEqual(list(ipaddress.collapse_addresses(
1904-
[ip_same1, ip_same2])),
1905-
[ipaddress.ip_network('1.1.1.1/32')])
19061896
ip1 = ipaddress.IPv6Network('2001::/100')
19071897
ip2 = ipaddress.IPv6Network('2001::/120')
19081898
ip3 = ipaddress.IPv6Network('2001::/96')
@@ -1917,6 +1907,21 @@ def testCollapsing(self):
19171907
collapsed = ipaddress.collapse_addresses([ip1, ip2, ip3])
19181908
self.assertEqual(list(collapsed), [ip3])
19191909

1910+
# test a mix of IP addresses and networks
1911+
ip1 = ipaddress.IPv4Address('1.1.1.0')
1912+
ip2 = ipaddress.IPv4Address('1.1.1.1')
1913+
ip3 = ipaddress.IPv4Network('1.1.1.2/31')
1914+
# check that addresses are subsumed properly.
1915+
collapsed = ipaddress.collapse_addresses([ip1, ip2, ip3])
1916+
self.assertEqual(list(collapsed),
1917+
[ipaddress.IPv4Network('1.1.1.0/30')])
1918+
1919+
# unsupported types
1920+
self.assertRaises(TypeError, ipaddress.collapse_addresses, [42])
1921+
self.assertRaises(TypeError, ipaddress.collapse_addresses, [None])
1922+
self.assertRaises(TypeError, ipaddress.collapse_addresses,
1923+
[ipaddress.IPv4Interface('1.1.1.4/30')])
1924+
19201925
# the toejam test
19211926
addr_tuples = [
19221927
(ipaddress.ip_address('1.1.1.1'),
@@ -1941,6 +1946,24 @@ def testCollapsing(self):
19411946
for ip1, ip2 in addr_tuples:
19421947
self.assertRaises(TypeError, ipaddress.collapse_addresses,
19431948
[ip1, ip2])
1949+
for ip1 in [
1950+
ipaddress.ip_address('1.1.1.1'),
1951+
ipaddress.IPv4Network('1.1.0.0/24'),
1952+
ipaddress.IPv4Network('1.1.0.0/32'),
1953+
]:
1954+
for ip2 in [
1955+
ipaddress.ip_address('::1'),
1956+
ipaddress.IPv6Network('2001::/120'),
1957+
ipaddress.IPv6Network('2001::/128'),
1958+
ipaddress.ip_address('::1%scope'),
1959+
ipaddress.IPv6Network('2001::%scope/120'),
1960+
ipaddress.IPv6Network('2001::%scope/128'),
1961+
]:
1962+
with self.subTest(ip1=ip1, ip2=ip2):
1963+
with self.assertRaises(TypeError):
1964+
list(ipaddress.collapse_addresses([ip1, ip2]))
1965+
with self.assertRaises(TypeError):
1966+
list(ipaddress.collapse_addresses([ip2, ip1]))
19441967

19451968
def testSummarizing(self):
19461969
#ip = ipaddress.ip_address
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Make :func:`ipaddress.collapse_addresses` always raising ``TypeError`` for
2+
unsupported types, including IP interfaces. Document that IP addresses are
3+
supported.

0 commit comments

Comments
 (0)