Skip to content

Commit c5999c6

Browse files
ENH: Added netmask and hostmask (#30)
1 parent 2bfb706 commit c5999c6

File tree

3 files changed

+140
-1
lines changed

3 files changed

+140
-1
lines changed

cyberpandas/ip_array.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,78 @@ def packed(self):
535535
# TODO: I wonder if that should be post-fixed by 0s.
536536
return self.data.tobytes()
537537

538+
def _apply_mask(self, op, v4_prefixlen, v6_prefixlen):
539+
"""Apply a netmask or hostmask"""
540+
self = self.copy()
541+
is_v4 = self.is_ipv4
542+
v4_net = getattr(
543+
ipaddress.ip_network(u'0.0.0.0/{}'.format(v4_prefixlen)),
544+
op)
545+
v4_mask = IPArray([v4_net])
546+
self.data[is_v4] = v4_mask.data
547+
548+
v6_net = getattr(
549+
ipaddress.ip_network(u'0::0/{}'.format(v6_prefixlen)),
550+
op)
551+
v6_mask = IPArray([v6_net])
552+
self.data[~is_v4] = v6_mask.data
553+
return self
554+
555+
def netmask(self, v4_prefixlen=32, v6_prefixlen=128):
556+
"""Compute an array of netmasks for an array of IP addresses.
557+
558+
Note that this is a method, rather than a property, to support
559+
taking `v4_prefixlen` and `v6_prefixlen` as arguments.
560+
561+
Parameters
562+
----------
563+
v4_prefixlen : int, default 32
564+
Length of the network prefix, in bits, for IPv4 addresses
565+
v6_prefixlen : int, default 128
566+
Lnegth of the network prefix, in bits, for IPv6 addresses
567+
568+
Returns
569+
-------
570+
IPArray
571+
572+
See Also
573+
--------
574+
IPArray.hostmask
575+
576+
Examples
577+
--------
578+
>>> arr = ip.IPArray(['192.0.0.0', '1:1::'])
579+
>>> arr.netmask(v4_prefixlen=16, v6_prefixlen=32)
580+
IPArray(['255.255.0.0', 'ffff:ffff::'])
581+
"""
582+
return self._apply_mask('netmask', v4_prefixlen, v6_prefixlen)
583+
584+
def hostmask(self, v4_prefixlen=32, v6_prefixlen=128):
585+
"""Compute an array of hostmasks for an array of IP addresses.
586+
587+
Parameters
588+
----------
589+
v4_prefixlen : int, default 32
590+
Length of the network prefix, in bits, for IPv4 addresses
591+
v6_prefixlen : int, default 128
592+
Lnegth of the network prefix, in bits, for IPv6 addresses
593+
594+
Returns
595+
-------
596+
IPArray
597+
598+
See Also
599+
--------
600+
IPArray.netmask
601+
602+
Examples
603+
--------
604+
>>> arr = ip.IPArray(['192.0.0.0', '1:1::'])
605+
>>> arr.netmask(v4_prefixlen=16, v6_prefixlen=32)
606+
IPArray(['0.0.255.255', '::ffff:ffff:ffff:ffff:ffff:ffff'])
607+
"""
608+
return self._apply_mask('hostmask', v4_prefixlen, v6_prefixlen)
609+
538610

539611
# -----------------------------------------------------------------------------
540612
# Accessor
@@ -573,6 +645,14 @@ def isin(self, other):
573645
return delegated_method(self._data.isin, self._index,
574646
self._name, other)
575647

648+
def netmask(self, v4_prefixlen=32, v6_prefixlen=128):
649+
return delegated_method(self._data.netmask, self._index,
650+
self._name, v4_prefixlen, v6_prefixlen)
651+
652+
def hostmask(self, v4_prefixlen=32, v6_prefixlen=128):
653+
return delegated_method(self._data.hostmask, self._index,
654+
self._name, v4_prefixlen, v6_prefixlen)
655+
576656

577657
def is_ipaddress_type(obj):
578658
t = getattr(obj, 'dtype', obj)

docs/source/api.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@ an IPArray, calling the Series method will dispatch to these methods.
3939
.. automethod:: IPArray.unique
4040
.. automethod:: IPArray.isin
4141
.. automethod:: IPArray.isna
42+
.. autoattribute:: IPArray.netmask
4243

4344
IP Address Attributes
4445
"""""""""""""""""""""
4546

46-
IP-addres specific attributes.
47+
IP addresss-specific attributes.
4748

4849
.. autoattribute:: IPArray.is_ipv4
4950
.. autoattribute:: IPArray.is_ipv6
@@ -55,6 +56,9 @@ IP-addres specific attributes.
5556
.. autoattribute:: IPArray.is_reserved
5657
.. autoattribute:: IPArray.is_loopback
5758
.. autoattribute:: IPArray.is_link_local
59+
.. automethod:: IPArray.netmask
60+
.. automethod:: IPArray.hostmask
61+
5862

5963

6064
:class:`MACArray`

tests/ip/test_ip.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,3 +327,58 @@ def test_ip_range(start, stop, step, expected):
327327
result = ip.ip_range(start, stop, step)
328328
expected = ip.IPArray(expected)
329329
assert result.equals(expected)
330+
331+
332+
@pytest.mark.parametrize('addresses', [
333+
[u'0.0.0.0', u'192.168.1.1', u'::1:1:0:0:0:1']
334+
])
335+
@pytest.mark.parametrize('v4_prefixlen, v6_prefixlen', [
336+
(32, 128),
337+
(24, 96),
338+
(16, 64),
339+
(8, 8),
340+
])
341+
@pytest.mark.parametrize('op', ['netmask', 'hostmask'])
342+
def test_mask(op, v4_prefixlen, v6_prefixlen, addresses):
343+
is_v6 = [':' in x for x in addresses]
344+
prefixes = [v6_prefixlen if v6 else v4_prefixlen for v6 in is_v6]
345+
networks = [
346+
ipaddress.ip_network(u"{}/{}".format(addr, prefix), strict=False)
347+
for addr, prefix in zip(addresses, prefixes)
348+
]
349+
expected = [getattr(net, op) for net in networks]
350+
call = operator.methodcaller(op, v4_prefixlen=v4_prefixlen,
351+
v6_prefixlen=v6_prefixlen)
352+
result = list(call(ip.IPArray(addresses)))
353+
if op == 'hostmask':
354+
# ipaddress will return an IPv6(0), which doesn't compare equal
355+
# to an IPv4(0), our result.
356+
expected = [int(x) for x in expected]
357+
result = [int(x) for x in result]
358+
359+
assert result == expected
360+
361+
362+
def test_netmask_basic():
363+
arr = ip.IPArray([u'192.0.0.0', u'1:1::'])
364+
result = arr.netmask(v4_prefixlen=16, v6_prefixlen=32)
365+
expected = ip.IPArray([u'255.255.0.0', u'ffff:ffff::'])
366+
assert result.equals(expected)
367+
368+
result = pd.Series(arr, name='foo').ip.netmask(v4_prefixlen=16,
369+
v6_prefixlen=32)
370+
assert result.name == 'foo'
371+
assert result.values.equals(expected)
372+
373+
374+
def test_hostmask_basic():
375+
arr = ip.IPArray([u'192.0.0.0', u'1:1::'])
376+
result = arr.hostmask(v4_prefixlen=16, v6_prefixlen=32)
377+
expected = ip.IPArray([u'0.0.255.255',
378+
u'::ffff:ffff:ffff:ffff:ffff:ffff'])
379+
assert result.equals(expected)
380+
381+
result = pd.Series(arr, name='foo').ip.hostmask(v4_prefixlen=16,
382+
v6_prefixlen=32)
383+
assert result.name == 'foo'
384+
assert result.values.equals(expected)

0 commit comments

Comments
 (0)