ipaddress.IPAddress support (#417)

* ipaddress-compatible types added, fix #333

* Unittests for ipaddress-types added

* Docs updated after ipaddress-types added

* HISTORY.rst updated to reflect ipaddress-related types introduction

* Fix docs table format

* Strings double quotes reverted

* ipaddress types support fixed, IPvAnyAddress type redefined

* Error handling fixed for ipaddress-related types

* Positive cases for IPv4Address and IPv6Address types in unittests added
This commit is contained in:
Vitaly R. Samigullin
2019-03-15 13:18:06 +03:00
committed by Samuel Colvin
parent 37855aa90c
commit f41e3afaa0
7 changed files with 255 additions and 3 deletions
+1
View File
@@ -6,6 +6,7 @@ History
v0.20.2 (unreleased)
....................
* fix typo in `NoneIsNotAllowedError` message, #414 by @YaraslauZhylko
* add ``IPAddress``, ``IPv4Address`` and ``IPv6Address`` types, #333 by @pilosus
v0.20.1 (2019-02-26)
....................
+12 -2
View File
@@ -1,11 +1,12 @@
import uuid
from decimal import Decimal
from ipaddress import IPv4Address, IPv6Address
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)
confloat, conint, constr, IPvAnyAddress)
class Model(BaseModel):
@@ -56,6 +57,9 @@ class Model(BaseModel):
uuid_v3: UUID3 = None
uuid_v4: UUID4 = None
uuid_v5: UUID5 = None
ipvany: IPvAnyAddress = None
ipv4: IPv4Address = None
ipv6: IPv6Address = None
m = Model(
cos_function='math.cos',
@@ -88,7 +92,10 @@ m = Model(
uuid_v1=uuid.uuid1(),
uuid_v3=uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org'),
uuid_v4=uuid.uuid4(),
uuid_v5=uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org')
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')
)
print(m.dict())
"""
@@ -126,5 +133,8 @@ 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',
}
"""
+7
View File
@@ -240,6 +240,13 @@ table = [
'Pydantic standard "format" extension',
''
],
[
'IPvAnyAddress',
'string',
'{"format": "ipvanyaddress"}',
'Pydantic standard "format" extension',
'IPv4 or IPv4 address as used in ``ipaddress`` module',
],
[
'StrictStr',
'string',
+12
View File
@@ -288,3 +288,15 @@ class DataclassTypeError(PydanticTypeError):
class CallableError(PydanticTypeError):
msg_template = '{value} is not callable'
class IPvAnyAddressError(PydanticValueError):
msg_template = 'value is not a valid IPv4 or IPv6 address'
class IPv4AddressError(PydanticValueError):
msg_template = 'value is not a valid IPv4 address'
class IPv6AddressError(PydanticValueError):
msg_template = 'value is not a valid IPv6 address'
+20 -1
View File
@@ -1,12 +1,13 @@
import json
import re
from decimal import Decimal
from ipaddress import IPv4Address, IPv6Address, _BaseAddress
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Generator, Optional, Pattern, Set, Type, Union, cast
from uuid import UUID
from . import errors
from .utils import AnyType, import_string, make_dsn, url_regex_generator, validate_email
from .utils import AnyType, change_exception, import_string, make_dsn, url_regex_generator, validate_email
from .validators import (
anystr_length_validator,
anystr_strip_whitespace,
@@ -61,6 +62,7 @@ __all__ = [
'DirectoryPath',
'Json',
'JsonWrapper',
'IPvAnyAddress',
]
NoneStr = Optional[str]
@@ -71,6 +73,7 @@ OptionalInt = Optional[int]
OptionalIntFloat = Union[OptionalInt, float]
OptionalIntFloatDecimal = Union[OptionalIntFloat, Decimal]
if TYPE_CHECKING: # pragma: no cover
from .utils import AnyCallable
@@ -497,3 +500,19 @@ class Json(metaclass=JsonMeta):
raise errors.JsonError()
except TypeError:
raise errors.JsonTypeError()
class IPvAnyAddress(_BaseAddress):
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate
@classmethod
def validate(cls, value: Union[str, bytes, int]) -> Union[IPv4Address, IPv6Address]:
try:
return IPv4Address(value)
except ValueError:
pass
with change_exception(errors.IPvAnyAddressError, ValueError):
return IPv6Address(value)
+19
View File
@@ -3,6 +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 pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Pattern, Set, Tuple, Type, TypeVar, Union, cast
from uuid import UUID
@@ -218,6 +219,22 @@ def decimal_validator(v: Any) -> Decimal:
return v
def ip_v4_address_validator(v: Any) -> IPv4Address:
if isinstance(v, IPv4Address):
return v
with change_exception(errors.IPv4AddressError, ValueError):
return IPv4Address(v)
def ip_v6_address_validator(v: Any) -> IPv6Address:
if isinstance(v, IPv6Address):
return v
with change_exception(errors.IPv6AddressError, ValueError):
return IPv6Address(v)
def path_validator(v: Any) -> Path:
if isinstance(v, Path):
return v
@@ -284,6 +301,8 @@ _VALIDATORS: List[Tuple[AnyType, List[AnyCallable]]] = [
(set, [set_validator]),
(UUID, [not_none_validator, uuid_validator]),
(Decimal, [not_none_validator, decimal_validator]),
(IPv4Address, [not_none_validator, ip_v4_address_validator]),
(IPv6Address, [not_none_validator, ip_v6_address_validator]),
]
+184
View File
@@ -0,0 +1,184 @@
from ipaddress import IPv4Address, IPv6Address
import pytest
from pydantic import BaseModel, IPvAnyAddress, ValidationError
@pytest.mark.parametrize(
'value,cls',
[
('0.0.0.0', IPv4Address),
('1.1.1.1', IPv4Address),
('10.10.10.10', IPv4Address),
('192.168.0.1', IPv4Address),
('255.255.255.255', IPv4Address),
('::1:0:1', IPv6Address),
('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff', IPv6Address),
(b'\x00\x00\x00\x00', IPv4Address),
(b'\x01\x01\x01\x01', IPv4Address),
(b'\n\n\n\n', IPv4Address),
(b'\xc0\xa8\x00\x01', IPv4Address),
(b'\xff\xff\xff\xff', IPv4Address),
(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01', IPv6Address),
(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', IPv6Address),
(0, IPv4Address),
(16_843_009, IPv4Address),
(168_430_090, IPv4Address),
(3_232_235_521, IPv4Address),
(4_294_967_295, IPv4Address),
(4_294_967_297, IPv6Address),
(340_282_366_920_938_463_463_374_607_431_768_211_455, IPv6Address),
(IPv4Address('192.168.0.1'), IPv4Address),
(IPv6Address('::1:0:1'), IPv6Address),
],
)
def test_ipaddress_success(value, cls):
class Model(BaseModel):
ip: IPvAnyAddress
assert Model(ip=value).ip == cls(value)
@pytest.mark.parametrize(
'value',
[
'0.0.0.0',
'1.1.1.1',
'10.10.10.10',
'192.168.0.1',
'255.255.255.255',
b'\x00\x00\x00\x00',
b'\x01\x01\x01\x01',
b'\n\n\n\n',
b'\xc0\xa8\x00\x01',
b'\xff\xff\xff\xff',
0,
16_843_009,
168_430_090,
3_232_235_521,
4_294_967_295,
IPv4Address('0.0.0.0'),
IPv4Address('1.1.1.1'),
IPv4Address('10.10.10.10'),
IPv4Address('192.168.0.1'),
IPv4Address('255.255.255.255'),
],
)
def test_ipv4address_success(value):
class Model(BaseModel):
ipv4: IPv4Address
assert Model(ipv4=value).ipv4 == IPv4Address(value)
@pytest.mark.parametrize(
'value',
[
'::1:0:1',
'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01',
b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff',
4_294_967_297,
340_282_366_920_938_463_463_374_607_431_768_211_455,
IPv6Address('::1:0:1'),
IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'),
],
)
def test_ipv6address_success(value):
class Model(BaseModel):
ipv6: IPv6Address
assert Model(ipv6=value).ipv6 == IPv6Address(value)
@pytest.mark.parametrize(
'value,errors',
[
(
'hello,world',
[{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 address', 'type': 'value_error.ipvanyaddress'}],
),
(
'192.168.0.1.1.1',
[{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 address', 'type': 'value_error.ipvanyaddress'}],
),
(
-1,
[{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 address', 'type': 'value_error.ipvanyaddress'}],
),
(
2 ** 128 + 1,
[{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 address', 'type': 'value_error.ipvanyaddress'}],
),
],
)
def test_ipaddress_fails(value, errors):
class Model(BaseModel):
ip: IPvAnyAddress
with pytest.raises(ValidationError) as exc_info:
Model(ip=value)
assert exc_info.value.errors() == errors
@pytest.mark.parametrize(
'value,errors',
[
(
'hello,world',
[{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}],
),
(
'192.168.0.1.1.1',
[{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}],
),
(-1, [{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}]),
(
2 ** 32 + 1,
[{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}],
),
(
IPv6Address('::0:1:0'),
[{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}],
),
],
)
def test_ipv4address_fails(value, errors):
class Model(BaseModel):
ipv4: IPv4Address
with pytest.raises(ValidationError) as exc_info:
Model(ipv4=value)
assert exc_info.value.errors() == errors
@pytest.mark.parametrize(
'value,errors',
[
(
'hello,world',
[{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}],
),
(
'192.168.0.1.1.1',
[{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}],
),
(-1, [{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}]),
(
2 ** 128 + 1,
[{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}],
),
(
IPv4Address('192.168.0.1'),
[{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}],
),
],
)
def test_ipv6address_fails(value, errors):
class Model(BaseModel):
ipv6: IPv6Address
with pytest.raises(ValidationError) as exc_info:
Model(ipv6=value)
assert exc_info.value.errors() == errors