diff options
author | Peter Moody <pmoody@google.com> | 2014-07-26 09:15:00 -0700 |
---|---|---|
committer | Peter Moody <pmoody@google.com> | 2014-07-26 09:15:00 -0700 |
commit | c813f4790d1092b1515ee18fe8270180ed3cc5cb (patch) | |
tree | b5347307a1109d0400bfdfa7671d28868d6e499f | |
parent | 669c0368e3e4ad5dbfd611640b461018592436b2 (diff) | |
download | ipaddr-py-c813f4790d1092b1515ee18fe8270180ed3cc5cb.tar.gz |
ipaddr: add the ability to create non-trivial IPv*Network objects via tuples.
patch by pmarks.
-rw-r--r-- | trunk/ipaddr.py | 217 | ||||
-rwxr-xr-x | trunk/ipaddr_test.py | 60 |
2 files changed, 195 insertions, 82 deletions
diff --git a/trunk/ipaddr.py b/trunk/ipaddr.py index c9aee63..33eb405 100644 --- a/trunk/ipaddr.py +++ b/trunk/ipaddr.py @@ -876,6 +876,26 @@ class _BaseNet(_IPAddrBase): else: raise NetmaskValueError('Bit pattern does not match /1*0*/') + def _prefix_from_prefix_int(self, prefixlen): + """Validate and return a prefix length integer. + + Args: + prefixlen: An integer containing the prefix length. + + Returns: + The input, possibly converted from long to int. + + Raises: + NetmaskValueError: If the input is not an integer, or out of range. + """ + if not isinstance(prefixlen, (int, long)): + raise NetmaskValueError('%r is not an integer' % prefixlen) + prefixlen = int(prefixlen) + if not (0 <= prefixlen <= self._max_prefixlen): + raise NetmaskValueError('%d is not a valid prefix length' % + prefixlen) + return prefixlen + def _prefix_from_prefix_string(self, prefixlen_str): """Turn a prefix length string into an integer. @@ -893,12 +913,10 @@ class _BaseNet(_IPAddrBase): if not _BaseV4._DECIMAL_DIGITS.issuperset(prefixlen_str): raise ValueError prefixlen = int(prefixlen_str) - if not (0 <= prefixlen <= self._max_prefixlen): - raise ValueError except ValueError: raise NetmaskValueError('%s is not a valid prefix length' % prefixlen_str) - return prefixlen + return self._prefix_from_prefix_int(prefixlen) def _prefix_from_ip_string(self, ip_str): """Turn a netmask/hostmask string into a prefix length. @@ -1239,6 +1257,11 @@ class IPv4Address(_BaseV4, _BaseIP): """ _BaseV4.__init__(self, address) + # Efficient copy constructor. + if isinstance(address, IPv4Address): + self._ip = address._ip + return + # Efficient constructor from integer. if isinstance(address, (int, long)): self._ip = address @@ -1279,29 +1302,32 @@ class IPv4Network(_BaseV4, _BaseNet): """Instantiate a new IPv4 network object. Args: - address: A string or integer representing the IP [& network]. - '192.168.1.1/24' - '192.168.1.1/255.255.255.0' - '192.168.1.1/0.0.0.255' - are all functionally the same in IPv4. Similarly, - '192.168.1.1' - '192.168.1.1/255.255.255.255' - '192.168.1.1/32' - are also functionaly equivalent. That is to say, failing to - provide a subnetmask will create an object with a mask of /32. - - If the mask (portion after the / in the argument) is given in - dotted quad form, it is treated as a netmask if it starts with a - non-zero field (e.g. /255.0.0.0 == /8) and as a hostmask if it - starts with a zero field (e.g. 0.255.255.255 == /8), with the - single exception of an all-zero mask which is treated as a - netmask == /0. If no mask is given, a default of /32 is used. - - Additionally, an integer can be passed, so - IPv4Network('192.168.1.1') == IPv4Network(3232235777). - or, more generally - IPv4Network(int(IPv4Network('192.168.1.1'))) == - IPv4Network('192.168.1.1') + address: The IPv4 network as a string, 2-tuple, or any format + supported by the IPv4Address constructor. + + Strings typically use CIDR format, such as '192.0.2.0/24'. + If a dotted-quad is provided after the '/', it is treated as + a netmask if it starts with a nonzero bit (e.g. 255.0.0.0 == /8) + or a hostmask if it starts with a zero bit + (e.g. /0.0.0.255 == /8), with the single exception of an all-zero + mask which is treated as /0. + + The 2-tuple format consists of an (ip, prefixlen), where ip is any + format recognized by the IPv4Address constructor, and prefixlen is + an integer from 0 through 32. + + A plain IPv4 address (in any format) will be forwarded to the + IPv4Address constructor, with an implied prefixlen of 32. + + For example, the following inputs are equivalent: + IPv4Network('192.0.2.1/32') + IPv4Network('192.0.2.1/255.255.255.255') + IPv4Network('192.0.2.1') + IPv4Network(0xc0000201) + IPv4Network(IPv4Address('192.0.2.1')) + IPv4Network(('192.0.2.1', 32)) + IPv4Network((0xc0000201, 32)) + IPv4Network((IPv4Address('192.0.2.1'), 32)) strict: A boolean. If true, ensure that we have been passed A true network address, eg, 192.168.1.0/24 and not an @@ -1318,41 +1344,51 @@ class IPv4Network(_BaseV4, _BaseNet): _BaseNet.__init__(self, address) _BaseV4.__init__(self, address) - # Constructing from an integer or packed bytes. - if isinstance(address, (int, long, Bytes)): + # Constructing from a single IP address. + if isinstance(address, (int, long, Bytes, IPv4Address)): self.ip = IPv4Address(address) self._ip = self.ip._ip self._prefixlen = self._max_prefixlen self.netmask = IPv4Address(self._ALL_ONES) return - # Assume input argument to be string or any object representation - # which converts into a formatted IP prefix string. - addr = str(address).split('/') + # Constructing from an (ip, prefixlen) tuple. + if isinstance(address, tuple): + try: + ip, prefixlen = address + except ValueError: + raise AddressValueError(address) + self.ip = IPv4Address(ip) + self._ip = self.ip._ip + self._prefixlen = self._prefix_from_prefix_int(prefixlen) - if len(addr) > 2: - raise AddressValueError(address) + else: + # Assume input argument to be string or any object representation + # which converts into a formatted IP prefix string. + addr = str(address).split('/') - self._ip = self._ip_int_from_string(addr[0]) - self.ip = IPv4Address(self._ip) + if len(addr) > 2: + raise AddressValueError(address) - if len(addr) == 2: - try: - # Check for a netmask in prefix length form. - self._prefixlen = self._prefix_from_prefix_string(addr[1]) - except NetmaskValueError: - # Check for a netmask or hostmask in dotted-quad form. - # This may raise NetmaskValueError. - self._prefixlen = self._prefix_from_ip_string(addr[1]) - else: - self._prefixlen = self._max_prefixlen + self._ip = self._ip_int_from_string(addr[0]) + self.ip = IPv4Address(self._ip) + + if len(addr) == 2: + try: + # Check for a netmask in prefix length form. + self._prefixlen = self._prefix_from_prefix_string(addr[1]) + except NetmaskValueError: + # Check for a netmask or hostmask in dotted-quad form. + # This may raise NetmaskValueError. + self._prefixlen = self._prefix_from_ip_string(addr[1]) + else: + self._prefixlen = self._max_prefixlen self.netmask = IPv4Address(self._ip_int_from_prefix(self._prefixlen)) if strict: if self.ip != self.network: - raise ValueError('%s has host bits set' % - self.ip) + raise ValueError('%s has host bits set' % self.ip) if self._prefixlen == (self._max_prefixlen - 1): self.iterhosts = self.__iter__ @@ -1752,6 +1788,11 @@ class IPv6Address(_BaseV6, _BaseIP): """ _BaseV6.__init__(self, address) + # Efficient copy constructor. + if isinstance(address, IPv6Address): + self._ip = address._ip + return + # Efficient constructor from integer. if isinstance(address, (int, long)): self._ip = address @@ -1771,9 +1812,6 @@ class IPv6Address(_BaseV6, _BaseIP): # 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) @@ -1793,28 +1831,34 @@ class IPv6Network(_BaseV6, _BaseNet): def __init__(self, address, strict=False): - """Instantiate a new IPv6 Network object. + """Instantiate a new IPv6 network object. Args: - address: A string or integer representing the IPv6 network or the IP - and prefix/netmask. - '2001:4860::/128' - '2001:4860:0000:0000:0000:0000:0000:0000/128' - '2001:4860::' - are all functionally the same in IPv6. That is to say, - failing to provide a subnetmask will create an object with - a mask of /128. + address: The IPv6 network as a string, 2-tuple, or any format + supported by the IPv6Address constructor. - Additionally, an integer can be passed, so - IPv6Network('2001:4860::') == - IPv6Network(42541956101370907050197289607612071936L). - or, more generally - IPv6Network(IPv6Network('2001:4860::')._ip) == - IPv6Network('2001:4860::') + Strings should be in CIDR format, such as '2001:db8::/32'. + + The 2-tuple format consists of an (ip, prefixlen), where ip is any + format recognized by the IPv6Address constructor, and prefixlen is + an integer from 0 through 128. + + A plain IPv6 address (in any format) will be forwarded to the + IPv6Address constructor, with an implied prefixlen of 128. + + For example, the following inputs are equivalent: + IPv6Network('2001:db8::/128') + IPv6Network('2001:db8:0:0:0:0:0:0/128') + IPv6Network('2001:db8::') + IPv6Network(0x20010db8 << 96) + IPv6Network(IPv6Address('2001:db8::')) + IPv6Network(('2001:db8::', 128)) + IPv6Network((0x20010db8 << 96, 128)) + IPv6Network((IPv6Address('2001:db8::'), 128)) strict: A boolean. If true, ensure that we have been passed - A true network address, eg, 192.168.1.0/24 and not an - IP address on a network, eg, 192.168.1.1/24. + A true network address, eg, 2001:db8::/32 and not an + IP address on a network, eg, 2001:db8::1/32. Raises: AddressValueError: If address isn't a valid IPv6 address. @@ -1827,29 +1871,40 @@ class IPv6Network(_BaseV6, _BaseNet): _BaseNet.__init__(self, address) _BaseV6.__init__(self, address) - # Constructing from an integer or packed bytes. - if isinstance(address, (int, long, Bytes)): + # Constructing from a single IP address. + if isinstance(address, (int, long, Bytes, IPv6Address)): self.ip = IPv6Address(address) self._ip = self.ip._ip self._prefixlen = self._max_prefixlen self.netmask = IPv6Address(self._ALL_ONES) return - # Assume input argument to be string or any object representation - # which converts into a formatted IP prefix string. - addr = str(address).split('/') + # Constructing from an (ip, prefixlen) tuple. + if isinstance(address, tuple): + try: + ip, prefixlen = address + except ValueError: + raise AddressValueError(address) + self.ip = IPv6Address(ip) + self._ip = self.ip._ip + self._prefixlen = self._prefix_from_prefix_int(prefixlen) - if len(addr) > 2: - raise AddressValueError(address) + else: + # Assume input argument to be string or any object representation + # which converts into a formatted IP prefix string. + addr = str(address).split('/') - self._ip = self._ip_int_from_string(addr[0]) - self.ip = IPv6Address(self._ip) + if len(addr) > 2: + raise AddressValueError(address) - if len(addr) == 2: - # This may raise NetmaskValueError - self._prefixlen = self._prefix_from_prefix_string(addr[1]) - else: - self._prefixlen = self._max_prefixlen + self._ip = self._ip_int_from_string(addr[0]) + self.ip = IPv6Address(self._ip) + + if len(addr) == 2: + # This may raise NetmaskValueError + self._prefixlen = self._prefix_from_prefix_string(addr[1]) + else: + self._prefixlen = self._max_prefixlen self.netmask = IPv6Address(self._ip_int_from_prefix(self._prefixlen)) diff --git a/trunk/ipaddr_test.py b/trunk/ipaddr_test.py index 89a6893..79a18c4 100755 --- a/trunk/ipaddr_test.py +++ b/trunk/ipaddr_test.py @@ -158,6 +158,14 @@ class IpaddrUnitTest(unittest.TestCase): self.assertRaises(ipaddr.AddressValueError, ipaddr.IPv4Address(1)._ip_int_from_string, '1.a.2.3') + self.assertRaises(ipaddr.AddressValueError, ipaddr.IPv4Network, + ('1.2.3.0',)) + self.assertRaises(ipaddr.AddressValueError, ipaddr.IPv6Network, + ('2001:db8::',)) + self.assertRaises(ipaddr.AddressValueError, ipaddr.IPv4Network, + ('1.2.3.0', 24, 0)) + self.assertRaises(ipaddr.AddressValueError, ipaddr.IPv6Network, + ('2001:db8::', 32, 0)) def testGetNetwork(self): self.assertEqual(int(self.ipv4.network), 16909056) @@ -443,6 +451,13 @@ class IpaddrUnitTest(unittest.TestCase): net = ipaddr.IPv4Network(net_str) self.assertEqual(str(net), net_str) + # Parse some 2-tuple inputs. + self.assertEqual(str(ipaddr.IPv4Network((0, i))), net_str) + self.assertEqual(str(ipaddr.IPv4Network(('0.0.0.0', i))), net_str) + self.assertEqual( + str(ipaddr.IPv4Network((ipaddr.IPAddress('0.0.0.0'), i))), + net_str) + # Generate and re-parse the expanded netmask. self.assertEqual( str(ipaddr.IPv4Network('0.0.0.0/%s' % net.netmask)), @@ -470,6 +485,13 @@ class IpaddrUnitTest(unittest.TestCase): net_str = '::/%d' % i self.assertEqual(str(ipaddr.IPv6Network(net_str)), net_str) + # Parse some 2-tuple inputs. + self.assertEqual(str(ipaddr.IPv6Network((0, i))), net_str) + self.assertEqual(str(ipaddr.IPv6Network(('::', i))), net_str) + self.assertEqual( + str(ipaddr.IPv6Network((ipaddr.IPAddress('::'), i))), + net_str) + # Zero prefix is treated as decimal. self.assertEqual(str(ipaddr.IPv6Network('::/0%d' % i)), net_str) @@ -479,12 +501,16 @@ class IpaddrUnitTest(unittest.TestCase): self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv4Network, '1.2.3.4/-1') self.assertRaises(ipaddr.NetmaskValueError, + ipaddr.IPv4Network, ('1.2.3.4', -1)) + self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv4Network, '1.2.3.4/+1') self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv4Network, '1.2.3.4/0x1') self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv4Network, '1.2.3.4/33') self.assertRaises(ipaddr.NetmaskValueError, + ipaddr.IPv4Network, ('1.2.3.4', 33)) + self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv4Network, '1.2.3.4/254.254.255.256') self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv4Network, '1.1.1.1/240.255.0.0') @@ -501,16 +527,43 @@ class IpaddrUnitTest(unittest.TestCase): self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv6Network, '::1/-1') self.assertRaises(ipaddr.NetmaskValueError, + ipaddr.IPv6Network, ('::1', -1)) + self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv6Network, '::1/+1') self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv6Network, '::1/0x1') self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv6Network, '::1/129') self.assertRaises(ipaddr.NetmaskValueError, + ipaddr.IPv6Network, ('::1', 129)) + self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv6Network, '::1/1.2.3.4') # IPv6 expanded form is currently not supported. self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv6Network, '::/::') + # The 2-tuple constructors only accept integer prefixlens for now. + self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv4Network, + ('0.0.0.0', '0')) + self.assertRaises(ipaddr.NetmaskValueError, ipaddr.IPv6Network, + ('::', '0')) + + def testCopyConstructors(self): + # Create address types with unparseable str() outputs, to ensure that + # the copy constructors aren't taking the slow path. + class BadStringIPv4Address(ipaddr.IPv4Address): + def __str__(self): + return "<IPv4>" + class BadStringIPv6Address(ipaddr.IPv6Address): + def __str__(self): + return "<IPv6>" + + v4addr = BadStringIPv4Address('1.2.3.4') + self.assertEqual("<IPv4>", str(v4addr)) + self.assertEqual(v4addr, ipaddr.IPv4Address(v4addr)) + + v6addr = BadStringIPv6Address('2001:db8::') + self.assertEqual("<IPv6>", str(v6addr)) + self.assertEqual(v6addr, ipaddr.IPv6Address(v6addr)) def testNth(self): self.assertEqual(str(self.ipv4[5]), '1.2.3.5') @@ -773,7 +826,12 @@ class IpaddrUnitTest(unittest.TestCase): def testStrictNetworks(self): self.assertRaises(ValueError, ipaddr.IPNetwork, '192.168.1.1/24', strict=True) - self.assertRaises(ValueError, ipaddr.IPNetwork, '::1/120', strict=True) + self.assertRaises(ValueError, ipaddr.IPNetwork, ('192.168.1.1', 24), + strict=True) + self.assertRaises(ValueError, ipaddr.IPNetwork, '::1/120', + strict=True) + self.assertRaises(ValueError, ipaddr.IPNetwork, ('::1', 120), + strict=True) def testOverlaps(self): other = ipaddr.IPv4Network('1.2.3.0/30') |