Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Doc/library/ipaddress.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
29 changes: 23 additions & 6 deletions Lib/ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,8 +776,7 @@ def address_exclude(self, other):
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
Expand All @@ -788,12 +787,10 @@ def address_exclude(self, other):

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:
Expand Down Expand Up @@ -975,6 +972,26 @@ def is_multicast(self):
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.
Expand Down
87 changes: 86 additions & 1 deletion Lib/test/test_ipaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ def pickle_test(self, addr):
y = pickle.loads(pickle.dumps(x, proto))
self.assertEqual(y, x)


class CommonTestMixin_v4(CommonTestMixin):

def test_leading_zeros(self):
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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):

Expand Down
Original file line number Diff line number Diff line change
@@ -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.