diff --git a/Doc/library/ipaddress.rst b/Doc/library/ipaddress.rst index 387d8034bb8c84a..53e8e7c945dd7b7 100644 --- a/Doc/library/ipaddress.rst +++ b/Doc/library/ipaddress.rst @@ -739,7 +739,7 @@ dictionaries. Traceback (most recent call last): File "", line 1, in raise ValueError( - ValueError: next prefix must be between 1 and 32 + ValueError: prefix must be between 1 and 32 >>> IPv4Network('255.255.255.0/24').next_network() Traceback (most recent call last): File "", line 1, in @@ -748,6 +748,36 @@ dictionaries. .. versionadded:: 3.16 + .. method:: prev_network(prev_prefix=None) + + Finds the previous closest network of prefix size *prev_prefix*. If + *prev_prefix=None*, then the current network prefix will be used. + Returns a single network object. + + Raises :exc:`ValueError` if *prev_prefix* is out of range or no further + network of the requested size exists. + + >>> IPv4Network('192.0.2.0/24').prev_network() + IPv4Network('192.0.1.0/24') + >>> IPv4Network('192.0.2.0/24').prev_network(prev_prefix=25) + IPv4Network('192.0.1.128/25') + >>> IPv4Network('192.0.2.0/24').prev_network(prev_prefix=23) + IPv4Network('192.0.0.0/23') + >>> IPv4Network('192.0.80.0/22').prev_network(prev_prefix=18) + IPv4Network('192.0.0.0/18') + >>> IPv4Network('192.0.80.0/22').prev_network(prev_prefix=50) + Traceback (most recent call last): + File "", line 1, in + raise ValueError( + ValueError: prefix must be between 1 and 32 + >>> IPv4Network('0.0.0.0/24').prev_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) Construct an IPv6 network definition. *address* can be one of the following: @@ -820,6 +850,7 @@ dictionaries. .. method:: subnet_of(other) .. method:: supernet_of(other) .. method:: next_network(next_prefix=None) + .. method:: prev_network(prev_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 e6565b186a598c5..5affaf1207380f7 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -321,6 +321,9 @@ 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. +Add :meth:`~ipaddress.IPv4Network.prev_network` and +:meth:`~ipaddress.IPv6Network.prev_network` methods to find the previous nearest +network with a specific prefix size. logging diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index 9c7a0a46f4f0e1f..dfdcda9306851e9 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1119,6 +1119,50 @@ def is_loopback(self): return (self.network_address.is_loopback and self.broadcast_address.is_loopback) + def _network_by_offset(self, prefix, offset): + """Get the network offset from this network. + + Args: + prefix: Target prefix length. + offset: Number of networks to move (+1 or -1). + + Returns: + An IPv(4|6) Network object. + + """ + if prefix < 1 or prefix > self.max_prefixlen: + raise ValueError( + f"prefix must be between 1 and {self.max_prefixlen}" + ) + new_netmask, _ = self._make_netmask(prefix) + error = f"out of address space, cannot make another /{prefix} network" + + if prefix > self.prefixlen: + if offset < 0: + new_ip = self.network_address._ip - 1 + else: + new_ip = self.broadcast_address._ip + 1 + if new_ip < 0 or new_ip > self._ALL_ONES: + raise ValueError(error) + + new_ip &= new_netmask._ip + else: + bit_shift = self.max_prefixlen - prefix + new_ip = ( + ((new_netmask._ip & self.network_address._ip) >> bit_shift) + + offset + ) << bit_shift + + if new_ip < 0: + raise ValueError(error) + + try: + return self.__class__( + f"{self._string_from_ip_int(new_ip)}/{prefix}" + ) + except OverflowError: + raise ValueError(error) from None + def next_network(self, next_prefix=None): """Get the next closest network with a specific prefix. @@ -1132,33 +1176,25 @@ def next_network(self, next_prefix=None): """ 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 - ) + return self._network_by_offset(next_prefix, 1) - 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 + def prev_network(self, prev_prefix=None): + """Get the previous closest network with a specific prefix. + + Args: + prev_prefix: The desired previous prefix length, if not specified the + same self.prefixlen will be used + + Returns: + An IPv(4|6) Network object of the previous closest network. + + """ + if prev_prefix is None: + prev_prefix = self.prefixlen + + return self._network_by_offset(prev_prefix, -1) class _BaseConstants: diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index a74b692784eb594..ad6dfa225f7d1ec 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -1597,8 +1597,57 @@ def testNextNetworkWithBadPrefix(self): def testNextNetworkOutOfAddressSpace(self): ipv4 = ipaddress.IPv4Network('255.255.255.0/24') self.assertRaises(ValueError, ipv4.next_network) + self.assertRaises(ValueError, ipv4.next_network, 25) ipv6 = ipaddress.IPv6Network('ffff:ffff:ffff:ffff:ffff:ffff:ffff:0/112') self.assertRaises(ValueError, ipv6.next_network) + self.assertRaises(ValueError, ipv4.next_network, 112) + + def testPrevNetwork(self): + ipv4 = ipaddress.IPv4Network('1.2.3.0/24') + self.assertEqual( + ipv4.prev_network(), + ipaddress.IPv4Network('1.2.2.0/24'), + ) + self.assertEqual( + ipv4.prev_network(prev_prefix=16), + ipaddress.IPv4Network('1.1.0.0/16'), + ) + self.assertEqual( + ipv4.prev_network(prev_prefix=25), + ipaddress.IPv4Network('1.2.2.128/25'), + ) + self.assertEqual( + ipv4.prev_network(prev_prefix=23), + ipaddress.IPv4Network('1.2.0.0/23'), + ) + + ipv6 = ipaddress.IPv6Network('2001:dbb8:aaaa:aaaa::/64') + self.assertEqual( + ipv6.prev_network(), + ipaddress.IPv6Network('2001:dbb8:aaaa:aaa9::/64'), + ) + self.assertEqual( + ipv6.prev_network(prev_prefix=48), + ipaddress.IPv6Network('2001:dbb8:aaa9::/48'), + ) + self.assertEqual( + ipv6.prev_network(prev_prefix=88), + ipaddress.IPv6Network('2001:dbb8:aaaa:aaa9:ffff:ff00::/88'), + ) + + def testPrevNetworkWithBadPrefix(self): + self.assertRaises(ValueError, self.ipv4_network.prev_network, 0) + self.assertRaises(ValueError, self.ipv4_network.prev_network, 35) + self.assertRaises(ValueError, self.ipv6_network.prev_network, 0) + self.assertRaises(ValueError, self.ipv6_network.prev_network, 150) + + def testPrevNetworkOutOfAddressSpace(self): + ipv4 = ipaddress.IPv4Network('0.0.0.0/24') + self.assertRaises(ValueError, ipv4.prev_network) + self.assertRaises(ValueError, ipv4.prev_network, 25) + ipv6 = ipaddress.IPv6Network('::/112') + self.assertRaises(ValueError, ipv6.prev_network) + self.assertRaises(ValueError, ipv6.prev_network, 112) def testFancySubnetting(self): self.assertEqual(sorted(self.ipv4_network.subnets(prefixlen_diff=3)), 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 index 46ed99d1e1b4992..6c025a27de4efa1 100644 --- 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 @@ -1,2 +1,4 @@ -Add :meth:`~ipaddress.IPv4Network.next_network` and -:meth:`~ipaddress.IPv6Network.next_network`. Patch by Faisal Mahmood. +Add :meth:`~ipaddress.IPv4Network.next_network`, +:meth:`~ipaddress.IPv6Network.next_network`, +:meth:`~ipaddress.IPv4Network.prev_network` and +:meth:`~ipaddress.IPv6Network.prev_network`. Patch by Faisal Mahmood.