summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorewosborne <ewosborne@users.noreply.github.com>2019-09-12 05:03:31 -0400
committerZachary Ware <zachary.ware@gmail.com>2019-09-12 10:03:31 +0100
commitf9c95a4ba24c52eb1c052e3052d677e90a429a9a (patch)
treea8b9c9e60ca5807a2a3bf395dfadacefa78a08fe
parent92777d5e5aed1753bafe07265dbe98b2d271815b (diff)
downloadcpython-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.py72
-rw-r--r--Lib/test/test_ipaddress.py66
-rw-r--r--Misc/NEWS.d/next/Library/2018-02-13-12-25-43.bpo-32820.0stF0u.rst4
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.