diff --git a/HISTORY.rst b/HISTORY.rst index 1424eb6..381d0a8 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,10 +3,15 @@ History ------- +v0.21.1 (unreleased) +.................... +* add ``IPv{4,6,Any}Network`` and ``IPv{4,6,Any}Interface`` types from ``ipaddress`` stdlib, #333 by @pilosus + + v0.21.0 (2019-03-15) .................... * fix typo in ``NoneIsNotAllowedError`` message, #414 by @YaraslauZhylko -* add ``IPAddress``, ``IPv4Address`` and ``IPv6Address`` types, #333 by @pilosus +* add ``IPvAnyAddress``, ``IPv4Address`` and ``IPv6Address`` types, #333 by @pilosus v0.20.1 (2019-02-26) .................... diff --git a/docs/examples/exotic.py b/docs/examples/exotic.py index 08f5bbb..98852f5 100644 --- a/docs/examples/exotic.py +++ b/docs/examples/exotic.py @@ -1,12 +1,12 @@ import uuid from decimal import Decimal -from ipaddress import IPv4Address, IPv6Address +from ipaddress import IPv4Address, IPv6Address, IPv4Interface, IPv6Interface, IPv4Network, IPv6Network from pathlib import Path from uuid import UUID from pydantic import (DSN, UUID1, UUID3, UUID4, UUID5, BaseModel, DirectoryPath, EmailStr, FilePath, NameEmail, NegativeFloat, NegativeInt, PositiveFloat, PositiveInt, PyObject, UrlStr, conbytes, condecimal, - confloat, conint, constr, IPvAnyAddress) + confloat, conint, constr, IPvAnyAddress, IPvAnyInterface, IPvAnyNetwork) class Model(BaseModel): @@ -60,6 +60,12 @@ class Model(BaseModel): ipvany: IPvAnyAddress = None ipv4: IPv4Address = None ipv6: IPv6Address = None + ip_vany_network: IPvAnyNetwork = None + ip_v4_network: IPv4Network = None + ip_v6_network: IPv6Network = None + ip_vany_interface: IPvAnyInterface = None + ip_v4_interface: IPv4Interface = None + ip_v6_interface: IPv6Interface = None m = Model( cos_function='math.cos', @@ -95,7 +101,13 @@ m = Model( uuid_v5=uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org'), ipvany=IPv4Address('192.168.0.1'), ipv4=IPv4Address('255.255.255.255'), - ipv6=IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff') + ipv6=IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'), + ip_vany_network=IPv4Network('192.168.0.0/24'), + ip_v4_network=IPv4Network('192.168.0.0/24'), + ip_v6_network=IPv6Network('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128'), + ip_vany_interface=IPv4Interface('192.168.0.0/24'), + ip_v4_interface=IPv4Interface('192.168.0.0/24'), + ip_v6_interface=IPv6Interface('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128') ) print(m.dict()) """ @@ -133,8 +145,14 @@ print(m.dict()) 'uuid_v3': UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e'), 'uuid_v4': UUID('22209f7a-aad1-491c-bb83-ea19b906d210'), 'uuid_v5': UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d'), - 'ip'='192.168.0.1', - 'ipv4'='255.255.255.255', - 'ipv6'='ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', + 'ipvany': IPv4Address('192.168.0.1'), + 'ipv4': IPv4Address('255.255.255.255'), + 'ipv6': IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'), + 'ip_vany_network': IPv4Network('192.168.0.0/24'), + 'ip_v4_network': IPv4Network('192.168.0.0/24'), + 'ip_v6_network': IPv4Network('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128'), + 'ip_vany_interface': IPv4Interface('192.168.0.0/24'), + 'ip_v4_interface': IPv4Interface('192.168.0.0/24'), + 'ip_v6_interface': IPv6Interface('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128') } """ diff --git a/docs/schema_mapping.py b/docs/schema_mapping.py index ad42438..eee09dd 100755 --- a/docs/schema_mapping.py +++ b/docs/schema_mapping.py @@ -247,6 +247,20 @@ table = [ 'Pydantic standard "format" extension', 'IPv4 or IPv4 address as used in ``ipaddress`` module', ], + [ + 'IPvAnyInterface', + 'string', + '{"format": "ipvanyinterface"}', + 'Pydantic standard "format" extension', + 'IPv4 or IPv4 interface as used in ``ipaddress`` module', + ], + [ + 'IPvAnyNetwork', + 'string', + '{"format": "ipvanynetwork"}', + 'Pydantic standard "format" extension', + 'IPv4 or IPv4 network as used in ``ipaddress`` module', + ], [ 'StrictStr', 'string', diff --git a/pydantic/errors.py b/pydantic/errors.py index 68eba2f..187bd01 100644 --- a/pydantic/errors.py +++ b/pydantic/errors.py @@ -294,9 +294,33 @@ class IPvAnyAddressError(PydanticValueError): msg_template = 'value is not a valid IPv4 or IPv6 address' +class IPvAnyInterfaceError(PydanticValueError): + msg_template = 'value is not a valid IPv4 or IPv6 interface' + + +class IPvAnyNetworkError(PydanticValueError): + msg_template = 'value is not a valid IPv4 or IPv6 network' + + class IPv4AddressError(PydanticValueError): msg_template = 'value is not a valid IPv4 address' class IPv6AddressError(PydanticValueError): msg_template = 'value is not a valid IPv6 address' + + +class IPv4NetworkError(PydanticValueError): + msg_template = 'value is not a valid IPv4 network' + + +class IPv6NetworkError(PydanticValueError): + msg_template = 'value is not a valid IPv6 network' + + +class IPv4InterfaceError(PydanticValueError): + msg_template = 'value is not a valid IPv4 interface' + + +class IPv6InterfaceError(PydanticValueError): + msg_template = 'value is not a valid IPv6 interface' diff --git a/pydantic/types.py b/pydantic/types.py index 2a644db..1a05912 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -1,9 +1,18 @@ import json import re from decimal import Decimal -from ipaddress import IPv4Address, IPv6Address, _BaseAddress +from ipaddress import ( + IPv4Address, + IPv4Interface, + IPv4Network, + IPv6Address, + IPv6Interface, + IPv6Network, + _BaseAddress, + _BaseNetwork, +) from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Pattern, Set, Type, Union, cast +from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Pattern, Set, Tuple, Type, Union, cast from uuid import UUID from . import errors @@ -63,6 +72,8 @@ __all__ = [ 'Json', 'JsonWrapper', 'IPvAnyAddress', + 'IPvAnyInterface', + 'IPvAnyNetwork', ] NoneStr = Optional[str] @@ -72,6 +83,7 @@ NoneStrBytes = Optional[StrBytes] OptionalInt = Optional[int] OptionalIntFloat = Union[OptionalInt, float] OptionalIntFloatDecimal = Union[OptionalIntFloat, Decimal] +NetworkType = Union[str, bytes, int, Tuple[Union[str, bytes, int], Union[str, int]]] if TYPE_CHECKING: # pragma: no cover @@ -516,3 +528,37 @@ class IPvAnyAddress(_BaseAddress): with change_exception(errors.IPvAnyAddressError, ValueError): return IPv6Address(value) + + +class IPvAnyInterface(_BaseAddress): + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls, value: NetworkType) -> Union[IPv4Interface, IPv6Interface]: + try: + return IPv4Interface(value) + except ValueError: + pass + + with change_exception(errors.IPvAnyInterfaceError, ValueError): + return IPv6Interface(value) + + +class IPvAnyNetwork(_BaseNetwork): # type: ignore + @classmethod + def __get_validators__(cls) -> 'CallableGenerator': + yield cls.validate + + @classmethod + def validate(cls, value: NetworkType) -> Union[IPv4Network, IPv6Network]: + # Assume IP Network is defined with a default value for ``strict`` argument. + # Define your own class if you want to specify network address check strictness. + try: + return IPv4Network(value) + except ValueError: + pass + + with change_exception(errors.IPvAnyNetworkError, ValueError): + return IPv6Network(value) diff --git a/pydantic/validators.py b/pydantic/validators.py index 3a28c6a..c11a0d5 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -3,7 +3,7 @@ from collections import OrderedDict from datetime import date, datetime, time, timedelta from decimal import Decimal, DecimalException from enum import Enum -from ipaddress import IPv4Address, IPv6Address +from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Pattern, Set, Tuple, Type, TypeVar, Union, cast from uuid import UUID @@ -235,6 +235,50 @@ def ip_v6_address_validator(v: Any) -> IPv6Address: return IPv6Address(v) +def ip_v4_network_validator(v: Any) -> IPv4Network: + """ + Assume IPv4Network initialised with a default ``strict`` argument + + See more: + https://docs.python.org/library/ipaddress.html#ipaddress.IPv4Network + """ + if isinstance(v, IPv4Network): + return v + + with change_exception(errors.IPv4NetworkError, ValueError): + return IPv4Network(v) + + +def ip_v6_network_validator(v: Any) -> IPv6Network: + """ + Assume IPv6Network initialised with a default ``strict`` argument + + See more: + https://docs.python.org/library/ipaddress.html#ipaddress.IPv6Network + """ + if isinstance(v, IPv6Network): + return v + + with change_exception(errors.IPv6NetworkError, ValueError): + return IPv6Network(v) + + +def ip_v4_interface_validator(v: Any) -> IPv4Interface: + if isinstance(v, IPv4Interface): + return v + + with change_exception(errors.IPv4InterfaceError, ValueError): + return IPv4Interface(v) + + +def ip_v6_interface_validator(v: Any) -> IPv6Interface: + if isinstance(v, IPv6Interface): + return v + + with change_exception(errors.IPv6InterfaceError, ValueError): + return IPv6Interface(v) + + def path_validator(v: Any) -> Path: if isinstance(v, Path): return v @@ -280,7 +324,8 @@ def pattern_validator(v: Any) -> Pattern[str]: pattern_validators = [not_none_validator, str_validator, pattern_validator] -# order is important here, for example: bool is a subclass of int so has to come first, datetime before date same +# order is important here, for example: bool is a subclass of int so has to come first, datetime before date same, +# IPv4Interface before IPv4Address, etc _VALIDATORS: List[Tuple[AnyType, List[AnyCallable]]] = [ (Enum, [enum_validator]), (str, [not_none_validator, str_validator, anystr_strip_whitespace, anystr_length_validator]), @@ -301,8 +346,12 @@ _VALIDATORS: List[Tuple[AnyType, List[AnyCallable]]] = [ (set, [set_validator]), (UUID, [not_none_validator, uuid_validator]), (Decimal, [not_none_validator, decimal_validator]), + (IPv4Interface, [not_none_validator, ip_v4_interface_validator]), + (IPv6Interface, [not_none_validator, ip_v6_interface_validator]), (IPv4Address, [not_none_validator, ip_v4_address_validator]), (IPv6Address, [not_none_validator, ip_v6_address_validator]), + (IPv4Network, [not_none_validator, ip_v4_network_validator]), + (IPv6Network, [not_none_validator, ip_v6_network_validator]), ] diff --git a/pydantic/version.py b/pydantic/version.py index 413d3dd..c8f58fc 100644 --- a/pydantic/version.py +++ b/pydantic/version.py @@ -2,4 +2,4 @@ from distutils.version import StrictVersion __all__ = ['VERSION'] -VERSION = StrictVersion('0.21') +VERSION = StrictVersion('0.21.1') diff --git a/tests/test_types_ipaddress.py b/tests/test_types_ipaddress.py index e101694..cd1351b 100644 --- a/tests/test_types_ipaddress.py +++ b/tests/test_types_ipaddress.py @@ -1,8 +1,14 @@ -from ipaddress import IPv4Address, IPv6Address +from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network import pytest -from pydantic import BaseModel, IPvAnyAddress, ValidationError +from pydantic import BaseModel, IPvAnyAddress, IPvAnyInterface, IPvAnyNetwork, ValidationError + +# +# ipaddress.IPv4Address +# ipaddress.IPv6Address +# pydantic.IPvAnyAddress +# @pytest.mark.parametrize( @@ -182,3 +188,354 @@ def test_ipv6address_fails(value, errors): with pytest.raises(ValidationError) as exc_info: Model(ipv6=value) assert exc_info.value.errors() == errors + + +# +# ipaddress.IPv4Network +# ipaddress.IPv6Network +# pydantic.IPvAnyNetwork +# + + +@pytest.mark.parametrize( + 'value,cls', + [ + ('192.168.0.0/24', IPv4Network), + ('192.168.128.0/30', IPv4Network), + ('2001:db00::0/120', IPv6Network), + (2 ** 32 - 1, IPv4Network), # no mask equals to mask /32 + (20_282_409_603_651_670_423_947_251_286_015, IPv6Network), # /128 + (b'\xff\xff\xff\xff', IPv4Network), # /32 + (b'\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', IPv6Network), + (('192.168.0.0', 24), IPv4Network), + (('2001:db00::0', 120), IPv6Network), + (IPv4Network('192.168.0.0/24'), IPv4Network), + ], +) +def test_ipnetwork_success(value, cls): + class Model(BaseModel): + ip: IPvAnyNetwork = None + + assert Model(ip=value).ip == cls(value) + + +@pytest.mark.parametrize( + 'value,cls', + [ + ('192.168.0.0/24', IPv4Network), + ('192.168.128.0/30', IPv4Network), + (2 ** 32 - 1, IPv4Network), # no mask equals to mask /32 + (b'\xff\xff\xff\xff', IPv4Network), # /32 + (('192.168.0.0', 24), IPv4Network), + (IPv4Network('192.168.0.0/24'), IPv4Network), + ], +) +def test_ip_v4_network_success(value, cls): + class Model(BaseModel): + ip: IPv4Network = None + + assert Model(ip=value).ip == cls(value) + + +@pytest.mark.parametrize( + 'value,cls', + [ + ('2001:db00::0/120', IPv6Network), + (20_282_409_603_651_670_423_947_251_286_015, IPv6Network), # /128 + (b'\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', IPv6Network), + (('2001:db00::0', 120), IPv6Network), + (IPv6Network('2001:db00::0/120'), IPv6Network), + ], +) +def test_ip_v6_network_success(value, cls): + class Model(BaseModel): + ip: IPv6Network = None + + assert Model(ip=value).ip == cls(value) + + +@pytest.mark.parametrize( + 'value,errors', + [ + ( + 'hello,world', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 network', 'type': 'value_error.ipvanynetwork'}], + ), + ( + '192.168.0.1.1.1/24', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 network', 'type': 'value_error.ipvanynetwork'}], + ), + ( + -1, + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 network', 'type': 'value_error.ipvanynetwork'}], + ), + ( + 2 ** 128 + 1, + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 network', 'type': 'value_error.ipvanynetwork'}], + ), + ], +) +def test_ipnetwork_fails(value, errors): + class Model(BaseModel): + ip: IPvAnyNetwork = None + + with pytest.raises(ValidationError) as exc_info: + Model(ip=value) + assert exc_info.value.errors() == errors + + +@pytest.mark.parametrize( + 'value,errors', + [ + ( + 'hello,world', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}], + ), + ( + '192.168.0.1.1.1/24', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}], + ), + (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}]), + ( + 2 ** 128 + 1, + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}], + ), + ( + '2001:db00::1/120', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}], + ), + ], +) +def test_ip_v4_network_fails(value, errors): + class Model(BaseModel): + ip: IPv4Network = None + + with pytest.raises(ValidationError) as exc_info: + Model(ip=value) + assert exc_info.value.errors() == errors + + +@pytest.mark.parametrize( + 'value,errors', + [ + ( + 'hello,world', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}], + ), + ( + '192.168.0.1.1.1/24', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}], + ), + (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}]), + ( + 2 ** 128 + 1, + [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}], + ), + ( + '192.168.0.1/24', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}], + ), + ], +) +def test_ip_v6_network_fails(value, errors): + class Model(BaseModel): + ip: IPv6Network = None + + with pytest.raises(ValidationError) as exc_info: + Model(ip=value) + assert exc_info.value.errors() == errors + + +# +# ipaddress.IPv4Interface +# ipaddress.IPv6Interface +# pydantic.IPvAnyInterface +# + + +@pytest.mark.parametrize( + 'value,cls', + [ + ('192.168.0.0/24', IPv4Interface), + ('192.168.0.1/24', IPv4Interface), + ('192.168.128.0/30', IPv4Interface), + ('192.168.128.1/30', IPv4Interface), + ('2001:db00::0/120', IPv6Interface), + ('2001:db00::1/120', IPv6Interface), + (2 ** 32 - 1, IPv4Interface), # no mask equals to mask /32 + (2 ** 32 - 1, IPv4Interface), # so ``strict`` has no effect + (20_282_409_603_651_670_423_947_251_286_015, IPv6Interface), # /128 + (20_282_409_603_651_670_423_947_251_286_014, IPv6Interface), + (b'\xff\xff\xff\xff', IPv4Interface), # /32 + (b'\xff\xff\xff\xff', IPv4Interface), + (b'\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', IPv6Interface), + (b'\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', IPv6Interface), + (('192.168.0.0', 24), IPv4Interface), + (('192.168.0.1', 24), IPv4Interface), + (('2001:db00::0', 120), IPv6Interface), + (('2001:db00::1', 120), IPv6Interface), + (IPv4Interface('192.168.0.0/24'), IPv4Interface), + (IPv4Interface('192.168.0.1/24'), IPv4Interface), + (IPv6Interface('2001:db00::0/120'), IPv6Interface), + (IPv6Interface('2001:db00::1/120'), IPv6Interface), + ], +) +def test_ipinterface_success(value, cls): + class Model(BaseModel): + ip: IPvAnyInterface = None + + assert Model(ip=value).ip == cls(value) + + +@pytest.mark.parametrize( + 'value,cls', + [ + ('192.168.0.0/24', IPv4Interface), + ('192.168.0.1/24', IPv4Interface), + ('192.168.128.0/30', IPv4Interface), + ('192.168.128.1/30', IPv4Interface), + (2 ** 32 - 1, IPv4Interface), # no mask equals to mask /32 + (2 ** 32 - 1, IPv4Interface), # so ``strict`` has no effect + (b'\xff\xff\xff\xff', IPv4Interface), # /32 + (b'\xff\xff\xff\xff', IPv4Interface), + (('192.168.0.0', 24), IPv4Interface), + (('192.168.0.1', 24), IPv4Interface), + (IPv4Interface('192.168.0.0/24'), IPv4Interface), + (IPv4Interface('192.168.0.1/24'), IPv4Interface), + ], +) +def test_ip_v4_interface_success(value, cls): + class Model(BaseModel): + ip: IPv4Interface + + assert Model(ip=value).ip == cls(value) + + +@pytest.mark.parametrize( + 'value,cls', + [ + ('2001:db00::0/120', IPv6Interface), + ('2001:db00::1/120', IPv6Interface), + (20_282_409_603_651_670_423_947_251_286_015, IPv6Interface), # /128 + (20_282_409_603_651_670_423_947_251_286_014, IPv6Interface), + (b'\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', IPv6Interface), + (b'\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', IPv6Interface), + (('2001:db00::0', 120), IPv6Interface), + (('2001:db00::1', 120), IPv6Interface), + (IPv6Interface('2001:db00::0/120'), IPv6Interface), + (IPv6Interface('2001:db00::1/120'), IPv6Interface), + ], +) +def test_ip_v6_interface_success(value, cls): + class Model(BaseModel): + ip: IPv6Interface = None + + assert Model(ip=value).ip == cls(value) + + +@pytest.mark.parametrize( + 'value,errors', + [ + ( + 'hello,world', + [ + { + 'loc': ('ip',), + 'msg': 'value is not a valid IPv4 or IPv6 interface', + 'type': 'value_error.ipvanyinterface', + } + ], + ), + ( + '192.168.0.1.1.1/24', + [ + { + 'loc': ('ip',), + 'msg': 'value is not a valid IPv4 or IPv6 interface', + 'type': 'value_error.ipvanyinterface', + } + ], + ), + ( + -1, + [ + { + 'loc': ('ip',), + 'msg': 'value is not a valid IPv4 or IPv6 interface', + 'type': 'value_error.ipvanyinterface', + } + ], + ), + ( + 2 ** 128 + 1, + [ + { + 'loc': ('ip',), + 'msg': 'value is not a valid IPv4 or IPv6 interface', + 'type': 'value_error.ipvanyinterface', + } + ], + ), + ], +) +def test_ipinterface_fails(value, errors): + class Model(BaseModel): + ip: IPvAnyInterface = None + + with pytest.raises(ValidationError) as exc_info: + Model(ip=value) + assert exc_info.value.errors() == errors + + +@pytest.mark.parametrize( + 'value,errors', + [ + ( + 'hello,world', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 interface', 'type': 'value_error.ipv4interface'}], + ), + ( + '192.168.0.1.1.1/24', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 interface', 'type': 'value_error.ipv4interface'}], + ), + (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 interface', 'type': 'value_error.ipv4interface'}]), + ( + 2 ** 128 + 1, + [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 interface', 'type': 'value_error.ipv4interface'}], + ), + ], +) +def test_ip_v4_interface_fails(value, errors): + class Model(BaseModel): + ip: IPv4Interface = None + + with pytest.raises(ValidationError) as exc_info: + Model(ip=value) + assert exc_info.value.errors() == errors + + +@pytest.mark.parametrize( + 'value,errors', + [ + ( + 'hello,world', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 interface', 'type': 'value_error.ipv6interface'}], + ), + ( + '192.168.0.1.1.1/24', + [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 interface', 'type': 'value_error.ipv6interface'}], + ), + (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 interface', 'type': 'value_error.ipv6interface'}]), + ( + 2 ** 128 + 1, + [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 interface', 'type': 'value_error.ipv6interface'}], + ), + ], +) +def test_ip_v6_interface_fails(value, errors): + class Model(BaseModel): + ip: IPv6Interface = None + + with pytest.raises(ValidationError) as exc_info: + Model(ip=value) + assert exc_info.value.errors() == errors