diff --git a/docs/index.md b/docs/index.md index ae76ef2a..53d151d6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,7 +14,7 @@ ::: validators.iban - +::: validators.ip_address ::: validators.length diff --git a/poetry.lock b/poetry.lock index fa8d1b20..55104e53 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1316,4 +1316,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "2503c8f28f5fefc9238c258ebef5cfb19013a2558f1b644e25b2c84bdc18a2f6" +content-hash = "3d0d330e676d623b065cd7111aa7bc28a4dd44c7bba6f97938d7e54009546193" diff --git a/tests/test_ip_address.py b/tests/test_ip_address.py new file mode 100644 index 00000000..98a977fa --- /dev/null +++ b/tests/test_ip_address.py @@ -0,0 +1,103 @@ +"""Test IP Address.""" +# -*- coding: utf-8 -*- + +# external +import pytest + +# local +from validators import ipv4, ipv6, ValidationFailure + + +@pytest.mark.parametrize( + ("address",), + [ + ("127.0.0.1",), + ("123.5.77.88",), + ("12.12.12.12",), + # w/ cidr + ("127.0.0.1/0",), + ("123.5.77.88/8",), + ("12.12.12.12/32",), + ], +) +def test_returns_true_on_valid_ipv4_address(address: str): + """Test returns true on valid ipv4 address.""" + assert ipv4(address) + assert not ipv6(address) + + +@pytest.mark.parametrize( + ("address",), + [ + # leading zeroes error-out from Python 3.9.5 + # ("100.100.033.033",), + ("900.200.100.75",), + ("0127.0.0.1",), + ("abc.0.0.1",), + # w/ cidr + ("1.1.1.1/-1",), + ("1.1.1.1/33",), + ("1.1.1.1/foo",), + ], +) +def test_returns_failed_validation_on_invalid_ipv4_address(address: str): + """Test returns failed validation on invalid ipv4 address.""" + assert isinstance(ipv4(address), ValidationFailure) + + +@pytest.mark.parametrize( + ("address",), + [ + ("::",), + ("::1",), + ("1::",), + ("dead:beef:0:0:0:0000:42:1",), + ("abcd:ef::42:1",), + ("0:0:0:0:0:ffff:1.2.3.4",), + ("::192.168.30.2",), + ("0000:0000:0000:0000:0000::",), + ("0:a:b:c:d:e:f::",), + # w/ cidr + ("::1/128",), + ("::1/0",), + ("dead:beef:0:0:0:0:42:1/8",), + ("abcd:ef::42:1/32",), + ("0:0:0:0:0:ffff:1.2.3.4/16",), + ("2001:0db8:85a3:0000:0000:8a2e:0370:7334/64",), + ("::192.168.30.2/128",), + ], +) +def test_returns_true_on_valid_ipv6_address(address: str): + """Test returns true on valid ipv6 address.""" + assert ipv6(address) + assert not ipv4(address) + + +@pytest.mark.parametrize( + ("address",), + [ + ("abc.0.0.1",), + ("abcd:1234::123::1",), + ("1:2:3:4:5:6:7:8:9",), + ("1:2:3:4:5:6:7:8::",), + ("1:2:3:4:5:6:7::8:9",), + ("abcd::1ffff",), + ("1111:",), + (":8888",), + (":1.2.3.4",), + ("18:05",), + (":",), + (":1:2:",), + (":1:2::",), + ("::1:2::",), + ("8::1:2::9",), + ("02001:0000:1234:0000:0000:C1C0:ABCD:0876",), + # w/ cidr + ("::1/129",), + ("::1/-1",), + ("::1/foo",), + ], +) +def test_returns_failed_validation_on_invalid_ipv6_address(address: str): + """Test returns failed validation on invalid ipv6 address.""" + assert isinstance(ipv6(address), ValidationFailure) diff --git a/tests/test_ipv4.py b/tests/test_ipv4.py deleted file mode 100644 index f0f2f372..00000000 --- a/tests/test_ipv4.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -import pytest - -from validators import ipv4, ipv6, ValidationFailure - - -@pytest.mark.parametrize(('address',), [ - ('127.0.0.1',), - ('123.5.77.88',), - ('12.12.12.12',), -]) -def test_returns_true_on_valid_ipv4_address(address): - assert ipv4(address) - assert not ipv6(address) - - -@pytest.mark.parametrize(('address',), [ - ('abc.0.0.1',), - ('1278.0.0.1',), - ('127.0.0.abc',), - ('900.200.100.75',), - ('0127.0.0.1',), -]) -def test_returns_failed_validation_on_invalid_ipv4_address(address): - assert isinstance(ipv4(address), ValidationFailure) diff --git a/tests/test_ipv4_cidr.py b/tests/test_ipv4_cidr.py deleted file mode 100644 index 3216a17a..00000000 --- a/tests/test_ipv4_cidr.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -import pytest - -from validators import ipv4_cidr, ipv6_cidr, ValidationFailure - - -@pytest.mark.parametrize(('cidr',), [ - ('127.0.0.1/0',), - ('123.5.77.88/8',), - ('12.12.12.12/32',), -]) -def test_returns_true_on_valid_ipv4_cidr(cidr): - assert ipv4_cidr(cidr) - assert not ipv6_cidr(cidr) - - -@pytest.mark.parametrize(('cidr',), [ - ('abc.0.0.1',), - ('1.1.1.1',), - ('1.1.1.1/-1',), - ('1.1.1.1/33',), - ('1.1.1.1/foo',), -]) -def test_returns_failed_validation_on_invalid_ipv4_cidr(cidr): - assert isinstance(ipv4_cidr(cidr), ValidationFailure) diff --git a/tests/test_ipv6.py b/tests/test_ipv6.py deleted file mode 100644 index 286f1fb5..00000000 --- a/tests/test_ipv6.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -import pytest - -from validators import ipv4, ipv6, ValidationFailure - - -@pytest.mark.parametrize(('address',), [ - ('::',), - ('::1',), - ('1::',), - ('dead:beef:0:0:0:0000:42:1',), - ('abcd:ef::42:1',), - ('0:0:0:0:0:ffff:1.2.3.4',), - ('::192.168.30.2',), - ('0000:0000:0000:0000:0000::',), - ('0:a:b:c:d:e:f::',), -]) -def test_returns_true_on_valid_ipv6_address(address): - assert ipv6(address) - assert not ipv4(address) - - -@pytest.mark.parametrize(('address',), [ - ('abc.0.0.1',), - ('abcd:1234::123::1',), - ('1:2:3:4:5:6:7:8:9',), - ('1:2:3:4:5:6:7:8::',), - ('1:2:3:4:5:6:7::8:9',), - ('abcd::1ffff',), - ('1111:',), - (':8888',), - (':1.2.3.4',), - ('18:05',), - (':',), - (':1:2:',), - (':1:2::',), - ('::1:2::',), - ('8::1:2::9',), - ('02001:0000:1234:0000:0000:C1C0:ABCD:0876',), -]) -def test_returns_failed_validation_on_invalid_ipv6_address(address): - assert isinstance(ipv6(address), ValidationFailure) diff --git a/tests/test_ipv6_cidr.py b/tests/test_ipv6_cidr.py deleted file mode 100644 index 308390a9..00000000 --- a/tests/test_ipv6_cidr.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -import pytest - -from validators import ipv4_cidr, ipv6_cidr, ValidationFailure - - -@pytest.mark.parametrize(('cidr',), [ - ('::1/0',), - ('dead:beef:0:0:0:0:42:1/8',), - ('abcd:ef::42:1/32',), - ('0:0:0:0:0:ffff:1.2.3.4/64',), - ('::192.168.30.2/128',), -]) -def test_returns_true_on_valid_ipv6_cidr(cidr): - assert ipv6_cidr(cidr) - assert not ipv4_cidr(cidr) - - -@pytest.mark.parametrize(('cidr',), [ - ('abc.0.0.1',), - ('abcd:1234::123::1',), - ('1:2:3:4:5:6:7:8:9',), - ('abcd::1ffff',), - ('1.1.1.1',), - ('::1',), - ('::1/129',), - ('::1/-1',), - ('::1/foo',), -]) -def test_returns_failed_validation_on_invalid_ipv6_cidr(cidr): - assert isinstance(ipv6_cidr(cidr), ValidationFailure) diff --git a/validators/__init__.py b/validators/__init__.py index f837445f..2005ed88 100644 --- a/validators/__init__.py +++ b/validators/__init__.py @@ -9,7 +9,7 @@ from .hashes import md5, sha1, sha224, sha256, sha512 from .i18n import fi_business_id, fi_ssn from .iban import iban -from .ip_address import ipv4, ipv4_cidr, ipv6, ipv6_cidr +from .ip_address import ipv4, ipv6 from .length import length from .mac_address import mac_address from .slug import slug @@ -29,9 +29,7 @@ "fi_business_id", "fi_ssn", "iban", - "ipv4_cidr", "ipv4", - "ipv6_cidr", "ipv6", "jcb", "length", diff --git a/validators/ip_address.py b/validators/ip_address.py index e0c061db..309bd674 100644 --- a/validators/ip_address.py +++ b/validators/ip_address.py @@ -1,156 +1,111 @@ +"""IP Address.""" + +# standard +from ipaddress import ( + AddressValueError, + NetmaskValueError, + IPv4Address, + IPv4Network, + IPv6Address, + IPv6Network, +) + +# local from .utils import validator @validator -def ipv4(value): - """ - Return whether a given value is a valid IP version 4 address. - - This validator is based on `WTForms IPAddress validator`_ +def ipv4(value: str, /, *, cidr: bool = True, strict: bool = False): + """Returns whether a given value is a valid IPv4 address. - .. _WTForms IPAddress validator: - https://github.com/wtforms/wtforms/blob/master/wtforms/validators.py + From Python version 3.9.5 leading zeros are no longer tolerated + and are treated as an error. The initial version of ipv4 validator + was inspired from [WTForms IPAddress validator][1]. - Examples:: + [1]: https://github.com/wtforms/wtforms/blob/master/src/wtforms/validators.py + Examples: >>> ipv4('123.0.0.7') - True - + # Output: True + >>> ipv4('1.1.1.1/8') + # Output: True >>> ipv4('900.80.70.11') - ValidationFailure(func=ipv4, args={'value': '900.80.70.11'}) - - .. versionadded:: 0.2 - - :param value: IP address string to validate - """ - groups = value.split(".") - if ( - len(groups) != 4 - or any(not x.isdigit() for x in groups) - or any(len(x) > 3 for x in groups) - ): - return False - return all(0 <= int(part) < 256 for part in groups) - - -@validator -def ipv4_cidr(value): - """ - Return whether a given value is a valid CIDR-notated IP version 4 - address range. - - This validator is based on RFC4632 3.1. - - Examples:: - - >>> ipv4_cidr('1.1.1.1/8') - True - - >>> ipv4_cidr('1.1.1.1') - ValidationFailure(func=ipv4_cidr, args={'value': '1.1.1.1'}) + # Output: ValidationFailure(func=ipv4, args={'value': '900.80.70.11'}) + + Args: + value: + IP address string to validate. + cidr: + IP address string may contain CIDR annotation + strict: + If strict is True and host bits are set in the supplied address. + Otherwise, the host bits are masked out to determine the + appropriate network address. ref [IPv4Network][2]. + [2]: https://docs.python.org/3/library/ipaddress.html#ipaddress.IPv4Network + + Returns: + (Literal[True]): + If `value` is a valid IPv4 address. + (ValidationFailure): + If `value` is an invalid IPv4 address. + + Note: + - *In version 0.14.0*: + - Add supports for CIDR notation + + > *New in version 0.2.0* """ try: - prefix, suffix = value.split('/', 2) - except ValueError: + if cidr and value.count("/") == 1: + return IPv4Network(value, strict=strict) + return IPv4Address(value) + except (AddressValueError, NetmaskValueError): return False - if not ipv4(prefix) or not suffix.isdigit(): - return False - return 0 <= int(suffix) <= 32 @validator -def ipv6(value): - """ - Return whether a given value is a valid IP version 6 address - (including IPv4-mapped IPv6 addresses). +def ipv6(value: str, /, *, cidr: bool = True, strict: bool = False): + """Returns if a given value is a valid IPv6 address. - This validator is based on `WTForms IPAddress validator`_. + Including IPv4-mapped IPv6 addresses. The initial version of ipv6 validator + was inspired from [WTForms IPAddress validator][1]. - .. _WTForms IPAddress validator: - https://github.com/wtforms/wtforms/blob/master/wtforms/validators.py - - Examples:: - - >>> ipv6('abcd:ef::42:1') - True + [1]: https://github.com/wtforms/wtforms/blob/master/src/wtforms/validators.py + Examples: >>> ipv6('::ffff:192.0.2.128') - True - - >>> ipv6('::192.0.2.128') - True - + # Output: True + >>> ipv6('::1/128') + # Output: True >>> ipv6('abc.0.0.1') - ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'}) - - .. versionadded:: 0.2 - - :param value: IP address string to validate - """ - ipv6_groups = value.split(':') - if len(ipv6_groups) == 1: - return False - ipv4_groups = ipv6_groups[-1].split('.') - - if len(ipv4_groups) > 1: - if not ipv4(ipv6_groups[-1]): - return False - ipv6_groups = ipv6_groups[:-1] - else: - ipv4_groups = [] - - count_blank = 0 - for part in ipv6_groups: - if not part: - count_blank += 1 - continue - try: - num = int(part, 16) - except ValueError: - return False - else: - if not 0 <= num <= 65536 or len(part) > 4: - return False - - max_groups = 6 if ipv4_groups else 8 - part_count = len(ipv6_groups) - count_blank - if count_blank == 0 and part_count == max_groups: - # no :: -> must have size of max_groups - return True - elif count_blank == 1 and ipv6_groups[-1] and ipv6_groups[0] and part_count < max_groups: - # one :: inside the address or prefix or suffix : -> filter least two cases - return True - elif count_blank == 2 and part_count < max_groups and ( - ((ipv6_groups[0] and not ipv6_groups[-1]) or (not ipv6_groups[0] and ipv6_groups[-1])) or ipv4_groups): - # leading or trailing :: or : at end and begin -> filter last case - # Check if it has ipv4 groups because they get removed from the ipv6_groups - return True - elif count_blank == 3 and part_count == 0: - # :: is the address -> filter everything else - return True - return False - - -@validator -def ipv6_cidr(value): - """ - Returns whether a given value is a valid CIDR-notated IP version 6 - address range. - - This validator is based on RFC4632 3.1. - - Examples:: - - >>> ipv6_cidr('::1/128') - True - - >>> ipv6_cidr('::1') - ValidationFailure(func=ipv6_cidr, args={'value': '::1'}) + # Output: ValidationFailure(func=ipv6, args={'value': 'abc.0.0.1'}) + + Args: + value: + IP address string to validate. + cidr: + IP address string may contain CIDR annotation + strict: + If strict is True and host bits are set in the supplied address. + Otherwise, the host bits are masked out to determine the + appropriate network address. ref [IPv6Network][2]. + [2]: https://docs.python.org/3/library/ipaddress.html#ipaddress.IPv6Network + + Returns: + (Literal[True]): + If `value` is a valid IPv6 address. + (ValidationFailure): + If `value` is an invalid IPv6 address. + + Note: + - *In version 0.14.0*: + - Add supports for CIDR notation + + > *New in version 0.2.0* """ try: - prefix, suffix = value.split('/', 2) - except ValueError: - return False - if not ipv6(prefix) or not suffix.isdigit(): + if cidr and value.count("/") == 1: + return IPv6Network(value, strict=strict) + return IPv6Address(value) + except (AddressValueError, NetmaskValueError): return False - return 0 <= int(suffix) <= 128