Skip to content

Commit 5fb195f

Browse files
committed
Issue #16531: ipaddress.IPv4Network and ipaddress.IPv6Network now accept an (address, netmask) tuple argument, so as to easily construct network objects from existing addresses.
1 parent 3b5162d commit 5fb195f

File tree

4 files changed

+210
-21
lines changed

4 files changed

+210
-21
lines changed

Doc/library/ipaddress.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,12 @@ so to avoid duplication they are only documented for :class:`IPv4Network`.
392392
3. An integer packed into a :class:`bytes` object of length 4, big-endian.
393393
The interpretation is similar to an integer *address*.
394394

395+
4. A two-tuple of an address description and a netmask, where the address
396+
description is either a string, a 32-bits integer, a 4-bytes packed
397+
integer, or an existing IPv4Address object; and the netmask is either
398+
an integer representing the prefix length (e.g. ``24``) or a string
399+
representing the prefix mask (e.g. ``255.255.255.0``).
400+
395401
An :exc:`AddressValueError` is raised if *address* is not a valid IPv4
396402
address. A :exc:`NetmaskValueError` is raised if the mask is not valid for
397403
an IPv4 address.
@@ -404,6 +410,10 @@ so to avoid duplication they are only documented for :class:`IPv4Network`.
404410
objects will raise :exc:`TypeError` if the argument's IP version is
405411
incompatible to ``self``
406412

413+
.. versionchanged:: 3.5
414+
415+
Added the two-tuple form for the *address* constructor parameter.
416+
407417
.. attribute:: version
408418
.. attribute:: max_prefixlen
409419

@@ -568,6 +578,11 @@ so to avoid duplication they are only documented for :class:`IPv4Network`.
568578
3. An integer packed into a :class:`bytes` object of length 16, bit-endian.
569579
The interpretation is similar to an integer *address*.
570580

581+
4. A two-tuple of an address description and a netmask, where the address
582+
description is either a string, a 128-bits integer, a 16-bytes packed
583+
integer, or an existing IPv4Address object; and the netmask is an
584+
integer representing the prefix length.
585+
571586
An :exc:`AddressValueError` is raised if *address* is not a valid IPv6
572587
address. A :exc:`NetmaskValueError` is raised if the mask is not valid for
573588
an IPv6 address.
@@ -576,6 +591,10 @@ so to avoid duplication they are only documented for :class:`IPv4Network`.
576591
then :exc:`ValueError` is raised. Otherwise, the host bits are masked out
577592
to determine the appropriate network address.
578593

594+
.. versionchanged:: 3.5
595+
596+
Added the two-tuple form for the *address* constructor parameter.
597+
579598
.. attribute:: version
580599
.. attribute:: max_prefixlen
581600
.. attribute:: is_multicast

Lib/ipaddress.py

Lines changed: 74 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -991,15 +991,15 @@ def supernet(self, prefixlen_diff=1, new_prefix=None):
991991
raise ValueError('cannot set prefixlen_diff and new_prefix')
992992
prefixlen_diff = self._prefixlen - new_prefix
993993

994-
if self.prefixlen - prefixlen_diff < 0:
994+
new_prefixlen = self.prefixlen - prefixlen_diff
995+
if new_prefixlen < 0:
995996
raise ValueError(
996997
'current prefixlen is %d, cannot have a prefixlen_diff of %d' %
997998
(self.prefixlen, prefixlen_diff))
998-
# TODO (pmoody): optimize this.
999-
t = self.__class__('%s/%d' % (self.network_address,
1000-
self.prefixlen - prefixlen_diff),
1001-
strict=False)
1002-
return t.__class__('%s/%d' % (t.network_address, t.prefixlen))
999+
return self.__class__((
1000+
int(self.network_address) & (int(self.netmask) << prefixlen_diff),
1001+
new_prefixlen
1002+
))
10031003

10041004
@property
10051005
def is_multicast(self):
@@ -1389,6 +1389,18 @@ def __init__(self, address):
13891389
self._prefixlen = self._max_prefixlen
13901390
return
13911391

