Skip to content

Commit 91dc64b

Browse files
csabellapitrou
authored andcommitted
bpo-20825: Containment test for ip_network in ip_network.
1 parent 04e36af commit 91dc64b

File tree

4 files changed

+136
-7
lines changed

4 files changed

+136
-7
lines changed

Doc/library/ipaddress.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,28 @@ so to avoid duplication they are only documented for :class:`IPv4Network`.
543543
>>> ip_network('192.0.2.0/24').supernet(new_prefix=20)
544544
IPv4Network('192.0.0.0/20')
545545

546+
.. method:: subnet_of(other)
547+
548+
Returns *True* if this network is a subnet of *other*.
549+
550+
>>> a = ip_network('192.168.1.0/24')
551+
>>> b = ip_network('192.168.1.128/30')
552+
>>> b.subnet_of(a)
553+
True
554+
555+
.. versionadded:: 3.7
556+
557+
.. method:: supernet_of(other)
558+
559+
Returns *True* if this network is a supernet of *other*.
560+
561+
>>> a = ip_network('192.168.1.0/24')
562+
>>> b = ip_network('192.168.1.128/30')
563+
>>> a.supernet_of(b)
564+
True
565+
566+
.. versionadded:: 3.7
567+
546568
.. method:: compare_networks(other)
547569

