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
This commit is contained in:
parent
92777d5e5a
commit
f9c95a4ba2
|
@ -618,6 +618,78 @@ class _BaseAddress(_IPAddressBase):
|
|||
def __reduce__(self):
|
||||
return self.__class__, (self._ip,)
|
||||
|
||||
def __format__(self, fmt):
|
||||
"""Returns an IP address as a formatted string.
|
||||
|
||||
Supported presentation types are:
|
||||
's': returns the IP address as a string (default)
|
||||
'b' or 'n': converts to binary and returns a zero-padded string
|
||||
'X' or 'x': converts to upper- or lower-case hex and returns a zero-padded string
|
||||
|
||||
For binary and hex presentation types, the alternate form specifier
|
||||
'#' and the grouping option '_' are supported.
|
||||
"""
|
||||
|
||||
|
||||
# Support string formatting
|
||||
if not fmt or fmt[-1] == 's':
|
||||
# let format() handle it
|
||||
return format(str(self), fmt)
|
||||
|
||||
# From here on down, support for 'bnXx'
|
||||
|
||||
import re
|
||||
fmt_re = '^(?P<alternate>#?)(?P<grouping>_?)(?P<fmt_base>[xbnX]){1}$'
|
||||
m = re.match(fmt_re, fmt)
|
||||
if not m:
|
||||
return super().__format__(fmt)
|
||||
|
||||
groupdict = m.groupdict()
|
||||
alternate = groupdict['alternate']
|
||||
grouping = groupdict['grouping']
|
||||
fmt_base = groupdict['fmt_base']
|
||||
|
||||
# Set some defaults
|
||||
if fmt_base == 'n':
|
||||
if self._version == 4:
|
||||
fmt_base = 'b' # Binary is default for ipv4
|
||||
if self._version == 6:
|
||||
fmt_base = 'x' # Hex is default for ipv6
|
||||
|
||||
# Handle binary formatting
|
||||
if fmt_base == 'b':
|
||||
if self._version == 4:
|
||||
# resulting string is '0b' + 32 bits
|
||||
# plus 7 _ if needed
|
||||
padlen = IPV4LENGTH+2 + (7*len(grouping))
|
||||
elif self._version == 6:
|
||||
# resulting string is '0b' + 128 bits
|
||||
# plus 31 _ if needed
|
||||
padlen = IPV6LENGTH+2 + (31*len(grouping))
|
||||
|
||||
# Handle hex formatting
|
||||
elif fmt_base in 'Xx':
|
||||
if self._version == 4:
|
||||
# resulting string is '0x' + 8 hex digits
|
||||
# plus a single _ if needed
|
||||
padlen = int(IPV4LENGTH/4)+2 + len(grouping)
|
||||
elif self._version == 6:
|
||||
# resulting string is '0x' + 32 hex digits
|
||||
# plus 7 _ if needed
|
||||
padlen = int(IPV6LENGTH/4)+2 + (7*len(grouping))
|
||||
|
||||
retstr = f'{int(self):#0{padlen}{grouping}{fmt_base}}'
|
||||
|
||||
if fmt_base == 'X':
|
||||
retstr = retstr.upper()
|
||||
|
||||
# If alternate is not set, strip the two leftmost
|
||||
# characters ('0b')
|
||||
if not alternate:
|
||||
retstr = retstr[2:]
|
||||
|
||||
return retstr
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class _BaseNetwork(_IPAddressBase):
|
||||
|
|
|
@ -174,6 +174,31 @@ class CommonTestMixin_v6(CommonTestMixin):
|
|||
class AddressTestCase_v4(BaseTestCase, CommonTestMixin_v4):
|
||||
factory = ipaddress.IPv4Address
|
||||
|
||||
def test_format(self):
|
||||
v4 = ipaddress.IPv4Address("1.2.3.42")
|
||||
v4_pairs = [
|
||||
("b" ,"00000001000000100000001100101010"),
|
||||
("n" ,"00000001000000100000001100101010"),
|
||||
("x" ,"0102032a"),
|
||||
("X" ,"0102032A"),
|
||||
("_b" ,"0000_0001_0000_0010_0000_0011_0010_1010"),
|
||||
("_n" ,"0000_0001_0000_0010_0000_0011_0010_1010"),
|
||||
("_x" ,"0102_032a"),
|
||||
("_X" ,"0102_032A"),
|
||||
("#b" ,"0b00000001000000100000001100101010"),
|
||||
("#n" ,"0b00000001000000100000001100101010"),
|
||||
("#x" ,"0x0102032a"),
|
||||
("#X" ,"0X0102032A"),
|
||||
("#_b" ,"0b0000_0001_0000_0010_0000_0011_0010_1010"),
|
||||
("#_n" ,"0b0000_0001_0000_0010_0000_0011_0010_1010"),
|
||||
("#_x" ,"0x0102_032a"),
|
||||
("#_X" ,"0X0102_032A"),
|
||||
("s" ,"1.2.3.42"),
|
||||
("" ,"1.2.3.42"),
|
||||
]
|
||||
for (fmt, txt) in v4_pairs:
|
||||
self.assertEqual(txt, format(v4, fmt))
|
||||
|
||||
def test_network_passed_as_address(self):
|
||||
addr = "127.0.0.1/24"
|
||||
with self.assertAddressError("Unexpected '/' in %r", addr):
|
||||
|
@ -261,6 +286,47 @@ class AddressTestCase_v4(BaseTestCase, CommonTestMixin_v4):
|
|||
class AddressTestCase_v6(BaseTestCase, CommonTestMixin_v6):
|
||||
factory = ipaddress.IPv6Address
|
||||
|
||||
def test_format(self):
|
||||
|
||||
v6 = ipaddress.IPv6Address("::1.2.3.42")
|
||||
v6_pairs = [
|
||||
("b",
|
||||
"000000000000000000000000000000000000000000000000000000"
|
||||
"000000000000000000000000000000000000000000000000010000"
|
||||
"00100000001100101010"),
|
||||
("n", "0000000000000000000000000102032a"),
|
||||
("x", "0000000000000000000000000102032a"),
|
||||
("X", "0000000000000000000000000102032A"),
|
||||
("_b",
|
||||
"0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000"
|
||||
"_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000"
|
||||
"_0000_0000_0000_0000_0001_0000_0010_0000_0011_0010"
|
||||
"_1010"),
|
||||
("_n", "0000_0000_0000_0000_0000_0000_0102_032a"),
|
||||
("_x", "0000_0000_0000_0000_0000_0000_0102_032a"),
|
||||
("_X", "0000_0000_0000_0000_0000_0000_0102_032A"),
|
||||
("#b",
|
||||
"0b0000000000000000000000000000000000000000000000000000"
|
||||
"000000000000000000000000000000000000000000000000000100"
|
||||
"0000100000001100101010"),
|
||||
("#n", "0x0000000000000000000000000102032a"),
|
||||
("#x", "0x0000000000000000000000000102032a"),
|
||||
("#X", "0X0000000000000000000000000102032A"),
|
||||
("#_b",
|
||||
"0b0000_0000_0000_0000_0000_0000_0000_0000_0000_0000"
|
||||
"_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000"
|
||||
"_0000_0000_0000_0000_0000_0001_0000_0010_0000_0011"
|
||||
"_0010_1010"),
|
||||
("#_n", "0x0000_0000_0000_0000_0000_0000_0102_032a"),
|
||||
("#_x", "0x0000_0000_0000_0000_0000_0000_0102_032a"),
|
||||
("#_X", "0X0000_0000_0000_0000_0000_0000_0102_032A"),
|
||||
("s", "::102:32a"),
|
||||
("", "::102:32a"),
|
||||
]
|
||||
|
||||
for (fmt, txt) in v6_pairs:
|
||||
self.assertEqual(txt, format(v6, fmt))
|
||||
|
||||
def test_network_passed_as_address(self):
|
||||
addr = "::1/24"
|
||||
with self.assertAddressError("Unexpected '/' in %r", addr):
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Added __format__ to IPv4 and IPv6 classes. Always outputs a fully zero-
|
||||
padded string. Supports b/x/n modifiers (bin/hex/native format). Native
|
||||
format for IPv4 is bin, native format for IPv6 is hex. Also supports '#' and
|
||||
'_' modifiers.
|
Loading…
Reference in New Issue