1392+
if isinstance(address, tuple):
1393+
IPv4Address.__init__(self, address[0])
1394+
if len(address) > 1:
1395+
self._prefixlen = int(address[1])
1396+
else:
1397+
self._prefixlen = self._max_prefixlen
1398+
1399+
self.network = IPv4Network(address, strict=False)
1400+
self.netmask = self.network.netmask
1401+
self.hostmask = self.network.hostmask
1402+
return
1403+
13921404
addr = _split_optional_netmask(address)
13931405
IPv4Address.__init__(self, addr[0])
13941406

@@ -1504,22 +1516,42 @@ def __init__(self, address, strict=True):
15041516
_BaseV4.__init__(self, address)
15051517
_BaseNetwork.__init__(self, address)
15061518

1507-
# Constructing from a packed address
1508-
if isinstance(address, bytes):
1519+
# Constructing from a packed address or integer
1520+
if isinstance(address, (int, bytes)):
15091521
self.network_address = IPv4Address(address)
15101522
self._prefixlen = self._max_prefixlen
15111523
self.netmask = IPv4Address(self._ALL_ONES)
15121524
#fixme: address/network test here
15131525
return
15141526

1515-
# Efficient constructor from integer.
1516-
if isinstance(address, int):
1517-
self.network_address = IPv4Address(address)
1518-
self._prefixlen = self._max_prefixlen
1519-
self.netmask = IPv4Address(self._ALL_ONES)
1520-
#fixme: address/network test here.
1527+
if isinstance(address, tuple):
1528+
if len(address) > 1:
1529+
# If address[1] is a string, treat it like a netmask.
1530+
if isinstance(address[1], str):
1531+
self.netmask = IPv4Address(address[1])
1532+
self._prefixlen = self._prefix_from_ip_int(
1533+
int(self.netmask))
1534+
# address[1] should be an int.
1535+
else:
1536+
self._prefixlen = int(address[1])
1537+
self.netmask = IPv4Address(self._ip_int_from_prefix(
1538+
self._prefixlen))
1539+
# We weren't given an address[1].
1540+
else:
1541+
self._prefixlen = self._max_prefixlen
1542+
self.netmask = IPv4Address(self._ip_int_from_prefix(
1543+
self._prefixlen))
1544+
self.network_address = IPv4Address(address[0])
1545+
packed = int(self.network_address)
1546+
if packed & int(self.netmask) != packed:
1547+
if strict:
1548+
raise ValueError('%s has host bits set' % self)
1549+
else:
1550+
self.network_address = IPv4Address(packed &
1551+
int(self.netmask))
15211552
return
15221553

1554+
15231555
# Assume input argument to be string or any object representation
15241556
# which converts into a formatted IP prefix string.
15251557
addr = _split_optional_netmask(address)
@@ -2030,6 +2062,16 @@ def __init__(self, address):
20302062
self.network = IPv6Network(self._ip)
20312063
self._prefixlen = self._max_prefixlen
20322064
return
2065+
if isinstance(address, tuple):
2066+
IPv6Address.__init__(self, address[0])
2067+
if len(address) > 1:
2068+
self._prefixlen = int(address[1])
2069+
else:
2070+
self._prefixlen = self._max_prefixlen
2071+
self.network = IPv6Network(address, strict=False)
2072+
self.netmask = self.network.netmask
2073+
self.hostmask = self.network.hostmask
2074+
return
20332075

20342076
addr = _split_optional_netmask(address)
20352077
IPv6Address.__init__(self, addr[0])
@@ -2147,18 +2189,29 @@ def __init__(self, address, strict=True):
21472189
_BaseV6.__init__(self, address)
21482190
_BaseNetwork.__init__(self, address)
21492191

2150-
# Efficient constructor from integer.
2151-
if isinstance(address, int):
2192+
# Efficient constructor from integer or packed address
2193+
if isinstance(address, (bytes, int)):
21522194
self.network_address = IPv6Address(address)
21532195
self._prefixlen = self._max_prefixlen
21542196
self.netmask = IPv6Address(self._ALL_ONES)
21552197
return
21562198

