mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
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:
committed by
Samuel Colvin
parent
37855aa90c
commit
f41e3afaa0
@@ -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
@@ -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',
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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]),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user