IPv{4,6,Any}{Interface,Network} types added (#419)

* IPv{4,6,Any}{Network,Interface} types added, unittests and docs updated

* HISTORY.rst minor update

* Remove strict argument from IP network types

* IP Networks validators and type hints fixed

* tweak history
This commit is contained in:
Vitaly R. Samigullin
2019-03-20 21:13:04 +03:00
committed by Samuel Colvin
parent 1fd509d8a4
commit 0458f9ece9
8 changed files with 527 additions and 14 deletions
+6 -1
View File
@@ -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)
....................
+24 -6
View File
@@ -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')
}
"""
+14
View File
@@ -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',
+24
View File
@@ -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'
+48 -2
View File
@@ -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)
+51 -2
View File
@@ -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]),
]
+1 -1
View File
@@ -2,4 +2,4 @@ from distutils.version import StrictVersion
__all__ = ['VERSION']
VERSION = StrictVersion('0.21')
VERSION = StrictVersion('0.21.1')
+359 -2
View File
@@ -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