From 91dc64ba3f51100540b2ab6c6cd72c3bb18a6d49 Mon Sep 17 00:00:00 2001 From: Cheryl Sabella Date: Sun, 22 Oct 2017 17:39:49 -0400 Subject: [PATCH] bpo-20825: Containment test for ip_network in ip_network. --- Doc/library/ipaddress.rst | 24 +++++ Lib/ipaddress.py | 29 +++++-- Lib/test/test_ipaddress.py | 87 ++++++++++++++++++- .../2017-10-21-09-13-16.bpo-20825.-1MBEy.rst | 3 + 4 files changed, 136 insertions(+), 7 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2017-10-21-09-13-16.bpo-20825.-1MBEy.rst diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst index 36c6859d701..75c9107bd5e 100644 --- a/Doc/library/ipaddress.rst +++ b/Doc/library/ipaddress.rst @@ -543,6 +543,28 @@ so to avoid duplication they are only documented for :class:`IPv4Network`. >>> ip_network('192.0.2.0/24').supernet(new_prefix=20) IPv4Network('192.0.0.0/20') + .. method:: subnet_of(other) + + Returns *True* if this network is a subnet of *other*. + + >>> a = ip_network('192.168.1.0/24') + >>> b = ip_network('192.168.1.128/30') + >>> b.subnet_of(a) + True + + .. versionadded:: 3.7 + + .. method:: supernet_of(other) + + Returns *True* if this network is a supernet of *other*. + + >>> a = ip_network('192.168.1.0/24') + >>> b = ip_network('192.168.1.128/30') + >>> a.supernet_of(b) + True + + .. versionadded:: 3.7 + .. method:: compare_networks(other) Compare this network to *other*. In this comparison only the network @@ -621,6 +643,8 @@ so to avoid duplication they are only documented for :class:`IPv4Network`. .. method:: address_exclude(network) .. method:: subnets(prefixlen_diff=1, new_prefix=None) .. method:: supernet(prefixlen_diff=1, new_prefix=None) + .. method:: subnet_of(other) + .. method:: supernet_of(other) .. method:: compare_networks(other) Refer to the corresponding attribute documentation in diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index 70746f8de85..e8ce4cef2de 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -776,8 +776,7 @@ class _BaseNetwork(_IPAddressBase): if not isinstance(other, _BaseNetwork): raise TypeError("%s is not a network object" % other) - if not (other.network_address >= self.network_address and - other.broadcast_address <= self.broadcast_address): + if not other.subnet_of(self): raise ValueError('%s not contained in %s' % (other, self)) if other == self: return @@ -788,12 +787,10 @@ class _BaseNetwork(_IPAddressBase): s1, s2 = self.subnets() while s1 != other and s2 != other: - if (other.network_address >= s1.network_address and - other.broadcast_address <= s1.broadcast_address): + if other.subnet_of(s1): yield s2 s1, s2 = s1.subnets() - elif (other.network_address >= s2.network_address and - other.broadcast_address <= s2.broadcast_address): + elif other.subnet_of(s2): yield s1 s1, s2 = s2.subnets() else: @@ -975,6 +972,26 @@ class _BaseNetwork(_IPAddressBase): return (self.network_address.is_multicast and self.broadcast_address.is_multicast) + @staticmethod + def _is_subnet_of(a, b): + try: + # Always false if one is v4 and the other is v6. + if a._version != b._version: + raise TypeError(f"{a} and {b} are not of the same version") + return (b.network_address <= a.network_address and + b.broadcast_address >= a.broadcast_address) + except AttributeError: + raise TypeError(f"Unable to test subnet containment " + f"between {a} and {b}") + + def subnet_of(self, other): + """Return True if this network is a subnet of other.""" + return self._is_subnet_of(self, other) + + def supernet_of(self, other): + """Return True if this network is a supernet of other.""" + return self._is_subnet_of(other, self) + @property def is_reserved(self): """Test if the address is otherwise IETF reserved. diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index 5d9633024f7..dbf68b3f8f1 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -92,7 +92,6 @@ class CommonTestMixin: y = pickle.loads(pickle.dumps(x, proto)) self.assertEqual(y, x) - class CommonTestMixin_v4(CommonTestMixin): def test_leading_zeros(self): @@ -477,6 +476,56 @@ class InterfaceTestCase_v4(BaseTestCase, NetmaskTestMixin_v4): class NetworkTestCase_v4(BaseTestCase, NetmaskTestMixin_v4): factory = ipaddress.IPv4Network + def test_subnet_of(self): + # containee left of container + self.assertFalse( + self.factory('10.0.0.0/30').subnet_of( + self.factory('10.0.1.0/24'))) + # containee inside container + self.assertTrue( + self.factory('10.0.0.0/30').subnet_of( + self.factory('10.0.0.0/24'))) + # containee right of container + self.assertFalse( + self.factory('10.0.0.0/30').subnet_of( + self.factory('10.0.1.0/24'))) + # containee larger than container + self.assertFalse( + self.factory('10.0.1.0/24').subnet_of( + self.factory('10.0.0.0/30'))) + + def test_supernet_of(self): + # containee left of container + self.assertFalse( + self.factory('10.0.0.0/30').supernet_of( + self.factory('10.0.1.0/24'))) + # containee inside container + self.assertFalse( + self.factory('10.0.0.0/30').supernet_of( + self.factory('10.0.0.0/24'))) + # containee right of container + self.assertFalse( + self.factory('10.0.0.0/30').supernet_of( + self.factory('10.0.1.0/24'))) + # containee larger than container + self.assertTrue( + self.factory('10.0.0.0/24').supernet_of( + self.factory('10.0.0.0/30'))) + + def test_subnet_of_mixed_types(self): + with self.assertRaises(TypeError): + ipaddress.IPv4Network('10.0.0.0/30').supernet_of( + ipaddress.IPv6Network('::1/128')) + with self.assertRaises(TypeError): + ipaddress.IPv6Network('::1/128').supernet_of( + ipaddress.IPv4Network('10.0.0.0/30')) + with self.assertRaises(TypeError): + ipaddress.IPv4Network('10.0.0.0/30').subnet_of( + ipaddress.IPv6Network('::1/128')) + with self.assertRaises(TypeError): + ipaddress.IPv6Network('::1/128').subnet_of( + ipaddress.IPv4Network('10.0.0.0/30')) + class NetmaskTestMixin_v6(CommonTestMixin_v6): """Input validation on interfaces and networks is very similar""" @@ -540,6 +589,42 @@ class InterfaceTestCase_v6(BaseTestCase, NetmaskTestMixin_v6): class NetworkTestCase_v6(BaseTestCase, NetmaskTestMixin_v6): factory = ipaddress.IPv6Network + def test_subnet_of(self): + # containee left of container + self.assertFalse( + self.factory('2000:999::/56').subnet_of( + self.factory('2000:aaa::/48'))) + # containee inside container + self.assertTrue( + self.factory('2000:aaa::/56').subnet_of( + self.factory('2000:aaa::/48'))) + # containee right of container + self.assertFalse( + self.factory('2000:bbb::/56').subnet_of( + self.factory('2000:aaa::/48'))) + # containee larger than container + self.assertFalse( + self.factory('2000:aaa::/48').subnet_of( + self.factory('2000:aaa::/56'))) + + def test_supernet_of(self): + # containee left of container + self.assertFalse( + self.factory('2000:999::/56').supernet_of( + self.factory('2000:aaa::/48'))) + # containee inside container + self.assertFalse( + self.factory('2000:aaa::/56').supernet_of( + self.factory('2000:aaa::/48'))) + # containee right of container + self.assertFalse( + self.factory('2000:bbb::/56').supernet_of( + self.factory('2000:aaa::/48'))) + # containee larger than container + self.assertTrue( + self.factory('2000:aaa::/48').supernet_of( + self.factory('2000:aaa::/56'))) + class FactoryFunctionErrors(BaseTestCase): diff --git a/Misc/NEWS.d/next/Library/2017-10-21-09-13-16.bpo-20825.-1MBEy.rst b/Misc/NEWS.d/next/Library/2017-10-21-09-13-16.bpo-20825.-1MBEy.rst new file mode 100644 index 00000000000..c1d23ec664b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-10-21-09-13-16.bpo-20825.-1MBEy.rst @@ -0,0 +1,3 @@ +Add `subnet_of` and `superset_of` containment tests to +:class:`ipaddress.IPv6Network` and :class:`ipaddress.IPv4Network`. +Patch by Michel Albert and Cheryl Sabella.