[Python-checkins] cpython: Issue 14814: Provide more informative error messages in ipaddress, and ensure

nick.coghlan python-checkins at python.org
Sat Jul 7 11:31:40 CEST 2012


http://hg.python.org/cpython/rev/16ff4889a858
changeset:   77960:16ff4889a858
user:        Nick Coghlan <ncoghlan at gmail.com>
date:        Sat Jul 07 19:23:53 2012 +1000
summary:
  Issue 14814: Provide more informative error messages in ipaddress, and ensure that errors are caught as expected

files:
  Lib/ipaddress.py           |  141 ++++--
  Lib/test/test_ipaddress.py |  553 ++++++++++++++++++------
  Misc/NEWS                  |    4 +
  3 files changed, 502 insertions(+), 196 deletions(-)


diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py
--- a/Lib/ipaddress.py
+++ b/Lib/ipaddress.py
@@ -17,7 +17,6 @@
 IPV4LENGTH = 32
 IPV6LENGTH = 128
 
-
 class AddressValueError(ValueError):
     """A Value Error related to the address."""
 
@@ -117,7 +116,7 @@
     except (AddressValueError, NetmaskValueError):
         pass
 
-    raise ValueError('%r does not appear to be an IPv4 or IPv6 network' %
+    raise ValueError('%r does not appear to be an IPv4 or IPv6 interface' %
                      address)
 
 
@@ -157,6 +156,13 @@
         raise ValueError("Address negative or too large for IPv6")
 
 
+def _split_optional_netmask(address):
+    """Helper to split the netmask and raise AddressValueError if needed"""
+    addr = str(address).split('/')
+    if len(addr) > 2:
+        raise AddressValueError("Only one '/' permitted in %r" % address)
+    return addr
+
 def _find_address_range(addresses):
     """Find a sequence of IPv#Address.
 
@@ -481,7 +487,7 @@
     def __init__(self, address):
         if (not isinstance(address, bytes)
             and '/' in str(address)):
-            raise AddressValueError(address)
+            raise AddressValueError("Unexpected '/' in %r" % address)
 
     def __index__(self):
         return self._ip
@@ -1014,16 +1020,19 @@
             AddressValueError: if ip_str isn't a valid IPv4 Address.
 
         """
+        if not ip_str:
+            raise AddressValueError('Address cannot be empty')
+
         octets = ip_str.split('.')
         if len(octets) != 4:
-            raise AddressValueError(ip_str)
+            raise AddressValueError("Expected 4 octets in %r" % ip_str)
 
         packed_ip = 0
         for oc in octets:
             try:
                 packed_ip = (packed_ip << 8) | self._parse_octet(oc)
-            except ValueError:
-                raise AddressValueError(ip_str) from None
+            except ValueError as exc:
+                raise AddressValueError("%s in %r" % (exc, ip_str)) from None
         return packed_ip
 
     def _parse_octet(self, octet_str):
@@ -1039,15 +1048,21 @@
             ValueError: if the octet isn't strictly a decimal from [0..255].
 
         """
+        if not octet_str:
+            raise ValueError("Empty octet not permitted")
         # Whitelist the characters, since int() allows a lot of bizarre stuff.
-        # Higher level wrappers convert these to more informative errors
         if not self._DECIMAL_DIGITS.issuperset(octet_str):
-            raise ValueError
+            raise ValueError("Only decimal digits permitted in %r" % octet_str)
+        # Convert to integer (we know digits are legal)
         octet_int = int(octet_str, 10)
-        # Disallow leading zeroes, because no clear standard exists on
-        # whether these should be interpreted as decimal or octal.
-        if octet_int > 255 or (octet_str[0] == '0' and len(octet_str) > 1):
-            raise ValueError
+        # Any octets that look like they *might* be written in octal,
+        # and which don't look exactly the same in both octal and
+        # decimal are rejected as ambiguous
+        if octet_int > 7 and octet_str[0] == '0':
+            raise ValueError("Ambiguous leading zero in %r not permitted" %
+                              octet_str)
+        if octet_int > 255:
+            raise ValueError("Octet %d > 255 not permitted" % octet_int)
         return octet_int
 
     def _string_from_ip_int(self, ip_int):
@@ -1080,7 +1095,13 @@
         """
         mask = netmask.split('.')
         if len(mask) == 4:
-            if [x for x in mask if int(x) not in self._valid_mask_octets]:
+            for x in mask:
+                try:
+                    if int(x) in self._valid_mask_octets:
+                        continue
+                except ValueError:
+                    pass
+                # Found something that isn't an integer or isn't valid
                 return False
             if [y for idx, y in enumerate(mask) if idx > 0 and
                 y > mask[idx - 1]]:
@@ -1251,7 +1272,8 @@
         # Constructing from a packed address
         if isinstance(address, bytes):
             if len(address) != 4:
-                raise AddressValueError(address)
+                msg = "Packed address %r must be exactly 4 bytes"
+                raise AddressValueError(msg % address)
             self._ip = struct.unpack('!I', address)[0]
             return
 
@@ -1275,9 +1297,7 @@
             self._prefixlen = self._max_prefixlen
             return
 
-        addr = str(address).split('/')
-        if len(addr) > 2:
-            raise AddressValueError(address)
+        addr = _split_optional_netmask(address)
         IPv4Address.__init__(self, addr[0])
 
         self.network = IPv4Network(address, strict=False)
@@ -1382,7 +1402,8 @@
         # Constructing from a packed address
         if isinstance(address, bytes):
             if len(address) != 4:
-                raise AddressValueError(address)
+                msg = "Packed address %r must be exactly 4 bytes"
+                raise AddressValueError(msg % address)
             self.network_address = IPv4Address(
                 struct.unpack('!I', address)[0])
             self._prefixlen = self._max_prefixlen
@@ -1402,12 +1423,9 @@
 
         # Assume input argument to be string or any object representation
         # which converts into a formatted IP prefix string.
-        addr = str(address).split('/')
+        addr = _split_optional_netmask(address)
         self.network_address = IPv4Address(self._ip_int_from_string(addr[0]))
 
-        if len(addr) > 2:
-            raise AddressValueError(address)
-
         if len(addr) == 2:
             mask = addr[1].split('.')
 
@@ -1420,14 +1438,15 @@
                     self.netmask = IPv4Address(
                         self._ip_int_from_string(addr[1]) ^ self._ALL_ONES)
                 else:
-                    raise NetmaskValueError('%s is not a valid netmask'
+                    raise NetmaskValueError('%r is not a valid netmask'
                                                      % addr[1])
 
                 self._prefixlen = self._prefix_from_ip_int(int(self.netmask))
             else:
                 # We have a netmask in prefix length form.
                 if not self._is_valid_netmask(addr[1]):
-                    raise NetmaskValueError(addr[1])
+                    raise NetmaskValueError('%r is not a valid netmask'
+                                                     % addr[1])
                 self._prefixlen = int(addr[1])
                 self.netmask = IPv4Address(self._ip_int_from_prefix(
                     self._prefixlen))
@@ -1477,21 +1496,33 @@
             AddressValueError: if ip_str isn't a valid IPv6 Address.
 
         """
+        if not ip_str:
+            raise AddressValueError('Address cannot be empty')
+
         parts = ip_str.split(':')
 
         # An IPv6 address needs at least 2 colons (3 parts).
-        if len(parts) < 3:
-            raise AddressValueError(ip_str)
+        _min_parts = 3
+        if len(parts) < _min_parts:
+            msg = "At least %d parts expected in %r" % (_min_parts, ip_str)
+            raise AddressValueError(msg)
 
         # If the address has an IPv4-style suffix, convert it to hexadecimal.
         if '.' in parts[-1]:
-            ipv4_int = IPv4Address(parts.pop())._ip
+            try:
+                ipv4_int = IPv4Address(parts.pop())._ip
+            except AddressValueError as exc:
+                raise AddressValueError("%s in %r" % (exc, ip_str)) from None
             parts.append('%x' % ((ipv4_int >> 16) & 0xFFFF))
             parts.append('%x' % (ipv4_int & 0xFFFF))
 
         # An IPv6 address can't have more than 8 colons (9 parts).
-        if len(parts) > self._HEXTET_COUNT + 1:
-            raise AddressValueError(ip_str)
+        # The extra colon comes from using the "::" notation for a single
+        # leading or trailing zero part.
+        _max_parts = self._HEXTET_COUNT + 1
+        if len(parts) > _max_parts:
+            msg = "At most %d colons permitted in %r" % (_max_parts-1, ip_str)
+            raise AddressValueError(msg)
 
         # Disregarding the endpoints, find '::' with nothing in between.
         # This indicates that a run of zeroes has been skipped.
@@ -1501,7 +1532,8 @@
                 [None])
         except ValueError:
             # Can't have more than one '::'
-            raise AddressValueError(ip_str) from None
+            msg = "At most one '::' permitted in %r" % ip_str
+            raise AddressValueError(msg) from None
 
         # parts_hi is the number of parts to copy from above/before the '::'
         # parts_lo is the number of parts to copy from below/after the '::'
@@ -1512,20 +1544,30 @@
             if not parts[0]:
                 parts_hi -= 1
                 if parts_hi:
-                    raise AddressValueError(ip_str)  # ^: requires ^::
+                    msg = "Leading ':' only permitted as part of '::' in %r"
+                    raise AddressValueError(msg % ip_str)  # ^: requires ^::
             if not parts[-1]:
                 parts_lo -= 1
                 if parts_lo:
-                    raise AddressValueError(ip_str)  # :$ requires ::$
+                    msg = "Trailing ':' only permitted as part of '::' in %r"
+                    raise AddressValueError(msg % ip_str)  # :$ requires ::$
             parts_skipped = self._HEXTET_COUNT - (parts_hi + parts_lo)
             if parts_skipped < 1:
-                raise AddressValueError(ip_str)
+                msg = "Expected at most %d other parts with '::' in %r"
+                raise AddressValueError(msg % (self._HEXTET_COUNT-1, ip_str))
         else:
             # Otherwise, allocate the entire address to parts_hi.  The
             # endpoints could still be empty, but _parse_hextet() will check
             # for that.
             if len(parts) != self._HEXTET_COUNT:
-                raise AddressValueError(ip_str)
+                msg = "Exactly %d parts expected without '::' in %r"
+                raise AddressValueError(msg % (self._HEXTET_COUNT, ip_str))
+            if not parts[0]:
+                msg = "Leading ':' only permitted as part of '::' in %r"
+                raise AddressValueError(msg % ip_str)  # ^: requires ^::
+            if not parts[-1]:
+                msg = "Trailing ':' only permitted as part of '::' in %r"
+                raise AddressValueError(msg % ip_str)  # :$ requires ::$
             parts_hi = len(parts)
             parts_lo = 0
             parts_skipped = 0
@@ -1541,8 +1583,8 @@
                 ip_int <<= 16
                 ip_int |= self._parse_hextet(parts[i])
             return ip_int
-        except ValueError:
-            raise AddressValueError(ip_str) from None
+        except ValueError as exc:
+            raise AddressValueError("%s in %r" % (exc, ip_str)) from None
 
     def _parse_hextet(self, hextet_str):
         """Convert an IPv6 hextet string into an integer.
@@ -1561,12 +1603,14 @@
         # Whitelist the characters, since int() allows a lot of bizarre stuff.
         # Higher level wrappers convert these to more informative errors
         if not self._HEX_DIGITS.issuperset(hextet_str):
-            raise ValueError
+            raise ValueError("Only hex digits permitted in %r" % hextet_str)
         if len(hextet_str) > 4:
-            raise ValueError
+            msg = "At most 4 characters permitted in %r"
+            raise ValueError(msg % hextet_str)
         hextet_int = int(hextet_str, 16)
         if hextet_int > 0xFFFF:
-            raise ValueError
+            # This is unreachable due to the string length check above
+            raise ValueError("Part %d > 0xFFFF not permitted" % hextet_int)
         return hextet_int
 
     def _compress_hextets(self, hextets):
@@ -1869,7 +1913,8 @@
         # Constructing from a packed address
         if isinstance(address, bytes):
             if len(address) != 16:
-                raise AddressValueError(address)
+                msg = "Packed address %r must be exactly 16 bytes"
+                raise AddressValueError(msg % address)
             tmp = struct.unpack('!QQ', address)
             self._ip = (tmp[0] << 64) | tmp[1]
             return
@@ -1877,9 +1922,6 @@
         # Assume input argument to be string or any object representation
         # which converts into a formatted IP string.
         addr_str = str(address)
-        if not addr_str:
-            raise AddressValueError('')
-
         self._ip = self._ip_int_from_string(addr_str)
 
     @property
@@ -1897,7 +1939,7 @@
             self._prefixlen = self._max_prefixlen
             return
 
-        addr = str(address).split('/')
+        addr = _split_optional_netmask(address)
         IPv6Address.__init__(self, addr[0])
         self.network = IPv6Network(address, strict=False)
         self.netmask = self.network.netmask
@@ -2003,7 +2045,8 @@
         # Constructing from a packed address
         if isinstance(address, bytes):
             if len(address) != 16:
-                raise AddressValueError(address)
+                msg = "Packed address %r must be exactly 16 bytes"
+                raise AddressValueError(msg % address)
             tmp = struct.unpack('!QQ', address)
             self.network_address = IPv6Address((tmp[0] << 64) | tmp[1])
             self._prefixlen = self._max_prefixlen
@@ -2012,10 +2055,7 @@
 
         # Assume input argument to be string or any object representation
         # which converts into a formatted IP prefix string.
-        addr = str(address).split('/')
-
-        if len(addr) > 2:
-            raise AddressValueError(address)
+        addr = _split_optional_netmask(address)
 
         self.network_address = IPv6Address(self._ip_int_from_string(addr[0]))
 
@@ -2023,7 +2063,8 @@
             if self._is_valid_netmask(addr[1]):
                 self._prefixlen = int(addr[1])
             else:
-                raise NetmaskValueError(addr[1])
+                raise NetmaskValueError('%r is not a valid netmask'
+                                                     % addr[1])
         else:
             self._prefixlen = self._max_prefixlen
 
diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py
--- a/Lib/test/test_ipaddress.py
+++ b/Lib/test/test_ipaddress.py
@@ -5,8 +5,415 @@
 
 
 import unittest
+import re
+import contextlib
 import ipaddress
 
+class ErrorReporting(unittest.TestCase):
+    # One big change in ipaddress over the original ipaddr module is
+    # error reporting that tries to assume users *don't know the rules*
+    # for what constitutes an RFC compliant IP address
+
+    # Note that if the constructors are refactored so that addresses with
+    # multiple problems get classified differently, that's OK - just
+    # move the affected examples to the newly appropriate test case.
+
+    # The error reporting tests also cover a few corner cases that
+    # specifically *aren't* errors (such as leading zeroes in v6
+    # address parts) that don't have an obvious home in the main test
+    # suite
+
+    @contextlib.contextmanager
+    def assertCleanError(self, exc_type, details, *args):
+        """
+        Ensure exception does not display a context by default
+
+        Wraps unittest.TestCase.assertRaisesRegex
+        """
+        if args:
+            details = details % args
+        cm = self.assertRaisesRegex(exc_type, details)
+        with cm as exc:
+            yield exc
+        # Ensure we produce clean tracebacks on failure
+        if exc.exception.__context__ is not None:
+            self.assertTrue(exc.exception.__suppress_context__)
+
+    def assertAddressError(self, details, *args):
+        """Ensure a clean AddressValueError"""
+        return self.assertCleanError(ipaddress.AddressValueError,
+                                       details, *args)
+
+    def assertNetmaskError(self, details, *args):
+        """Ensure a clean NetmaskValueError"""
+        return self.assertCleanError(ipaddress.NetmaskValueError,
+                                details, *args)
+
+class AddressErrors_v4(ErrorReporting):
+
+    def test_empty_address(self):
+        with self.assertAddressError("Address cannot be empty"):
+            ipaddress.IPv4Address("")
+
+    def test_network_passed_as_address(self):
+        addr = "127.0.0.1/24"
+        with self.assertAddressError("Unexpected '/' in %r", addr):
+            ipaddress.IPv4Address(addr)
+
+    def test_bad_address_split(self):
+        def assertBadSplit(addr):
+            with self.assertAddressError("Expected 4 octets in %r", addr):
+                ipaddress.IPv4Address(addr)
+
+        assertBadSplit("127.0.1")
+        assertBadSplit("42.42.42.42.42")
+        assertBadSplit("42.42.42")
+        assertBadSplit("42.42")
+        assertBadSplit("42")
+        assertBadSplit("42..42.42.42")
+        assertBadSplit("42.42.42.42.")
+        assertBadSplit("42.42.42.42...")
+        assertBadSplit(".42.42.42.42")
+        assertBadSplit("...42.42.42.42")
+        assertBadSplit("016.016.016")
+        assertBadSplit("016.016")
+        assertBadSplit("016")
+        assertBadSplit("000")
+        assertBadSplit("0x0a.0x0a.0x0a")
+        assertBadSplit("0x0a.0x0a")
+        assertBadSplit("0x0a")
+        assertBadSplit(".")
+        assertBadSplit("bogus")
+        assertBadSplit("bogus.com")
+        assertBadSplit("1000")
+        assertBadSplit("1000000000000000")
+        assertBadSplit("192.168.0.1.com")
+
+    def test_empty_octet(self):
+        def assertBadOctet(addr):
+            with self.assertAddressError("Empty octet not permitted in %r",
+                                          addr):
+                ipaddress.IPv4Address(addr)
+
+        assertBadOctet("42..42.42")
+        assertBadOctet("...")
+
+    def test_invalid_characters(self):
+        def assertBadOctet(addr, octet):
+            msg = "Only decimal digits permitted in %r in %r" % (octet, addr)
+            with self.assertAddressError(re.escape(msg)):
+                ipaddress.IPv4Address(addr)
+
+        assertBadOctet("0x0a.0x0a.0x0a.0x0a", "0x0a")
+        assertBadOctet("42.42.42.-0", "-0")
+        assertBadOctet("42.42.42.+0", "+0")
+        assertBadOctet("42.42.42.-42", "-42")
+        assertBadOctet("+1.+2.+3.4", "+1")
+        assertBadOctet("1.2.3.4e0", "4e0")
+        assertBadOctet("1.2.3.4::", "4::")
+        assertBadOctet("1.a.2.3", "a")
+
+    def test_leading_zeros(self):
+        def assertBadOctet(addr, octet):
+            msg = "Ambiguous leading zero in %r not permitted in %r"
+            with self.assertAddressError(msg, octet, addr):
+                ipaddress.IPv4Address(addr)
+
+        assertBadOctet("016.016.016.016", "016")
+        assertBadOctet("001.000.008.016", "008")
+        self.assertEqual(ipaddress.IPv4Address("192.168.000.001"),
+                         ipaddress.IPv4Address("192.168.0.1"))
+
+    def test_octet_limit(self):
+        def assertBadOctet(addr, octet):
+            msg = "Octet %d > 255 not permitted in %r"
+            with self.assertAddressError(msg, octet, addr):
+                ipaddress.IPv4Address(addr)
+
+        assertBadOctet("12345.67899.-54321.-98765", 12345)
+        assertBadOctet("257.0.0.0", 257)
+
+    def test_bad_packed_length(self):
+        def assertBadLength(length):
+            addr = b'\x00' * length
+            msg = "Packed address %r must be exactly 4 bytes" % addr
+            with self.assertAddressError(re.escape(msg)):
+                ipaddress.IPv4Address(addr)
+
+        assertBadLength(3)
+        assertBadLength(5)
+
+
+class AddressErrors_v6(ErrorReporting):
+
+    def test_empty_address(self):
+        with self.assertAddressError("Address cannot be empty"):
+            ipaddress.IPv6Address("")
+
+    def test_network_passed_as_address(self):
+        addr = "::1/24"
+        with self.assertAddressError("Unexpected '/' in %r", addr):
+            ipaddress.IPv6Address(addr)
+
+    def test_bad_address_split_v6_not_enough_parts(self):
+        def assertBadSplit(addr):
+            msg = "At least 3 parts expected in %r"
+            with self.assertAddressError(msg, addr):
+                ipaddress.IPv6Address(addr)
+
+        assertBadSplit(":")
+        assertBadSplit(":1")
+        assertBadSplit("FEDC:9878")
+
+    def test_bad_address_split_v6_too_many_colons(self):
+        def assertBadSplit(addr):
+            msg = "At most 8 colons permitted in %r"
+            with self.assertAddressError(msg, addr):
+                ipaddress.IPv6Address(addr)
+
+        assertBadSplit("9:8:7:6:5:4:3::2:1")
+        assertBadSplit("10:9:8:7:6:5:4:3:2:1")
+        assertBadSplit("::8:7:6:5:4:3:2:1")
+        assertBadSplit("8:7:6:5:4:3:2:1::")
+        # A trailing IPv4 address is two parts
+        assertBadSplit("10:9:8:7:6:5:4:3:42.42.42.42")
+
+    def test_bad_address_split_v6_too_many_parts(self):
+        def assertBadSplit(addr):
+            msg = "Exactly 8 parts expected without '::' in %r"
+            with self.assertAddressError(msg, addr):
+                ipaddress.IPv6Address(addr)
+
+        assertBadSplit("3ffe:0:0:0:0:0:0:0:1")
+        assertBadSplit("9:8:7:6:5:4:3:2:1")
+        assertBadSplit("7:6:5:4:3:2:1")
+        # A trailing IPv4 address is two parts
+        assertBadSplit("9:8:7:6:5:4:3:42.42.42.42")
+        assertBadSplit("7:6:5:4:3:42.42.42.42")
+
+    def test_bad_address_split_v6_too_many_parts_with_double_colon(self):
+        def assertBadSplit(addr):
+            msg = "Expected at most 7 other parts with '::' in %r"
+            with self.assertAddressError(msg, addr):
+                ipaddress.IPv6Address(addr)
+
+        assertBadSplit("1:2:3:4::5:6:7:8")
+
+    def test_bad_address_split_v6_repeated_double_colon(self):
+        def assertBadSplit(addr):
+            msg = "At most one '::' permitted in %r"
+            with self.assertAddressError(msg, addr):
+                ipaddress.IPv6Address(addr)
+
+        assertBadSplit("3ffe::1::1")
+        assertBadSplit("1::2::3::4:5")
+        assertBadSplit("2001::db:::1")
+        assertBadSplit("3ffe::1::")
+        assertBadSplit("::3ffe::1")
+        assertBadSplit(":3ffe::1::1")
+        assertBadSplit("3ffe::1::1:")
+        assertBadSplit(":3ffe::1::1:")
+        assertBadSplit(":::")
+        assertBadSplit('2001:db8:::1')
+
+    def test_bad_address_split_v6_leading_colon(self):
+        def assertBadSplit(addr):
+            msg = "Leading ':' only permitted as part of '::' in %r"
+            with self.assertAddressError(msg, addr):
+                ipaddress.IPv6Address(addr)
+
+        assertBadSplit(":2001:db8::1")
+        assertBadSplit(":1:2:3:4:5:6:7")
+        assertBadSplit(":1:2:3:4:5:6:")
+        assertBadSplit(":6:5:4:3:2:1::")
+
+    def test_bad_address_split_v6_trailing_colon(self):
+        def assertBadSplit(addr):
+            msg = "Trailing ':' only permitted as part of '::' in %r"
+            with self.assertAddressError(msg, addr):
+                ipaddress.IPv6Address(addr)
+
+        assertBadSplit("2001:db8::1:")
+        assertBadSplit("1:2:3:4:5:6:7:")
+        assertBadSplit("::1.2.3.4:")
+        assertBadSplit("::7:6:5:4:3:2:")
+
+    def test_bad_v4_part_in(self):
+        def assertBadAddressPart(addr, v4_error):
+            with self.assertAddressError("%s in %r", v4_error, addr):
+                ipaddress.IPv6Address(addr)
+
+        assertBadAddressPart("3ffe::1.net", "Expected 4 octets in '1.net'")
+        assertBadAddressPart("3ffe::127.0.1",
+                             "Expected 4 octets in '127.0.1'")
+        assertBadAddressPart("::1.2.3",
+                             "Expected 4 octets in '1.2.3'")
+        assertBadAddressPart("::1.2.3.4.5",
+                             "Expected 4 octets in '1.2.3.4.5'")
+        assertBadAddressPart("3ffe::1.1.1.net",
+                             "Only decimal digits permitted in 'net' "
+                             "in '1.1.1.net'")
+
+    def test_invalid_characters(self):
+        def assertBadPart(addr, part):
+            msg = "Only hex digits permitted in %r in %r" % (part, addr)
+            with self.assertAddressError(re.escape(msg)):
+                ipaddress.IPv6Address(addr)
+
+        assertBadPart("3ffe::goog", "goog")
+        assertBadPart("3ffe::-0", "-0")
+        assertBadPart("3ffe::+0", "+0")
+        assertBadPart("3ffe::-1", "-1")
+        assertBadPart("1.2.3.4::", "1.2.3.4")
+        assertBadPart('1234:axy::b', "axy")
+
+    def test_part_length(self):
+        def assertBadPart(addr, part):
+            msg = "At most 4 characters permitted in %r in %r"
+            with self.assertAddressError(msg, part, addr):
+                ipaddress.IPv6Address(addr)
+
+        assertBadPart("3ffe::10000", "10000")
+        assertBadPart("02001:db8::", "02001")
+        assertBadPart('2001:888888::1', "888888")
+
+    def test_bad_packed_length(self):
+        def assertBadLength(length):
+            addr = b'\x00' * length
+            msg = "Packed address %r must be exactly 16 bytes" % addr
+            with self.assertAddressError(re.escape(msg)):
+                ipaddress.IPv6Address(addr)
+
+        assertBadLength(15)
+        assertBadLength(17)
+
+
+class NetmaskErrorsMixin_v4:
+    """Input validation on interfaces and networks is very similar"""
+
+    @property
+    def factory(self):
+        raise NotImplementedError
+
+    def test_split_netmask(self):
+        addr = "1.2.3.4/32/24"
+        with self.assertAddressError("Only one '/' permitted in %r" % addr):
+            self.factory(addr)
+
+    def test_address_errors(self):
+        def assertBadAddress(addr, details):
+            with self.assertAddressError(details):
+                self.factory(addr)
+
+        assertBadAddress("", "Address cannot be empty")
+        assertBadAddress("bogus", "Expected 4 octets")
+        assertBadAddress("google.com", "Expected 4 octets")
+        assertBadAddress("10/8", "Expected 4 octets")
+        assertBadAddress("::1.2.3.4", "Only decimal digits")
+        assertBadAddress("1.2.3.256", "256 > 255")
+
+    def test_netmask_errors(self):
+        def assertBadNetmask(addr, netmask):
+            msg = "%r is not a valid netmask"
+            with self.assertNetmaskError(msg % netmask):
+                self.factory("%s/%s" % (addr, netmask))
+
+        assertBadNetmask("1.2.3.4", "")
+        assertBadNetmask("1.2.3.4", "33")
+        assertBadNetmask("1.2.3.4", "254.254.255.256")
+        assertBadNetmask("1.1.1.1", "240.255.0.0")
+        assertBadNetmask("1.1.1.1", "1.a.2.3")
+        assertBadNetmask("1.1.1.1", "pudding")
+
+    def test_bad_packed_length(self):
+        def assertBadLength(length):
+            addr = b'\x00' * length
+            msg = "Packed address %r must be exactly 4 bytes" % addr
+            with self.assertAddressError(re.escape(msg)):
+                self.factory(addr)
+
+        assertBadLength(3)
+        assertBadLength(5)
+
+
+class InterfaceErrors_v4(ErrorReporting, NetmaskErrorsMixin_v4):
+    factory = ipaddress.IPv4Interface
+
+class NetworkErrors_v4(ErrorReporting, NetmaskErrorsMixin_v4):
+    factory = ipaddress.IPv4Network
+
+
+class NetmaskErrorsMixin_v6:
+    """Input validation on interfaces and networks is very similar"""
+
+    @property
+    def factory(self):
+        raise NotImplementedError
+
+    def test_split_netmask(self):
+        addr = "cafe:cafe::/128/190"
+        with self.assertAddressError("Only one '/' permitted in %r" % addr):
+            self.factory(addr)
+
+    def test_address_errors(self):
+        def assertBadAddress(addr, details):
+            with self.assertAddressError(details):
+                self.factory(addr)
+
+        assertBadAddress("", "Address cannot be empty")
+        assertBadAddress("google.com", "At least 3 parts")
+        assertBadAddress("1.2.3.4", "At least 3 parts")
+        assertBadAddress("10/8", "At least 3 parts")
+        assertBadAddress("1234:axy::b", "Only hex digits")
+
+    def test_netmask_errors(self):
+        def assertBadNetmask(addr, netmask):
+            msg = "%r is not a valid netmask"
+            with self.assertNetmaskError(msg % netmask):
+                self.factory("%s/%s" % (addr, netmask))
+
+        assertBadNetmask("::1", "")
+        assertBadNetmask("::1", "::1")
+        assertBadNetmask("::1", "1::")
+        assertBadNetmask("::1", "129")
+        assertBadNetmask("::1", "pudding")
+
+    def test_bad_packed_length(self):
+        def assertBadLength(length):
+            addr = b'\x00' * length
+            msg = "Packed address %r must be exactly 16 bytes" % addr
+            with self.assertAddressError(re.escape(msg)):
+                self.factory(addr)
+
+        assertBadLength(15)
+        assertBadLength(17)
+
+
+class InterfaceErrors_v6(ErrorReporting, NetmaskErrorsMixin_v6):
+    factory = ipaddress.IPv6Interface
+
+class NetworkErrors_v6(ErrorReporting, NetmaskErrorsMixin_v6):
+    factory = ipaddress.IPv6Network
+
+
+class FactoryFunctionErrors(ErrorReporting):
+
+    def assertFactoryError(self, factory, kind):
+        """Ensure a clean ValueError with the expected message"""
+        addr = "camelot"
+        msg = '%r does not appear to be an IPv4 or IPv6 %s'
+        with self.assertCleanError(ValueError, msg, addr, kind):
+            factory(addr)
+
+    def test_ip_address(self):
+        self.assertFactoryError(ipaddress.ip_address, "address")
+
+    def test_ip_interface(self):
+        self.assertFactoryError(ipaddress.ip_interface, "interface")
+
+    def test_ip_network(self):
+        self.assertFactoryError(ipaddress.ip_network, "network")
+
 
 class IpaddrUnitTest(unittest.TestCase):
 
@@ -46,104 +453,6 @@
         self.assertRaises(ValueError, ipaddress.v6_int_to_packed,
                           2 ** ipaddress.IPV6LENGTH)
 
-    def testInvalidStringsInAddressFactory(self):
-        def AssertInvalidIP(ip_str):
-            with self.assertRaises(ValueError) as ex:
-                ipaddress.ip_address(ip_str)
-            self.assertIsNone(ex.exception.__context__)
-
-        AssertInvalidIP("")
-        AssertInvalidIP("016.016.016.016")
-        AssertInvalidIP("016.016.016")
-        AssertInvalidIP("016.016")
-        AssertInvalidIP("016")
-        AssertInvalidIP("000.000.000.000")
-        AssertInvalidIP("000")
-        AssertInvalidIP("0x0a.0x0a.0x0a.0x0a")
-        AssertInvalidIP("0x0a.0x0a.0x0a")
-        AssertInvalidIP("0x0a.0x0a")
-        AssertInvalidIP("0x0a")
-        AssertInvalidIP("42.42.42.42.42")
-        AssertInvalidIP("42.42.42")
-        AssertInvalidIP("42.42")
-        AssertInvalidIP("42")
-        AssertInvalidIP("42..42.42")
-        AssertInvalidIP("42..42.42.42")
-        AssertInvalidIP("42.42.42.42.")
-        AssertInvalidIP("42.42.42.42...")
-        AssertInvalidIP(".42.42.42.42")
-        AssertInvalidIP("...42.42.42.42")
-        AssertInvalidIP("42.42.42.-0")
-        AssertInvalidIP("42.42.42.+0")
-        AssertInvalidIP(".")
-        AssertInvalidIP("...")
-        AssertInvalidIP("bogus")
-        AssertInvalidIP("bogus.com")
-        AssertInvalidIP("192.168.0.1.com")
-        AssertInvalidIP("12345.67899.-54321.-98765")
-        AssertInvalidIP("257.0.0.0")
-        AssertInvalidIP("42.42.42.-42")
-        AssertInvalidIP("3ffe::1.net")
-        AssertInvalidIP("3ffe::1::1")
-        AssertInvalidIP("1::2::3::4:5")
-        AssertInvalidIP("::7:6:5:4:3:2:")
-        AssertInvalidIP(":6:5:4:3:2:1::")
-        AssertInvalidIP("2001::db:::1")
-        AssertInvalidIP("FEDC:9878")
-        AssertInvalidIP("+1.+2.+3.4")
-        AssertInvalidIP("1.2.3.4e0")
-        AssertInvalidIP("::7:6:5:4:3:2:1:0")
-        AssertInvalidIP("7:6:5:4:3:2:1:0::")
-        AssertInvalidIP("9:8:7:6:5:4:3::2:1")
-        AssertInvalidIP("0:1:2:3::4:5:6:7")
-        AssertInvalidIP("3ffe:0:0:0:0:0:0:0:1")
-        AssertInvalidIP("3ffe::10000")
-        AssertInvalidIP("3ffe::goog")
-        AssertInvalidIP("3ffe::-0")
-        AssertInvalidIP("3ffe::+0")
-        AssertInvalidIP("3ffe::-1")
-        AssertInvalidIP(":")
-        AssertInvalidIP(":::")
-        AssertInvalidIP("::1.2.3")
-        AssertInvalidIP("::1.2.3.4.5")
-        AssertInvalidIP("::1.2.3.4:")
-        AssertInvalidIP("1.2.3.4::")
-        AssertInvalidIP("2001:db8::1:")
-        AssertInvalidIP(":2001:db8::1")
-        AssertInvalidIP(":1:2:3:4:5:6:7")
-        AssertInvalidIP("1:2:3:4:5:6:7:")
-        AssertInvalidIP(":1:2:3:4:5:6:")
-        AssertInvalidIP("1000")
-        AssertInvalidIP("1000000000000000")
-        AssertInvalidIP("02001:db8::")
-        self.assertRaises(ValueError, ipaddress.ip_interface, 'bogus')
-
-    def testInvalidStringsInConstructors(self):
-        def AssertInvalidIP(ip_class, ip_str):
-            with self.assertRaises(ipaddress.AddressValueError) as ex:
-                ip_class(ip_str)
-            if ex.exception.__context__ is not None:
-                # Provide clean tracebacks by default
-                self.assertTrue(ex.exception.__suppress_context__)
-
-        AssertInvalidIP(ipaddress.IPv4Address, '127.0.0.1/32')
-        AssertInvalidIP(ipaddress.IPv4Address(1)._ip_int_from_string,
-                          '1.a.2.3')
-        AssertInvalidIP(ipaddress.IPv4Interface, '')
-        AssertInvalidIP(ipaddress.IPv4Interface, 'google.com')
-        AssertInvalidIP(ipaddress.IPv6Address, '1234:axy::b')
-        AssertInvalidIP(ipaddress.IPv6Address, '2001:db8:::1')
-        AssertInvalidIP(ipaddress.IPv6Address, '2001:888888::1')
-        AssertInvalidIP(ipaddress.IPv4Interface, '::1.2.3.4')
-        AssertInvalidIP(ipaddress.IPv6Interface, '')
-        AssertInvalidIP(ipaddress.IPv6Interface, 'google.com')
-        AssertInvalidIP(ipaddress.IPv6Interface, '1.2.3.4')
-        AssertInvalidIP(ipaddress.IPv6Interface, 'cafe:cafe::/128/190')
-        AssertInvalidIP(ipaddress.IPv6Interface, '1234:axy::b')
-
-    def testInvalidHostmask(self):
-        self.assertFalse(ipaddress.IPv4Interface(1)._is_hostmask('1.a.2.3'))
-
     def testInternals(self):
         first, last = ipaddress._find_address_range([
             ipaddress.IPv4Address('10.10.10.10'),
@@ -278,22 +587,6 @@
         self.assertEqual(address('::'),
                          address(b'\x00' * 16))
 
-    def testIpFromPackedErrors(self):
-        def assertInvalidPackedAddress(f, length):
-            self.assertRaises(ValueError, f, b'\x00' * length)
-        assertInvalidPackedAddress(ipaddress.ip_address, 3)
-        assertInvalidPackedAddress(ipaddress.ip_address, 5)
-        assertInvalidPackedAddress(ipaddress.ip_address, 15)
-        assertInvalidPackedAddress(ipaddress.ip_address, 17)
-        assertInvalidPackedAddress(ipaddress.ip_interface, 3)
-        assertInvalidPackedAddress(ipaddress.ip_interface, 5)
-        assertInvalidPackedAddress(ipaddress.ip_interface, 15)
-        assertInvalidPackedAddress(ipaddress.ip_interface, 17)
-        assertInvalidPackedAddress(ipaddress.ip_network, 3)
-        assertInvalidPackedAddress(ipaddress.ip_network, 5)
-        assertInvalidPackedAddress(ipaddress.ip_network, 15)
-        assertInvalidPackedAddress(ipaddress.ip_network, 17)
-
     def testGetIp(self):
         self.assertEqual(int(self.ipv4_interface.ip), 16909060)
         self.assertEqual(str(self.ipv4_interface.ip), '1.2.3.4')
@@ -508,38 +801,6 @@
         self.assertFalse(ipaddress.IPv4Network('1.1.0.0/16').__contains__(
                 ipaddress.IPv4Network('1.0.0.0/15')))
 
-    def testBadAddress(self):
-        self.assertRaises(ipaddress.AddressValueError, ipaddress.IPv4Interface,
-                          'poop')
-        self.assertRaises(ipaddress.AddressValueError,
-                          ipaddress.IPv4Interface, '1.2.3.256')
-
-        self.assertRaises(ipaddress.AddressValueError, ipaddress.IPv6Interface,
-                          'poopv6')
-        self.assertRaises(ipaddress.AddressValueError,
-                          ipaddress.IPv4Interface, '1.2.3.4/32/24')
-        self.assertRaises(ipaddress.AddressValueError,
-                          ipaddress.IPv4Network, '1.2.3.4/32/24')
-        self.assertRaises(ipaddress.AddressValueError,
-                          ipaddress.IPv4Interface, '10/8')
-        self.assertRaises(ipaddress.AddressValueError,
-                          ipaddress.IPv6Interface, '10/8')
-
-
-    def testBadNetMask(self):
-        self.assertRaises(ipaddress.NetmaskValueError,
-                          ipaddress.IPv4Interface, '1.2.3.4/')
-        self.assertRaises(ipaddress.NetmaskValueError,
-                          ipaddress.IPv4Interface, '1.2.3.4/33')
-        self.assertRaises(ipaddress.NetmaskValueError,
-                          ipaddress.IPv4Interface, '1.2.3.4/254.254.255.256')
-        self.assertRaises(ipaddress.NetmaskValueError,
-                          ipaddress.IPv4Interface, '1.1.1.1/240.255.0.0')
-        self.assertRaises(ipaddress.NetmaskValueError,
-                          ipaddress.IPv6Interface, '::1/')
-        self.assertRaises(ipaddress.NetmaskValueError,
-                          ipaddress.IPv6Interface, '::1/129')
-
     def testNth(self):
         self.assertEqual(str(self.ipv4_network[5]), '1.2.3.5')
         self.assertRaises(IndexError, self.ipv4_network.__getitem__, 256)
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -23,6 +23,10 @@
 Library
 -------
 
+- Issue #14814: ipaddress now provides more informative error messages when
+  constructing instances directly (changes permitted during beta due to
+  provisional API status)
+
 - Issue #15247: FileIO now raises an error when given a file descriptor
   pointing to a directory.
 

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list