Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests/formatting #105

Merged
merged 26 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b98d78e
formatting test_address
VascoSch92 Sep 18, 2023
a785ad5
formatting test_subnet
VascoSch92 Sep 18, 2023
196c29f
formatting test_validators
VascoSch92 Sep 18, 2023
a18a680
test cases for test_validators
VascoSch92 Sep 18, 2023
bf791cb
test cases for test_subnet
VascoSch92 Sep 18, 2023
f591cb9
test cases for test_address
VascoSch92 Sep 18, 2023
d1b8bc3
Merge branch 'main' into pr/105
Diapolo10 Sep 18, 2023
bc6e74e
Merge branch 'main' into pr/105
Diapolo10 Sep 18, 2023
ce258d1
Merge branch 'tests/formatting' of https://github.com/VascoSch92/ipli…
Diapolo10 Sep 18, 2023
a0140cc
[PATCH] Linter fixes
Diapolo10 Sep 18, 2023
66b1457
Merge branch 'main' into tests/formatting
Diapolo10 Sep 18, 2023
5c65318
[PATCH] Simplified some code
Diapolo10 Sep 19, 2023
c047cf4
Merge branch 'pr/105' into nightly
Diapolo10 Sep 19, 2023
dcbf498
[PATCH] Switched to SubnetType over literals
Diapolo10 Sep 19, 2023
5453c64
[PATCH] Simplified address validator code
Diapolo10 Sep 19, 2023
d887bb8
Merge branch 'main' into tests/formatting
Diapolo10 Sep 19, 2023
02895f5
[PATCH] Updated coverage to 100%
Diapolo10 Sep 21, 2023
b9e5853
hid ip_addresses
VascoSch92 Sep 22, 2023
d9d1aa2
[PATCH] Made f-string use repr
Diapolo10 Sep 22, 2023
25793d7
[PATCH] De-duplicated IPv4 addresses in validators
Diapolo10 Sep 22, 2023
5f1b303
hid ip_addresses
VascoSch92 Sep 22, 2023
9fc5afb
add mask
VascoSch92 Sep 23, 2023
79eb3ac
add join
VascoSch92 Sep 23, 2023
352dbd3
add join
VascoSch92 Sep 23, 2023
01a6fab
add join
VascoSch92 Sep 23, 2023
ad4c306
Merge branch 'main' into tests/formatting
Diapolo10 Sep 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

# IPlib3 Changelog
# IPLib3 Changelog

All notable changes to this project will be documented in this file.

Expand Down
83 changes: 24 additions & 59 deletions iplib3/address.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from itertools import groupby
from typing import TYPE_CHECKING

