Skip to content

Commit 3495d09

Browse files
committed
gh-151769: Make IPv6Address ordering scope_id-aware
IPv6Address.__eq__ and __hash__ fold in the interface scope_id, but IPv6Address inherited the scope-blind _BaseAddress.__lt__. Under @functools.total_ordering, which derives > from < and ==, that made ordering inconsistent for addresses differing only by scope: both a > b and b > a could be true, so antisymmetry broke and sorted(), min() and max() were non-deterministic. Add a scope-aware IPv6Address.__lt__ that tie-breaks on scope_id only when the integer address is equal, so unscoped sorts before scoped. Overriding only __lt__ routes all four total_ordering comparisons through the scope-aware path, which also fixes scoped IPv6Interface and IPv6Network ordering since they delegate to address comparison. _BaseAddress.__lt__ is untouched, so IPv4 is unaffected.
1 parent aa5b164 commit 3495d09

3 files changed

Lines changed: 60 additions & 0 deletions

File tree

Lib/ipaddress.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2010,6 +2010,22 @@ def __eq__(self, other):
20102010
return False
20112011
return self._scope_id == getattr(other, '_scope_id', None)
20122012

2013+
def __lt__(self, other):
2014+
if not isinstance(other, _BaseAddress):
2015+
return NotImplemented
2016+
if self.version != other.version:
2017+
raise TypeError('%s and %s are not of the same version' % (
2018+
self, other))
2019+
if self._ip != other._ip:
2020+
return self._ip < other._ip
2021+
# Equal integer addresses are ordered by scope_id so that ordering
2022+
# stays consistent with __eq__/__hash__, which already fold it in.
2023+
# Unscoped sorts before scoped; scope ids compare lexicographically.
2024+
self_scope = self._scope_id
2025+
other_scope = getattr(other, '_scope_id', None)
2026+
return ((self_scope is not None, self_scope or '')
2027+
< (other_scope is not None, other_scope or ''))
2028+
20132029
def __reduce__(self):
20142030
return (self.__class__, (str(self),))
20152031

Lib/test/test_ipaddress.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66

77
import copy
8+
import itertools
89
import unittest
910
import re
1011
import contextlib
@@ -2029,6 +2030,43 @@ def testAddressComparison(self):
20292030
self.assertTrue(ipaddress.ip_address('::1%scope') <=
20302031
ipaddress.ip_address('::2%scope'))
20312032

2033+
def testScopedAddressComparison(self):
2034+
plain = ipaddress.ip_address('fe80::1')
2035+
eth0 = ipaddress.ip_address('fe80::1%eth0')
2036+
eth1 = ipaddress.ip_address('fe80::1%eth1')
2037+
scoped = [plain, eth0, eth1]
2038+
for a in scoped:
2039+
for b in scoped:
2040+
self.assertEqual((a < b) + (a == b) + (a > b), 1, msg=(a, b))
2041+
if a != b:
2042+
self.assertNotEqual(a > b, b > a, msg=(a, b))
2043+
self.assertTrue(plain < eth0)
2044+
self.assertTrue(eth0 < eth1)
2045+
self.assertTrue(eth0 > plain)
2046+
self.assertFalse(plain > eth0)
2047+
self.assertTrue(plain <= eth0)
2048+
self.assertTrue(eth0 >= plain)
2049+
expected = [plain, eth0, eth1]
2050+
for perm in itertools.permutations(scoped):
2051+
self.assertEqual(sorted(perm), expected, msg=perm)
2052+
self.assertEqual(min(perm), plain, msg=perm)
2053+
self.assertEqual(max(perm), eth1, msg=perm)
2054+
v4 = [ipaddress.ip_address('10.0.0.1'),
2055+
ipaddress.ip_address('10.0.0.2'),
2056+
ipaddress.ip_address('10.0.0.3')]
2057+
for perm in itertools.permutations(v4):
2058+
self.assertEqual(sorted(perm), v4, msg=perm)
2059+
ifaces = [ipaddress.ip_interface('fe80::1/64'),
2060+
ipaddress.ip_interface('fe80::1%eth0/64'),
2061+
ipaddress.ip_interface('fe80::1%eth1/64')]
2062+
for perm in itertools.permutations(ifaces):
2063+
self.assertEqual(sorted(perm), ifaces, msg=perm)
2064+
nets = [ipaddress.ip_network('fe80::/64'),
2065+
ipaddress.ip_network('fe80::%eth0/64'),
2066+
ipaddress.ip_network('fe80::%eth1/64')]
2067+
for perm in itertools.permutations(nets):
2068+
self.assertEqual(sorted(perm), nets, msg=perm)
2069+
20322070
def testInterfaceComparison(self):
20332071
self.assertTrue(ipaddress.ip_interface('1.1.1.1/24') ==
20342072
ipaddress.ip_interface('1.1.1.1/24'))
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Fix ordering of scoped :class:`~ipaddress.IPv6Address` objects. Comparison
2+
now takes the interface ``scope_id`` into account, consistent with equality
3+
and hashing, so that ``sorted()``, ``min()`` and ``max()`` are deterministic
4+
for addresses that differ only in scope. Ordering of the corresponding
5+
scoped :class:`~ipaddress.IPv6Interface` and :class:`~ipaddress.IPv6Network`
6+
objects is fixed as well. Patch by Olayinka Vaughan.

0 commit comments

Comments
 (0)