From: Nick Coghlan Date: Sat, 7 Jul 2012 09:23:53 +0000 (+1000) Subject: Issue 14814: Provide more informative error messages in ipaddress, and ensure that... X-Git-Tag: v3.3.0b2~309 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=36f8dcde067659cb94a6666d745c103e7251875b;p=python Issue 14814: Provide more informative error messages in ipaddress, and ensure that errors are caught as expected --- diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index bec504c4cd..6daa9559c8 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -17,7 +17,6 @@ import struct IPV4LENGTH = 32 IPV6LENGTH = 128 - class AddressValueError(ValueError): """A Value Error related to the address.""" @@ -117,7 +116,7 @@ def ip_interface(address): 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 @@ def v6_int_to_packed(address): 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 @@ class _BaseAddress(_IPAddressBase): 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 @@ class _BaseV4: 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 @@ class _BaseV4: 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 @@ class _BaseV4: """ 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 @@ class IPv4Address(_BaseV4, _BaseAddress): # 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 @@ class IPv4Interface(IPv4Address): 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 @@ class IPv4Network(_BaseV4, _BaseNetwork): # 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 @@ class IPv4Network(_BaseV4, _BaseNetwork): # 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 @@ class IPv4Network(_BaseV4, _BaseNetwork): 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 @@ class _BaseV6: 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 @@ class _BaseV6: [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 @@ class _BaseV6: 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 @@ class _BaseV6: 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 @@ class _BaseV6: # 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 @@ class IPv6Address(_BaseV6, _BaseAddress): # 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 @@ class IPv6Address(_BaseV6, _BaseAddress): # 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 @@ class IPv6Interface(IPv6Address): 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 @@ class IPv6Network(_BaseV6, _BaseNetwork): # 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 @@ class IPv6Network(_BaseV6, _BaseNetwork): # 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 @@ class IPv6Network(_BaseV6, _BaseNetwork): 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 index 5cd2ad4d19..61ec0d6e40 100644 --- 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 @@ class IpaddrUnitTest(unittest.TestCase): 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 @@ class IpaddrUnitTest(unittest.TestCase): 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 @@ class IpaddrUnitTest(unittest.TestCase): 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 index f7ccffa271..2d17eb3e57 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -23,6 +23,10 @@ Core and Builtins 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.