[Python-checkins] bpo-32820: __format__ method for ipaddress (#5627)

Zachary Ware webhook-mailer at python.org
Thu Sep 12 05:03:35 EDT 2019


https://github.com/python/cpython/commit/f9c95a4ba24c52eb1c052e3052d677e90a429a9a
commit: f9c95a4ba24c52eb1c052e3052d677e90a429a9a
branch: master
author: ewosborne <ewosborne at users.noreply.github.com>
committer: Zachary Ware <zachary.ware at gmail.com>
date: 2019-09-12T10:03:31+01:00
summary:

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

files:
A Misc/NEWS.d/next/Library/2018-02-13-12-25-43.bpo-32820.0stF0u.rst
M Lib/ipaddress.py
M Lib/test/test_ipaddress.py

diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py
index 873c7644081a..c389f0528823 100644
--- a/Lib/ipaddress.py
+++ b/Lib/ipaddress.py
@@ -618,6 +618,78 @@ def _get_address_key(self):
     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 de77111705b6..3a59a6102f4c 100644
--- a/Lib/test/test_ipaddress.py
+++ b/Lib/test/test_ipaddress.py
@@ -174,6 +174,31 @@ def assertBadLength(length):
 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 @@ def test_weakref(self):
 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 000000000000..dd5bd269cbf7
--- /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.



More information about the Python-checkins mailing list