]> granicus.if.org Git - python/commitdiff
bpo-20825: Containment test for ip_network in ip_network.
authorCheryl Sabella <cheryl.sabella@gmail.com>
Sun, 22 Oct 2017 21:39:49 +0000 (17:39 -0400)
committerAntoine Pitrou <pitrou@free.fr>
Sun, 22 Oct 2017 21:39:49 +0000 (23:39 +0200)
Doc/library/ipaddress.rst
Lib/ipaddress.py
Lib/test/test_ipaddress.py
Misc/NEWS.d/next/Library/2017-10-21-09-13-16.bpo-20825.-1MBEy.rst [new file with mode: 0644]

index 36c6859d7016ccc7850b5276eac025d4b878e4e9..75c9107bd5eb311bf1405a41345b1e5632ee61c5 100644 (file)
@@ -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
index 70746f8de85c092368f163caf97071439fc2465f..e8ce4cef2ded4b18fb6f4b9d7cf67b7f1762c969 100644 (file)
@@ -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.
index 5d9633024f7064e67579107fba716b3278c373bd..dbf68b3f8f1a06f3e3d2850e2c4307f4b58a266f 100644 (file)
@@ -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 (file)
index 0000000..c1d23ec
--- /dev/null
@@ -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.