from iplib3.constants.address import (
Expand All @@ -18,7 +19,6 @@
IPV6_MAX_SEGMENT_COUNT,
IPV6_MAX_SEGMENT_VALUE,
IPV6_MAX_VALUE,
IPV6_MIN_SEGMENT_COUNT,
IPV6_MIN_SEGMENT_VALUE,
IPV6_NUMBER_BIT_COUNT,
IPV6_SEGMENT_BIT_COUNT,
Expand Down Expand Up @@ -146,62 +146,28 @@ def _num_to_ipv6(num: int, shorten: bool = True, remove_zeroes: bool = False) ->
with optional zero removal and shortening.
"""

segment_min_length = (IPV6_SEGMENT_BIT_COUNT // IPV6_NUMBER_BIT_COUNT) * (not shorten)
zero_segment = f'{0:0{segment_min_length}}'

segments = []
for _ in range(IPV6_MAX_SEGMENT_COUNT):
num, segment = divmod(num, IPV6_MAX_SEGMENT_VALUE+1)
segments.append(hex(segment).split('x')[1].upper())
segments.append(f'{segment:0{segment_min_length}X}')

if remove_zeroes and '0' in segments:
if remove_zeroes and zero_segment in segments:

# Goes over the segments to find the
# longest strip with nothing but zeroes
# and replaces it with an empty string.
# The final str.join will turn to '::'.

longest = 0
longest_idx = 0
current = 0
current_idx = 0

for idx, seg in enumerate(segments):

if seg == '0':

if not current:
current_idx = idx
current += 1

else:
current = 0

if current > longest:
longest = current
longest_idx = current_idx

segments = (
(
segments[:longest_idx]
if IPV6_MIN_SEGMENT_COUNT < longest_idx < IPV6_MAX_SEGMENT_COUNT-1
else ['']
)
+ ['']
+ (
segments[longest_idx+longest:]
if IPV6_MIN_SEGMENT_COUNT < longest_idx+longest < IPV6_MAX_SEGMENT_COUNT
else ['']
)
longest_idx, length = max(
(idx, len(list(group)))
for idx, (item, group) in enumerate(groupby(segments))
if item == zero_segment
)

if not shorten:

# Fills up any segments to full length by
# adding missing zeroes to the front, if any.

segments = [
seg.zfill(IPV6_SEGMENT_BIT_COUNT // IPV6_NUMBER_BIT_COUNT)
if seg else ''
for seg in segments
]
segments[longest_idx:longest_idx+length] = ['', '']

return ':'.join(segments[::-1])

Expand All @@ -211,15 +177,15 @@ class IPAddress(PureAddress):

__slots__ = ('_ipv4', '_ipv6', '_submask')

def __new__(cls: type[IPAddress], address: int | (str | None) = None, port_num: int | None = None, **kwargs) -> IPAddress: # noqa: ANN003,PYI034
def __new__(cls: type[IPAddress], address: int | str | None = None, port_num: int | None = None, **kwargs) -> IPAddress: # noqa: ANN003,PYI034

if isinstance(address, str):
# Only IPv4-addresses have '.', ':' is used in both IPv4 and IPv6
cls = IPv4 if '.' in address else IPv6

self = object.__new__(cls)

self.__init__(address=address, port_num=port_num, **kwargs)
self.__init__(address=address, port_num=port_num, **kwargs) # type: ignore # noqa: PGH003 # mypy: ignore
return self

def __init__(self: IPAddress, address: int | None = IPV4_LOCALHOST, port_num: int | None = None) -> None:
Expand Down Expand Up @@ -254,13 +220,13 @@ def __str__(self: IPAddress) -> str:
def as_ipv4(self: IPAddress) -> IPv4:
"""Creates and returns an IPv4 version of the address, if possible"""

return IPv4(self.num_to_ipv4(), port_num=self.port)
return IPv4(self.num_to_ipv4(), port_num=self.port) # type: ignore # noqa: PGH003

@property
def as_ipv6(self: IPAddress) -> IPv6:
"""Creates and returns an IPv6-version of the address"""

return IPv6(self.num_to_ipv6(), port_num=self.port)
return IPv6(self.num_to_ipv6(), port_num=self.port) # type: ignore # noqa: PGH003


class IPv4(IPAddress):
Expand All @@ -270,17 +236,17 @@ class IPv4(IPAddress):

def __init__(self: IPv4, address: str | None = None, port_num: int | None = None) -> None:

if address is None:
address = self._num_to_ipv4(IPV4_LOCALHOST)
new_address = self._num_to_ipv4(IPV4_LOCALHOST) if address is None else address

_address, *_port = new_address.split(':')

_address, *_port = address.split(':')
if _port:
address = _address
new_address = _address

if port_num is None:
port_num = int(_port[0])

self._address = address
self._address = new_address
super().__init__(address=self._ipv4_to_num(), port_num=port_num)

def __str__(self: IPv4) -> str:
Expand Down Expand Up @@ -314,20 +280,19 @@ class IPv6(IPAddress):

def __init__(self: IPv6, address: str | None = None, port_num: int | None = None) -> None:

if address is None:
address = self._num_to_ipv6(IPV6_LOCALHOST)
new_address = self._num_to_ipv6(IPV6_LOCALHOST) if address is None else address

_address, *_port = address.split(']:')
_address, *_port = new_address.split(']:')

if _port:

# Removes the opening square bracket
address = _address[1:]
new_address = _address[1:]

if port_num is None:
port_num = int(_port[0])

self._address = address
self._address = new_address
super().__init__(address=self._ipv6_to_num(), port_num=port_num)

def __str__(self: IPv6) -> str:
Expand Down
17 changes: 17 additions & 0 deletions iplib3/constants/subnet.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""Subnet constants"""

from __future__ import annotations

from enum import Enum

from iplib3.constants.ipv4 import IPV4_MAX_SEGMENT_COUNT, IPV4_SEGMENT_BIT_COUNT
from iplib3.constants.ipv6 import IPV6_MAX_SEGMENT_COUNT, IPV6_SEGMENT_BIT_COUNT

Expand All @@ -9,3 +13,16 @@
IPV4_MAX_SUBNET_VALUE = IPV4_SEGMENT_BIT_COUNT * IPV4_MAX_SEGMENT_COUNT - 1 # == 31
IPV6_MIN_SUBNET_VALUE = 0 # Unlike in IPV4, this should *always* be valid
IPV6_MAX_SUBNET_VALUE = IPV6_SEGMENT_BIT_COUNT * IPV6_MAX_SEGMENT_COUNT - 1 # == 127


class SubnetType(str, Enum):
IPV4 = 'ipv4'
IPV6 = 'ipv6'

@classmethod
def _missing_(cls: type[SubnetType], value: object) -> SubnetType:
for member in cls:
if isinstance(value, str) and member.value == value.lower():
return member

raise ValueError("Invalid subnet type")
71 changes: 37 additions & 34 deletions iplib3/subnet.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,21 @@

from __future__ import annotations

from enum import Enum
from typing import overload

from iplib3.constants.ipv4 import (
IPV4_MIN_SEGMENT_COUNT,
IPV4_SEGMENT_BIT_COUNT,
)
from iplib3.constants.subnet import (
IPV4_MAX_SUBNET_VALUE,
IPV4_MIN_SUBNET_VALUE,
IPV4_VALID_SUBNET_SEGMENTS,
IPV6_MAX_SUBNET_VALUE,
IPV6_MIN_SUBNET_VALUE,
SubnetType,
)


class SubnetType(str, Enum):
IPV4 = 'ipv4'
IPV6 = 'ipv6'

@classmethod
def _missing_(cls: SubnetType, value: str) -> SubnetType:
for member in cls:
if member.value == value.lower():
return member
return None


class PureSubnetMask:
"""
Platform and version-independent base class for subnets
Expand Down Expand Up @@ -79,6 +67,8 @@ class SubnetMask(PureSubnetMask):
__slots__ = ('_subnet_type',)

def __init__(self: SubnetMask, subnet_mask: int | str | None = None, subnet_type: SubnetType = SubnetType.IPV6) -> None:

subnet_type = SubnetType(subnet_type)
super().__init__()

if isinstance(subnet_mask, str) and '.' in subnet_mask:
Expand Down Expand Up @@ -109,6 +99,8 @@ def _subnet_to_num(subnet_mask: int | str, subnet_type: SubnetType) -> int:
@staticmethod
def _subnet_to_num(subnet_mask: int | str | None, subnet_type: SubnetType = SubnetType.IPV6) -> int | None:

subnet_type = SubnetType(subnet_type)

if subnet_mask is None:
return None

Expand All @@ -118,24 +110,39 @@ def _subnet_to_num(subnet_mask: int | str | None, subnet_type: SubnetType = Subn
f"Expected int, string, or None",
)

if subnet_type == SubnetType.IPV4:
subnet_mask = SubnetMask._ipv4_subnet_to_num(subnet_mask)

if subnet_type == SubnetType.IPV6:
if isinstance(subnet_mask, str):
if '.' in subnet_mask:
raise ValueError("IPv6-subnets don't use a string representation")
subnet_mask = int(subnet_mask)
if not IPV6_MIN_SUBNET_VALUE <= subnet_mask <= IPV6_MAX_SUBNET_VALUE:
raise ValueError(
f"Subnet '{subnet_mask}' not in valid range "
f"({IPV6_MIN_SUBNET_VALUE}-{IPV6_MAX_SUBNET_VALUE})",
)

return int(subnet_mask)

@staticmethod
def _ipv4_subnet_to_num(subnet_mask: int | str) -> int:
if isinstance(subnet_mask, str):
if '.' in subnet_mask:
if subnet_type == SubnetType.IPV6:
raise ValueError("IPv6-subnets don't use a string representation")
segments = tuple(int(s) for s in reversed(subnet_mask.split('.')))
if len(segments) != IPV4_MIN_SEGMENT_COUNT:
raise ValueError(
f"Subnet value not valid; '{subnet_mask}' is not a valid string representation",
)

segments = list(map(int, subnet_mask.split('.')))[::-1]
total = 0
segment_sum = sum(s<<(8*idx) for idx, s in enumerate(segments))
subnet_bits = f'{segment_sum:b}'.rstrip('0')

try:
for segment in segments:
total += {
subnet: idx
for idx, subnet in enumerate(IPV4_VALID_SUBNET_SEGMENTS)
}[segment]
except KeyError as err:
raise ValueError(f"'{segment}' is an invalid value in a subnet mask") from err
if '0' in subnet_bits:
raise ValueError(f"'{subnet_mask}' is an invalid subnet mask")

return total
subnet_mask = len(subnet_bits)

try:
subnet_mask = int(subnet_mask)
Expand All @@ -144,23 +151,19 @@ def _subnet_to_num(subnet_mask: int | str | None, subnet_type: SubnetType = Subn
f"Subnet value not valid; '{subnet_mask}' is neither a valid string representation nor an integer",
) from err

if subnet_type == SubnetType.IPV4 and not IPV4_MIN_SUBNET_VALUE <= subnet_mask <= IPV4_MAX_SUBNET_VALUE:
if not IPV4_MIN_SUBNET_VALUE <= subnet_mask <= IPV4_MAX_SUBNET_VALUE:
raise ValueError(
f"Subnet '{subnet_mask}' not in valid range "
f"({IPV4_MIN_SUBNET_VALUE}-{IPV4_MAX_SUBNET_VALUE})",
)

if subnet_type == SubnetType.IPV6 and not IPV6_MIN_SUBNET_VALUE <= subnet_mask <= IPV6_MAX_SUBNET_VALUE:
raise ValueError(
f"Subnet '{subnet_mask}' not in valid range "
f"({IPV6_MIN_SUBNET_VALUE}-{IPV6_MAX_SUBNET_VALUE})",
)

return subnet_mask

@staticmethod
def _prefix_to_subnet_mask(prefix_length: int, subnet_type: SubnetType) -> str:

subnet_type = SubnetType(subnet_type)

if subnet_type == SubnetType.IPV6:
raise ValueError("IPv6 does not support string representations of subnet masks")

Expand Down
Loading