2157-
# Constructing from a packed address
2158-
if isinstance(address, bytes):
2159-
self.network_address = IPv6Address(address)
2160-
self._prefixlen = self._max_prefixlen
2161-
self.netmask = IPv6Address(self._ALL_ONES)
2199+
if isinstance(address, tuple):
2200+
self.network_address = IPv6Address(address[0])
2201+
if len(address) > 1:
2202+
self._prefixlen = int(address[1])
2203+
else:
2204+
self._prefixlen = self._max_prefixlen
2205+
self.netmask = IPv6Address(self._ip_int_from_prefix(
2206+
self._prefixlen))
2207+
self.network_address = IPv6Address(address[0])
2208+
packed = int(self.network_address)
2209+
if packed & int(self.netmask) != packed:
2210+
if strict:
2211+
raise ValueError('%s has host bits set' % self)
2212+
else:
2213+
self.network_address = IPv6Address(packed &
2214+
int(self.netmask))
21622215
return
21632216

21642217
# Assume input argument to be string or any object representation

Lib/test/test_ipaddress.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,119 @@ def testRepr(self):
628628
self.assertEqual("IPv6Interface('::1/128')",
629629
repr(ipaddress.IPv6Interface('::1')))
630630

631+
# issue #16531: constructing IPv4Network from a (address, mask) tuple
632+
def testIPv4Tuple(self):
633+
# /32
634+
ip = ipaddress.IPv4Address('192.0.2.1')
635+
net = ipaddress.IPv4Network('192.0.2.1/32')
636+
self.assertEqual(ipaddress.IPv4Network(('192.0.2.1', 32)), net)
637+
self.assertEqual(ipaddress.IPv4Network((ip, 32)), net)
638+
self.assertEqual(ipaddress.IPv4Network((3221225985, 32)), net)
639+
self.assertEqual(ipaddress.IPv4Network(('192.0.2.1',
640+
'255.255.255.255')), net)
641+
self.assertEqual(ipaddress.IPv4Network((ip,
642+
'255.255.255.255')), net)
643+
self.assertEqual(ipaddress.IPv4Network((3221225985,
644+
'255.255.255.255')), net)
645+
# strict=True and host bits set
646+
with self.assertRaises(ValueError):
647+
ipaddress.IPv4Network(('192.0.2.1', 24))
648+
with self.assertRaises(ValueError):
649+
ipaddress.IPv4Network((ip, 24))
650+
with self.assertRaises(ValueError):
651+
ipaddress.IPv4Network((3221225985, 24))
652+
with self.assertRaises(ValueError):
653+
ipaddress.IPv4Network(('192.0.2.1', '255.255.255.0'))
654+
with self.assertRaises(ValueError):
655+
ipaddress.IPv4Network((ip, '255.255.255.0'))
656+
with self.assertRaises(ValueError):
657+
ipaddress.IPv4Network((3221225985, '255.255.255.0'))
658+
# strict=False and host bits set
659+
net = ipaddress.IPv4Network('192.0.2.0/24')
660+
self.assertEqual(ipaddress.IPv4Network(('192.0.2.1', 24),
661+
strict=False), net)
662+
self.assertEqual(ipaddress.IPv4Network((ip, 24),
663+
strict=False), net)
664+
self.assertEqual(ipaddress.IPv4Network((3221225985, 24),
665+
strict=False), net)
666+
self.assertEqual(ipaddress.IPv4Network(('192.0.2.1',
667+
'255.255.255.0'),
668+
strict=False), net)
669+
self.assertEqual(ipaddress.IPv4Network((ip,
670+
'255.255.255.0'),
671+
strict=False), net)
672+
self.assertEqual(ipaddress.IPv4Network((3221225985,
673+
'255.255.255.0'),
674+
strict=False), net)
675+
676+
# /24
677+
ip = ipaddress.IPv4Address('192.0.2.0')
678+
net = ipaddress.IPv4Network('192.0.2.0/24')
679+
self.assertEqual(ipaddress.IPv4Network(('192.0.2.0',
680+
'255.255.255.0')), net)
681+
self.assertEqual(ipaddress.IPv4Network((ip,
682+
'255.255.255.0')), net)
683+
self.assertEqual(ipaddress.IPv4Network((3221225984,
684+
'255.255.255.0')), net)
685+
self.assertEqual(ipaddress.IPv4Network(('192.0.2.0', 24)), net)
686+
self.assertEqual(ipaddress.IPv4Network((ip, 24)), net)
687+
self.assertEqual(ipaddress.IPv4Network((3221225984, 24)), net)
688+
689+
self.assertEqual(ipaddress.IPv4Interface(('192.0.2.1', 24)),
690+
ipaddress.IPv4Interface('192.0.2.1/24'))
691+
self.assertEqual(ipaddress.IPv4Interface((3221225985, 24)),
692+
ipaddress.IPv4Interface('192.0.2.1/24'))
693+
694+
# issue #16531: constructing IPv6Network from a (address, mask) tuple
695+
def testIPv6Tuple(self):
696+
# /128
697+
ip = ipaddress.IPv6Address('2001:db8::')
698+
net = ipaddress.IPv6Network('2001:db8::/128')
699+
self.assertEqual(ipaddress.IPv6Network(('2001:db8::', '128')),
700+
net)
701+
self.assertEqual(ipaddress.IPv6Network(
702+
(42540766411282592856903984951653826560, 128)),
703+
net)
704+
self.assertEqual(ipaddress.IPv6Network((ip, '128')),
705+
net)
706+
ip = ipaddress.IPv6Address('2001:db8::')
707+
net = ipaddress.IPv6Network('2001:db8::/96')
708+
self.assertEqual(ipaddress.IPv6Network(('2001:db8::', '96')),
709+
net)
710+
self.assertEqual(ipaddress.IPv6Network(
711+
(42540766411282592856903984951653826560, 96)),
712+
net)
713+
self.assertEqual(ipaddress.IPv6Network((ip, '96')),
714+
net)
715+
716+
# strict=True and host bits set
717+
ip = ipaddress.IPv6Address('2001:db8::1')
718+
with self.assertRaises(ValueError):
719+
ipaddress.IPv6Network(('2001:db8::1', 96))
720+
with self.assertRaises(ValueError):
721+
ipaddress.IPv6Network((
722+
42540766411282592856903984951653826561, 96))
723+
with self.assertRaises(ValueError):
724+
ipaddress.IPv6Network((ip, 96))
725+
# strict=False and host bits set
726+
net = ipaddress.IPv6Network('2001:db8::/96')
727+
self.assertEqual(ipaddress.IPv6Network(('2001:db8::1', 96),
728+
strict=False),
729+
net)
730+
self.assertEqual(ipaddress.IPv6Network(
731+
(42540766411282592856903984951653826561, 96),
732+
strict=False),
733+
net)
734+
self.assertEqual(ipaddress.IPv6Network((ip, 96), strict=False),
735+
net)
736+
737+
# /96
738+
self.assertEqual(ipaddress.IPv6Interface(('2001:db8::1', '96')),
739+
ipaddress.IPv6Interface('2001:db8::1/96'))
740+
self.assertEqual(ipaddress.IPv6Interface(
741+
(42540766411282592856903984951653826561, '96')),
742+
ipaddress.IPv6Interface('2001:db8::1/96'))
743+
631744
# issue57
632745
def testAddressIntMath(self):
633746
self.assertEqual(ipaddress.IPv4Address('1.1.1.1') + 255,

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ Core and Builtins
8181
Library
8282
-------
8383

84+
- Issue #16531: ipaddress.IPv4Network and ipaddress.IPv6Network now accept
85+
an (address, netmask) tuple argument, so as to easily construct network
86+
objects from existing addresses.
87+
8488
- Issue #21156: importlib.abc.InspectLoader.source_to_code() is now a
8589
staticmethod.
8690

0 commit comments

Comments
 (0)