diff options
author | ewosborne <ewosborne@users.noreply.github.com> | 2019-09-12 05:03:31 -0400 |
---|---|---|
committer | Zachary Ware <zachary.ware@gmail.com> | 2019-09-12 10:03:31 +0100 |
commit | f9c95a4ba24c52eb1c052e3052d677e90a429a9a (patch) | |
tree | a8b9c9e60ca5807a2a3bf395dfadacefa78a08fe | |
parent | 92777d5e5aed1753bafe07265dbe98b2d271815b (diff) | |
download | cpython-git-f9c95a4ba24c52eb1c052e3052d677e90a429a9a.tar.gz |
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
-rw-r--r-- | Lib/ipaddress.py | 72 | ||||
-rw-r--r-- | Lib/test/test_ipaddress.py | 66 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2018-02-13-12-25-43.bpo-32820.0stF0u.rst | 4 |
3 files changed, 142 insertions, 0 deletions
diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index 873c764408..c389f05288 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -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): diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index de77111705..3a59a6102f 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -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): diff --git a/Misc/NEWS.d/next/Library/2018-02-13-12-25-43.bpo-32820.0stF0u.rst b/Misc/NEWS.d/next/Library/2018-02-13-12-25-43.bpo-32820.0stF0u.rst new file mode 100644 index 0000000000..dd5bd269cb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2018-02-13-12-25-43.bpo-32820.0stF0u.rst @@ -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. |