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
diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py
index 873c764..c389f05 100644
--- a/Lib/ipaddress.py
+++ b/Lib/ipaddress.py
@@ -618,6 +618,78 @@
     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):