From 3902135beb45bef8e5b368334023077a9e925868 Mon Sep 17 00:00:00 2001 From: Oliver Giles Date: Mon, 3 May 2021 09:39:00 +1200 Subject: [PATCH 1/3] bpo-44012: IPv6Address.exploded with scope_id Support for scoped IPv6 addresses was implemented in bpo-34788 but missed implementing the .exploded method: >>> import ipaddress >>> ipaddress.IPv6Address('fe80::1%eth0').exploded Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.9/ipaddress.py", line 394, in exploded return self._explode_shorthand_ip_string() File "/usr/lib/python3.9/ipaddress.py", line 1824, in _explode_shorthand_ip_string ip_int = self._ip_int_from_string(ip_str) File "/usr/lib/python3.9/ipaddress.py", line 1705, in _ip_int_from_string raise AddressValueError("%s in %r" % (exc, ip_str)) from None ipaddress.AddressValueError: Only hex digits permitted in '1%eth0' in 'fe80::1%eth0' After this commit: >>> import ipaddress >>> ipaddress.IPv6Address('fe80::1%eth0').exploded 'fe80:0000:0000:0000:0000:0000:0000:0001%eth0' --- Lib/ipaddress.py | 11 ++++------- Lib/test/test_ipaddress.py | 2 ++ .../Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst | 1 + 3 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index af7aedfa6e51a15..dc7cf215576707c 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1812,9 +1812,6 @@ def _string_from_ip_int(cls, ip_int=None): def _explode_shorthand_ip_string(self): """Expand a shortened IPv6 address. - Args: - ip_str: A string, the IPv6 address. - Returns: A string, the expanded IPv6 address. @@ -1824,14 +1821,14 @@ def _explode_shorthand_ip_string(self): elif isinstance(self, IPv6Interface): ip_str = str(self.ip) else: - ip_str = str(self) + ip_str = str(self._string_from_ip_int(self._ip)) ip_int = self._ip_int_from_string(ip_str) hex_str = '%032x' % ip_int - parts = [hex_str[x:x+4] for x in range(0, 32, 4)] + exploded = ':'.join([hex_str[x:x+4] for x in range(0, 32, 4)]) if isinstance(self, (_BaseNetwork, IPv6Interface)): - return '%s/%d' % (':'.join(parts), self._prefixlen) - return ':'.join(parts) + return '%s/%d' % (exploded, self._prefixlen) + return exploded + '%' + self._scope_id if self._scope_id else exploded def _reverse_pointer(self): """Return the reverse DNS pointer name for the IPv6 address. diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index cdd9880c3c17faf..9e9ec3a179e5f29 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -2454,6 +2454,8 @@ def testExplodeShortHandIpStr(self): addr1.exploded) self.assertEqual('0000:0000:0000:0000:0000:0000:0000:0001/128', ipaddress.IPv6Interface('::1/128').exploded) + self.assertEqual('fe80:0000:0000:0000:0000:0000:0000:0001%1', + ipaddress.IPv6Address('fe80::1%1').exploded) # issue 77 self.assertEqual('2001:0000:5ef5:79fd:0000:059d:a0e5:0ba1', addr2.exploded) diff --git a/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst b/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst new file mode 100644 index 000000000000000..8a396ab471dd38e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst @@ -0,0 +1 @@ +:meth:`ipaddress.IPv6Address.exploded` now supports IPv6 addresses with a ``scope_id`` (link-local addresses) \ No newline at end of file From 7af67a3d4f7b6e62e21b8107536071f6b7d8d424 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Wed, 12 Aug 2026 18:57:40 +0300 Subject: [PATCH 2/3] Support the scope ID in IPv6Interface.exploded too IPv6Interface.exploded silently omitted the scope ID, unlike str() and compressed. Attach it in one place, before the prefix length, so that all three representations agree. Co-Authored-By: Claude Opus 5 (1M context) --- Lib/ipaddress.py | 10 +++++++--- Lib/test/test_ipaddress.py | 4 ++++ .../Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst | 6 +++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index fe2a9c7d4740910..6978483544e621a 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1911,14 +1911,18 @@ def _explode_shorthand_ip_string(self): elif isinstance(self, IPv6Interface): ip_str = str(self.ip) else: - ip_str = str(self._string_from_ip_int(self._ip)) + ip_str = self._string_from_ip_int(self._ip) ip_int = self._ip_int_from_string(ip_str) hex_str = '%032x' % ip_int exploded = ':'.join([hex_str[x:x+4] for x in range(0, 32, 4)]) - if isinstance(self, (_BaseNetwork, IPv6Interface)): + if isinstance(self, _BaseNetwork): return '%s/%d' % (exploded, self._prefixlen) - return exploded + '%' + self._scope_id if self._scope_id else exploded + if self._scope_id: + exploded = '%s%%%s' % (exploded, self._scope_id) + if isinstance(self, IPv6Interface): + return '%s/%d' % (exploded, self._prefixlen) + return exploded def _reverse_pointer(self): """Return the reverse DNS pointer name for the IPv6 address. diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index 900fb91a33ffc9a..375d45172a9f4d1 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -2720,6 +2720,10 @@ def testExplodeShortHandIpStr(self): ipaddress.IPv6Interface('::1/128').exploded) self.assertEqual('fe80:0000:0000:0000:0000:0000:0000:0001%1', ipaddress.IPv6Address('fe80::1%1').exploded) + self.assertEqual('fe80:0000:0000:0000:0000:0000:0000:0001%eth0', + ipaddress.IPv6Address('fe80::1%eth0').exploded) + self.assertEqual('fe80:0000:0000:0000:0000:0000:0000:0001%1/64', + ipaddress.IPv6Interface('fe80::1%1/64').exploded) # issue 77 self.assertEqual('2001:0000:5ef5:79fd:0000:059d:a0e5:0ba1', addr2.exploded) diff --git a/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst b/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst index 8a396ab471dd38e..7e0d23d936ab393 100644 --- a/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst +++ b/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst @@ -1 +1,5 @@ -:meth:`ipaddress.IPv6Address.exploded` now supports IPv6 addresses with a ``scope_id`` (link-local addresses) \ No newline at end of file +:attr:`ipaddress.IPv6Address.exploded` and +:attr:`ipaddress.IPv6Interface.exploded` now support addresses with a scope +ID (link-local addresses). +Previously the former raised :exc:`~ipaddress.AddressValueError` +and the latter omitted the scope ID. From 5cfc07eb4d269e83b217699f26613d3a199cc51d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 13 Aug 2026 08:55:58 +0300 Subject: [PATCH 3/3] Fix the reference in the NEWS entry IPv6Interface.exploded is not documented separately, so reference the classes instead of the attribute. Co-Authored-By: Claude Opus 5 (1M context) --- .../next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst b/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst index 7e0d23d936ab393..28c21c327504bde 100644 --- a/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst +++ b/Misc/NEWS.d/next/Library/2021-06-03-22-23-38.bpo-44012.BK3HfA.rst @@ -1,5 +1,5 @@ -:attr:`ipaddress.IPv6Address.exploded` and -:attr:`ipaddress.IPv6Interface.exploded` now support addresses with a scope -ID (link-local addresses). +The ``exploded`` attribute of :class:`ipaddress.IPv6Address` and +:class:`ipaddress.IPv6Interface` now supports addresses with a scope ID +(link-local addresses). Previously the former raised :exc:`~ipaddress.AddressValueError` and the latter omitted the scope ID.