From 3495d09b860e6267a9e190813b594ca1485f2fb5 Mon Sep 17 00:00:00 2001 From: Builder106 Date: Sat, 20 Jun 2026 00:23:44 -0700 Subject: [PATCH] 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. --- Lib/ipaddress.py | 16 ++++++++ Lib/test/test_ipaddress.py | 38 +++++++++++++++++++ ...-06-20-00-07-55.gh-issue-151769.7D4gBg.rst | 6 +++ 3 files changed, 60 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-06-20-00-07-55.gh-issue-151769.7D4gBg.rst diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index f1062a8cd052a55..9b5d9b3aa362735 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -2010,6 +2010,22 @@ def __eq__(self, other): return False return self._scope_id == getattr(other, '_scope_id', None) + def __lt__(self, other): + if not isinstance(other, _BaseAddress): + return NotImplemented + if self.version != other.version: + raise TypeError('%s and %s are not of the same version' % ( + self, other)) + if self._ip != other._ip: + return self._ip < other._ip + # Equal integer addresses are ordered by scope_id so that ordering + # stays consistent with __eq__/__hash__, which already fold it in. + # Unscoped sorts before scoped; scope ids compare lexicographically. + self_scope = self._scope_id + other_scope = getattr(other, '_scope_id', None) + return ((self_scope is not None, self_scope or '') + < (other_scope is not None, other_scope or '')) + def __reduce__(self): return (self.__class__, (str(self),)) diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index 3f017b97dc28a38..195b021093ca4d0 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -5,6 +5,7 @@ import copy +import itertools import unittest import re import contextlib @@ -2029,6 +2030,43 @@ def testAddressComparison(self): self.assertTrue(ipaddress.ip_address('::1%scope') <= ipaddress.ip_address('::2%scope')) + def testScopedAddressComparison(self): + plain = ipaddress.ip_address('fe80::1') + eth0 = ipaddress.ip_address('fe80::1%eth0') + eth1 = ipaddress.ip_address('fe80::1%eth1') + scoped = [plain, eth0, eth1] + for a in scoped: + for b in scoped: + self.assertEqual((a < b) + (a == b) + (a > b), 1, msg=(a, b)) + if a != b: + self.assertNotEqual(a > b, b > a, msg=(a, b)) + self.assertTrue(plain < eth0) + self.assertTrue(eth0 < eth1) + self.assertTrue(eth0 > plain) + self.assertFalse(plain > eth0) + self.assertTrue(plain <= eth0) + self.assertTrue(eth0 >= plain) + expected = [plain, eth0, eth1] + for perm in itertools.permutations(scoped): + self.assertEqual(sorted(perm), expected, msg=perm) + self.assertEqual(min(perm), plain, msg=perm) + self.assertEqual(max(perm), eth1, msg=perm) + v4 = [ipaddress.ip_address('10.0.0.1'), + ipaddress.ip_address('10.0.0.2'), + ipaddress.ip_address('10.0.0.3')] + for perm in itertools.permutations(v4): + self.assertEqual(sorted(perm), v4, msg=perm) + ifaces = [ipaddress.ip_interface('fe80::1/64'), + ipaddress.ip_interface('fe80::1%eth0/64'), + ipaddress.ip_interface('fe80::1%eth1/64')] + for perm in itertools.permutations(ifaces): + self.assertEqual(sorted(perm), ifaces, msg=perm) + nets = [ipaddress.ip_network('fe80::/64'), + ipaddress.ip_network('fe80::%eth0/64'), + ipaddress.ip_network('fe80::%eth1/64')] + for perm in itertools.permutations(nets): + self.assertEqual(sorted(perm), nets, msg=perm) + def testInterfaceComparison(self): self.assertTrue(ipaddress.ip_interface('1.1.1.1/24') == ipaddress.ip_interface('1.1.1.1/24')) diff --git a/Misc/NEWS.d/next/Library/2026-06-20-00-07-55.gh-issue-151769.7D4gBg.rst b/Misc/NEWS.d/next/Library/2026-06-20-00-07-55.gh-issue-151769.7D4gBg.rst new file mode 100644 index 000000000000000..524e30eb54d69d6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-20-00-07-55.gh-issue-151769.7D4gBg.rst @@ -0,0 +1,6 @@ +Fix ordering of scoped :class:`~ipaddress.IPv6Address` objects. Comparison +now takes the interface ``scope_id`` into account, consistent with equality +and hashing, so that ``sorted()``, ``min()`` and ``max()`` are deterministic +for addresses that differ only in scope. Ordering of the corresponding +scoped :class:`~ipaddress.IPv6Interface` and :class:`~ipaddress.IPv6Network` +objects is fixed as well. Patch by Olayinka Vaughan.