Skip to content

Commit f9c95a4

Browse files
ewosbornezware
authored andcommitted
bpo-32820: __format__ method for ipaddress (#5627)
* bits method and test_bits * Cleaned up assert string * blurb * added docstring * Faster method, per Eric Smith * redoing as __format__ * added ipv6 method * test cases and cleanup * updated news * cleanup and NEWS.d * cleaned up old NEWS * removed cut and paste leftover * one more cleanup * moved to regexp, moved away from v4- and v6-specific versions of __format__ * More cleanup, added ipv6 test cases * more cleanup * more cleanup * cleanup * cleanup * cleanup per review, part 1 * addressed review comments around help string and regexp matching * wrapped v6 test strings. contiguous integers: break at 72char. with underscores: break so that it looks clean. * 's' and '' tests for pv4 and ipv6 * whitespace cleanup * Remove trailing whitespace * Remove more trailing whitespace * Remove an excess blank line
1 parent 92777d5 commit f9c95a4

File tree

3 files changed

+142
-0
lines changed

3 files changed

+142
-0
lines changed

Lib/ipaddress.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,78 @@ def _get_address_key(self):
618618
def __reduce__(self):
619619
return self.__class__, (self._ip,)
620620

621+
def __format__(self, fmt):
622+
"""Returns an IP address as a formatted string.
623+
624+
Supported presentation types are:
625+
's': returns the IP address as a string (default)
626+
'b' or 'n': converts to binary and returns a zero-padded string
627+
'X' or 'x': converts to upper- or lower-case hex and returns a zero-padded string
628+
629+
For binary and hex presentation types, the alternate form specifier
630+
'#' and the grouping option '_' are supported.
631+
"""
632+
633+
634+
# Support string formatting
635+
if not fmt or fmt[-1] == 's':
636+
# let format() handle it
637+
return format(str(self), fmt)
638+
639+
# From here on down, support for 'bnXx'
640+
641+
import re
642+
fmt_re = '^(?P<alternate>#?)(?P<grouping>_?)(?P<fmt_base>[xbnX]){1}$'
643+
m = re.match(fmt_re, fmt)
644+
if not m:
645+
return super().__format__(fmt)
646+
647+
groupdict = m.groupdict()
648+
alternate = groupdict['alternate']
649+
grouping = groupdict['grouping']
650+
fmt_base = groupdict['fmt_base']
651+
652+
# Set some defaults
653+
if fmt_base == 'n':
654+
if self._version == 4:
655+
fmt_base = 'b' # Binary is default for ipv4
656+
if self._version == 6:
657+
fmt_base = 'x' # Hex is default for ipv6
658+
659+
# Handle binary formatting
660+
if fmt_base == 'b':
661+
if self._version == 4:
662+
# resulting string is '0b' + 32 bits
663+
# plus 7 _ if needed
664+
padlen = IPV4LENGTH+2 + (7*len(grouping))
665+
elif self._version == 6:
666+
# resulting string is '0b' + 128 bits
667+
# plus 31 _ if needed
668+
padlen = IPV6LENGTH+2 + (31*len(grouping))
669+
670+
# Handle hex formatting
671+
elif fmt_base in 'Xx':
672+
if self._version == 4:
673+
# resulting string is '0x' + 8 hex digits
674+
# plus a single _ if needed
675+
padlen = int(IPV4LENGTH/4)+2 + len(grouping)
676+
elif self._version == 6:
677+
# resulting string is '0x' + 32 hex digits
678+
# plus 7 _ if needed
679+
padlen = int(IPV6LENGTH/4)+2 + (7*len(grouping))
680+
681+
retstr = f'{int(self):#0{padlen}{grouping}{fmt_base}}'
682+
683+
if fmt_base == 'X':
684+
retstr = retstr.upper()
685+
686+
# If alternate is not set, strip the two leftmost
687+
# characters ('0b')
688+
if not alternate:
689+
retstr = retstr[2:]
690+
691+
return retstr
692+
621693

622694
@functools.total_ordering
623695
class _BaseNetwork(_IPAddressBase):

Lib/test/test_ipaddress.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,31 @@ def assertBadLength(length):
174174
class AddressTestCase_v4(BaseTestCase, CommonTestMixin_v4):
175175
factory = ipaddress.IPv4Address
176176

177+
def test_format(self):
178+
v4 = ipaddress.IPv4Address("1.2.3.42")
179+
v4_pairs = [
180+
("b" ,"00000001000000100000001100101010"),
181+
("n" ,"00000001000000100000001100101010"),
182+
("x" ,"0102032a"),
183+
("X" ,"0102032A"),
184+
("_b" ,"0000_0001_0000_0010_0000_0011_0010_1010"),
185+
("_n" ,"0000_0001_0000_0010_0000_0011_0010_1010"),
186+
("_x" ,"0102_032a"),
187+
("_X" ,"0102_032A"),
188+
("#b" ,"0b00000001000000100000001100101010"),
189+
("#n" ,"0b00000001000000100000001100101010"),
190+
("#x" ,"0x0102032a"),
191+
("#X" ,"0X0102032A"),
192+
("#_b" ,"0b0000_0001_0000_0010_0000_0011_0010_1010"),
193+
("#_n" ,"0b0000_0001_0000_0010_0000_0011_0010_1010"),
194+
("#_x" ,"0x0102_032a"),
195+
("#_X" ,"0X0102_032A"),
196+
("s" ,"1.2.3.42"),
197+
("" ,"1.2.3.42"),
198+
]
199+
for (fmt, txt) in v4_pairs:
200+
self.assertEqual(txt, format(v4, fmt))
201+
177202
def test_network_passed_as_address(self):
178203
addr = "127.0.0.1/24"
179204
with self.assertAddressError("Unexpected '/' in %r", addr):
@@ -261,6 +286,47 @@ def test_weakref(self):
261286
class AddressTestCase_v6(BaseTestCase, CommonTestMixin_v6):
262287
factory = ipaddress.IPv6Address
263288

289+
def test_format(self):
290+
291+
v6 = ipaddress.IPv6Address("::1.2.3.42")
292+
v6_pairs = [
293+
("b",
294+
"000000000000000000000000000000000000000000000000000000"
295+
"000000000000000000000000000000000000000000000000010000"
296+
"00100000001100101010"),
297+
("n", "0000000000000000000000000102032a"),
298+
("x", "0000000000000000000000000102032a"),
299+
("X", "0000000000000000000000000102032A"),
300+
("_b",
301+
"0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000"
302+
"_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000"
303+
"_0000_0000_0000_0000_0001_0000_0010_0000_0011_0010"
304+
"_1010"),
305+
("_n", "0000_0000_0000_0000_0000_0000_0102_032a"),
306+
("_x", "0000_0000_0000_0000_0000_0000_0102_032a"),
307+
("_X", "0000_0000_0000_0000_0000_0000_0102_032A"),
308+
("#b",
309+
"0b0000000000000000000000000000000000000000000000000000"
310+
"000000000000000000000000000000000000000000000000000100"
311+
"0000100000001100101010"),
312+
("#n", "0x0000000000000000000000000102032a"),
313+
("#x", "0x0000000000000000000000000102032a"),
314+
("#X", "0X0000000000000000000000000102032A"),
315+
("#_b",
316+
"0b0000_0000_0000_0000_0000_0000_0000_0000_0000_0000"
317+
"_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000"
318+
"_0000_0000_0000_0000_0000_0001_0000_0010_0000_0011"
319+
"_0010_1010"),
320+
("#_n", "0x0000_0000_0000_0000_0000_0000_0102_032a"),
321+
("#_x", "0x0000_0000_0000_0000_0000_0000_0102_032a"),
322+
("#_X", "0X0000_0000_0000_0000_0000_0000_0102_032A"),
323+
("s", "::102:32a"),
324+
("", "::102:32a"),
325+
]
326+
327+
for (fmt, txt) in v6_pairs:
328+
self.assertEqual(txt, format(v6, fmt))
329+
264330
def test_network_passed_as_address(self):
265331
addr = "::1/24"
266332
with self.assertAddressError("Unexpected '/' in %r", addr):
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added __format__ to IPv4 and IPv6 classes. Always outputs a fully zero-
2+
padded string. Supports b/x/n modifiers (bin/hex/native format). Native
3+
format for IPv4 is bin, native format for IPv6 is hex. Also supports '#' and
4+
'_' modifiers.

0 commit comments

Comments
 (0)