diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst index 9ccd8602bcb2c3..387d8034bb8c84 100644 --- a/Doc/library/ipaddress.rst +++ b/Doc/library/ipaddress.rst @@ -718,6 +718,35 @@ dictionaries. .. deprecated:: 3.7 It uses the same ordering and comparison algorithm as "<", "==", and ">" + .. method:: next_network(next_prefix=None) + + Finds the next closest network of prefix size *next_prefix*. If + *next_prefix=None*, then the current network prefix will be used. + Returns a single network object. + + Raises :exc:`ValueError` if *next_prefix* is out of range or no further + network of the requested size exists. + + >>> IPv4Network('192.0.2.0/24').next_network() + IPv4Network('192.0.3.0/24') + >>> IPv4Network('192.0.2.0/24').next_network(next_prefix=25) + IPv4Network('192.0.3.0/25') + >>> IPv4Network('192.0.2.0/24').next_network(next_prefix=23) + IPv4Network('192.0.4.0/23') + >>> IPv4Network('192.0.80.0/22').next_network(next_prefix=18) + IPv4Network('192.0.128.0/18') + >>> IPv4Network('192.0.80.0/22').next_network(next_prefix=50) + Traceback (most recent call last): + File "", line 1, in + raise ValueError( + ValueError: next prefix must be between 1 and 32 + >>> IPv4Network('255.255.255.0/24').next_network() + Traceback (most recent call last): + File "", line 1, in + raise ValueError( + ValueError: out of address space, cannot make another /24 network + + .. versionadded:: 3.16 .. class:: IPv6Network(address, strict=True) @@ -790,6 +819,7 @@ dictionaries. .. method:: supernet(prefixlen_diff=1, new_prefix=None) .. method:: subnet_of(other) .. method:: supernet_of(other) + .. method:: next_network(next_prefix=None) .. method:: compare_networks(other) Refer to the corresponding attribute documentation in diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 43ecdb41afc3a9..2bb829f6580d8d 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -245,6 +245,14 @@ imaplib (Contributed by Przemysław Buczkowski and Serhiy Storchaka in :gh:`89869`.) +ipaddress +--------- + +Add :meth:`~ipaddress.IPv4Network.next_network` and +:meth:`~ipaddress.IPv6Network.next_network` methods to find the next nearest +network with a specific prefix size. + + logging ------- diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index f1062a8cd052a5..9c7a0a46f4f0e1 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1119,6 +1119,47 @@ def is_loopback(self): return (self.network_address.is_loopback and self.broadcast_address.is_loopback) + def next_network(self, next_prefix=None): + """Get the next closest network with a specific prefix. + + Args: + next_prefix: The desired next prefix length, if not specified the + same self.prefixlen will be used + + Returns: + An IPv(4|6) Network object of the next closest network. + + """ + if next_prefix is None: + next_prefix = self.prefixlen + new_netmask = self.netmask + else: + if next_prefix < 1 or next_prefix > self.max_prefixlen: + raise ValueError( + f"next prefix must be between 1 and {self.max_prefixlen}" + ) + new_netmask, _ = self._make_netmask(next_prefix) + + bit_shift = ( + self.max_prefixlen - next_prefix + if next_prefix <= self.prefixlen + else self.max_prefixlen - self.prefixlen + ) + + next_ip = ( + ((new_netmask._ip & self.network_address._ip) >> bit_shift) + 1 + ) << bit_shift + + try: + return self.__class__( + f"{self._string_from_ip_int(next_ip)}/{next_prefix}" + ) + except OverflowError: + raise ValueError( + f"out of address space, cannot make another /{next_prefix} " + "network" + ) from None + class _BaseConstants: diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index 3f017b97dc28a3..a74b692784eb59 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -1559,6 +1559,47 @@ def testHosts(self): self.assertEqual(list(ipaddress.ip_network(str_args).hosts()), list(ipaddress.ip_network(tpl_args).hosts())) + def testNextNetwork(self): + ipv4 = ipaddress.IPv4Network('1.2.3.0/24') + self.assertEqual( + ipv4.next_network(), + ipaddress.IPv4Network('1.2.4.0/24'), + ) + self.assertEqual( + ipv4.next_network(next_prefix=16), + ipaddress.IPv4Network('1.3.0.0/16'), + ) + self.assertEqual( + ipv4.next_network(next_prefix=25), + ipaddress.IPv4Network('1.2.4.0/25'), + ) + + ipv6 = ipaddress.IPv6Network('2001:dbb8:aaaa:aaaa::/64') + self.assertEqual( + ipv6.next_network(), + ipaddress.IPv6Network('2001:dbb8:aaaa:aaab::/64'), + ) + self.assertEqual( + ipv6.next_network(next_prefix=48), + ipaddress.IPv6Network('2001:dbb8:aaab::/48'), + ) + self.assertEqual( + ipv6.next_network(next_prefix=88), + ipaddress.IPv6Network('2001:dbb8:aaaa:aaab::/88'), + ) + + def testNextNetworkWithBadPrefix(self): + self.assertRaises(ValueError, self.ipv4_network.next_network, 0) + self.assertRaises(ValueError, self.ipv4_network.next_network, 35) + self.assertRaises(ValueError, self.ipv6_network.next_network, 0) + self.assertRaises(ValueError, self.ipv6_network.next_network, 150) + + def testNextNetworkOutOfAddressSpace(self): + ipv4 = ipaddress.IPv4Network('255.255.255.0/24') + self.assertRaises(ValueError, ipv4.next_network) + ipv6 = ipaddress.IPv6Network('ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/112') + self.assertRaises(ValueError, ipv6.next_network) + def testFancySubnetting(self): self.assertEqual(sorted(self.ipv4_network.subnets(prefixlen_diff=3)), sorted(self.ipv4_network.subnets(new_prefix=27))) diff --git a/Misc/NEWS.d/next/Library/2021-01-09-18-40-15.bpo-42861.T7Ge9O.rst b/Misc/NEWS.d/next/Library/2021-01-09-18-40-15.bpo-42861.T7Ge9O.rst new file mode 100644 index 00000000000000..46ed99d1e1b499 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-01-09-18-40-15.bpo-42861.T7Ge9O.rst @@ -0,0 +1,2 @@ +Add :meth:`~ipaddress.IPv4Network.next_network` and +:meth:`~ipaddress.IPv6Network.next_network`. Patch by Faisal Mahmood.