548570
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`.
621643
.. method:: address_exclude(network)
622644
.. method:: subnets(prefixlen_diff=1, new_prefix=None)
623645
.. method:: supernet(prefixlen_diff=1, new_prefix=None)
646+
.. method:: subnet_of(other)
647+
.. method:: supernet_of(other)
624648
.. method:: compare_networks(other)
625649

626650
Refer to the corresponding attribute documentation in

Lib/ipaddress.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -776,8 +776,7 @@ def address_exclude(self, other):
776776
if not isinstance(other, _BaseNetwork):
777777
raise TypeError("%s is not a network object" % other)
778778

779-
if not (other.network_address >= self.network_address and
780-
other.broadcast_address <= self.broadcast_address):
779+
if not other.subnet_of(self):
781780
raise ValueError('%s not contained in %s' % (other, self))
782781
if other == self:
783782
return
@@ -788,12 +787,10 @@ def address_exclude(self, other):
788787

789788
s1, s2 = self.subnets()
790789
while s1 != other and s2 != other:
791-
if (other.network_address >= s1.network_address and
792-
other.broadcast_address <= s1.broadcast_address):
790+
if other.subnet_of(s1):
793791
yield s2
794792
s1, s2 = s1.subnets()
795-
elif (other.network_address >= s2.network_address and
796-
other.broadcast_address <= s2.broadcast_address):
793+
elif other.subnet_of(s2):
797794
yield s1
798795
s1, s2 = s2.subnets()
799796
else:
@@ -975,6 +972,26 @@ def is_multicast(self):
975972
return (self.network_address.is_multicast and
976973
self.broadcast_address.is_multicast)
977974

975+
@staticmethod
976+
def _is_subnet_of(a, b):
977+
try:
978+
# Always false if one is v4 and the other is v6.
979+
if a._version != b._version:
980+
raise TypeError(f"{a} and {b} are not of the same version")
981+
return (b.network_address <= a.network_address and
982+
b.broadcast_address >= a.broadcast_address)
983+
except AttributeError:
984+
raise TypeError(f"Unable to test subnet containment "
985+
f"between {a} and {b}")
986+
987+
def subnet_of(self, other):
988+
"""Return True if this network is a subnet of other."""
989+
return self._is_subnet_of(self, other)
990+
991+
def supernet_of(self, other):
992+
"""Return True if this network is a supernet of other."""
993+
return self._is_subnet_of(other, self)
994+
978995
@property
979996
def is_reserved(self):
980997
"""Test if the address is otherwise IETF reserved.

Lib/test/test_ipaddress.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ def pickle_test(self, addr):
9292
y = pickle.loads(pickle.dumps(x, proto))
9393
self.assertEqual(y, x)
9494

95-
9695
class CommonTestMixin_v4(CommonTestMixin):
9796

9897
def test_leading_zeros(self):
@@ -477,6 +476,56 @@ class InterfaceTestCase_v4(BaseTestCase, NetmaskTestMixin_v4):
477476
class NetworkTestCase_v4(BaseTestCase, NetmaskTestMixin_v4):
478477
factory = ipaddress.IPv4Network
479478

479+
def test_subnet_of(self):
480+
# containee left of container
481+
self.assertFalse(
482+
self.factory('10.0.0.0/30').subnet_of(
483+
self.factory('10.0.1.0/24')))
484+
# containee inside container
485+
self.assertTrue(
486+
self.factory('10.0.0.0/30').subnet_of(
487+
self.factory('10.0.0.0/24')))
488+
# containee right of container
489+
self.assertFalse(
490+
self.factory('10.0.0.0/30').subnet_of(
491+
self.factory('10.0.1.0/24')))
492+
# containee larger than container
493+
self.assertFalse(
494+
self.factory('10.0.1.0/24').subnet_of(
495+
self.factory('10.0.0.0/30')))
496+
497+
def test_supernet_of(self):
498+
# containee left of container
499+
self.assertFalse(
500+
self.factory('10.0.0.0/30').supernet_of(
501+
self.factory('10.0.1.0/24')))
502+
# containee inside container
503+
self.assertFalse(
504+
self.factory('10.0.0.0/30').supernet_of(
505+
self.factory('10.0.0.0/24')))
506+
# containee right of container
507+
self.assertFalse(
508+
self.factory('10.0.0.0/30').supernet_of(
509+
self.factory('10.0.1.0/24')))
510+
# containee larger than container
511+
self.assertTrue(
512+
self.factory('10.0.0.0/24').supernet_of(
513+
self.factory('10.0.0.0/30')))
514+
515+
def test_subnet_of_mixed_types(self):
516+
with self.assertRaises(TypeError):
517+
ipaddress.IPv4Network('10.0.0.0/30').supernet_of(
518+
ipaddress.IPv6Network('::1/128'))
519+
with self.assertRaises(TypeError):
520+
ipaddress.IPv6Network('::1/128').supernet_of(
521+
ipaddress.IPv4Network('10.0.0.0/30'))
522+
with self.assertRaises(TypeError):
523+
ipaddress.IPv4Network('10.0.0.0/30').subnet_of(
524+
ipaddress.IPv6Network('::1/128'))
525+
with self.assertRaises(TypeError):
526+
ipaddress.IPv6Network('::1/128').subnet_of(
527+
ipaddress.IPv4Network('10.0.0.0/30'))
528+
480529

481530
class NetmaskTestMixin_v6(CommonTestMixin_v6):
482531
"""Input validation on interfaces and networks is very similar"""
@@ -540,6 +589,42 @@ class InterfaceTestCase_v6(BaseTestCase, NetmaskTestMixin_v6):
540589
class NetworkTestCase_v6(BaseTestCase, NetmaskTestMixin_v6):
541590
factory = ipaddress.IPv6Network
542591

592+
def test_subnet_of(self):
593+
# containee left of container
594+
self.assertFalse(
595+
self.factory('2000:999::/56').subnet_of(
596+
self.factory('2000:aaa::/48')))
597+
# containee inside container
598+
self.assertTrue(
599+
self.factory('2000:aaa::/56').subnet_of(
600+
self.factory('2000:aaa::/48')))
601+
# containee right of container
602+
self.assertFalse(
603+
self.factory('2000:bbb::/56').subnet_of(
604+
self.factory('2000:aaa::/48')))
605+
# containee larger than container
606+
self.assertFalse(
607+
self.factory('2000:aaa::/48').subnet_of(
608+
self.factory('2000:aaa::/56')))
609+
610+
def test_supernet_of(self):
611+
# containee left of container
612+
self.assertFalse(
613+
self.factory('2000:999::/56').supernet_of(
614+
self.factory('2000:aaa::/48')))
615+
# containee inside container
616+
self.assertFalse(
617+
self.factory('2000:aaa::/56').supernet_of(
618+
self.factory('2000:aaa::/48')))
619+
# containee right of container
620+
self.assertFalse(
621+
self.factory('2000:bbb::/56').supernet_of(
622+
self.factory('2000:aaa::/48')))
623+
# containee larger than container
624+
self.assertTrue(
625+
self.factory('2000:aaa::/48').supernet_of(
626+
self.factory('2000:aaa::/56')))
627+
543628

544629
class FactoryFunctionErrors(BaseTestCase):
545630

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add `subnet_of` and `superset_of` containment tests to
2+
:class:`ipaddress.IPv6Network` and :class:`ipaddress.IPv4Network`.
3+
Patch by Michel Albert and Cheryl Sabella.

0 commit comments

Comments
 (0)