diff --git a/HISTORY.rst b/HISTORY.rst index f8b0b42..6bf30bf 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,10 @@ History ------- +v0.9.1 (2018-XX-XX) +................... +* add ``UUID1``, ``UUID3``, ``UUID4`` and ``UUID5`` types #167 + v0.9.0 (2018-04-28) ................... * tweak email-validator import error message #145 diff --git a/docs/examples/exotic.py b/docs/examples/exotic.py index 9fbdc5d..79f0b2b 100644 --- a/docs/examples/exotic.py +++ b/docs/examples/exotic.py @@ -1,8 +1,9 @@ +import uuid from pathlib import Path from uuid import UUID -from pydantic import (DSN, BaseModel, EmailStr, NameEmail, NegativeFloat, NegativeInt, PositiveFloat, PositiveInt, - PyObject, confloat, conint, constr) +from pydantic import (DSN, UUID1, UUID3, UUID4, UUID5, BaseModel, EmailStr, NameEmail, NegativeFloat, NegativeInt, + PositiveFloat, PositiveInt, PyObject, confloat, conint, constr) class Model(BaseModel): @@ -32,7 +33,11 @@ class Model(BaseModel): db_driver = 'postgres' db_query: dict = None dsn: DSN = None - uuid: UUID = None + uuid_any: UUID = None + uuid_v1: UUID1 = None + uuid_v3: UUID3 = None + uuid_v4: UUID4 = None + uuid_v5: UUID5 = None m = Model( cos_function='math.cos', @@ -48,7 +53,11 @@ m = Model( neg_float=-2.3, email_address='Samuel Colvin ', email_and_name='Samuel Colvin ', - uuid='ebcdab58-6eb8-46fb-a190-d07a33e9eac8' + uuid_any=uuid.uuid4(), + 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') ) print(m.dict()) """ @@ -68,6 +77,10 @@ print(m.dict()) 'email_and_name': ")>, ... 'dsn': 'postgres://postgres@localhost:5432/foobar', - 'uuid': UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8'), + 'uuid_any': UUID('ebcdab58-6eb8-46fb-a190-d07a33e9eac8'), + 'uuid_v1': UUID('c96e505c-4c62-11e8-a27c-dca90496b483'), + 'uuid_v3': UUID('6fa459ea-ee8a-3ca4-894e-db77e160355e'), + 'uuid_v4': UUID('22209f7a-aad1-491c-bb83-ea19b906d210'), + 'uuid_v5': UUID('886313e1-3b8a-5372-9b90-0c9aee199e5d'), } """ diff --git a/pydantic/types.py b/pydantic/types.py index 7526076..a8013f7 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -1,5 +1,6 @@ import re from typing import Optional, Type, Union +from uuid import UUID from .utils import import_string, make_dsn, validate_email from .validators import str_validator @@ -29,6 +30,10 @@ __all__ = [ 'confloat', 'PositiveFloat', 'NegativeFloat', + 'UUID1', + 'UUID3', + 'UUID4', + 'UUID5', ] NoneStr = Optional[str] @@ -237,4 +242,20 @@ class NegativeFloat(ConstrainedFloat): lt = 0 +class UUID1(UUID): + _required_version = 1 + + +class UUID3(UUID): + _required_version = 3 + + +class UUID4(UUID): + _required_version = 4 + + +class UUID5(UUID): + _required_version = 5 + + # TODO, JsonEither, JsonList, JsonDict diff --git a/pydantic/validators.py b/pydantic/validators.py index aae4863..b9595ef 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -114,16 +114,20 @@ def enum_validator(v, field, config, **kwargs) -> Enum: return enum_v.value if config.use_enum_values else enum_v -def uuid_validator(v) -> UUID: - if isinstance(v, UUID): - return v - elif isinstance(v, str): - return UUID(v) +def uuid_validator(v, field, config, **kwargs) -> UUID: + if isinstance(v, str): + v = UUID(v) elif isinstance(v, (bytes, bytearray)): - return UUID(v.decode()) - else: + v = UUID(v.decode()) + elif not isinstance(v, UUID): raise ValueError(f'str, byte or native UUID type expected not {type(v)}') + required_version = getattr(field.type_, '_required_version', None) + if required_version and v.version != required_version: + raise ValueError(f'uuid version {required_version} expected, not {v.version}') + + return v + # order is important here, for example: bool is a subclass of int so has to come first, datetime before date same _VALIDATORS = [ diff --git a/tests/test_types.py b/tests/test_types.py index 4e7149b..22d85a1 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,4 +1,5 @@ import os +import uuid from collections import OrderedDict from datetime import date, datetime, time, timedelta from enum import Enum, IntEnum @@ -6,8 +7,8 @@ from uuid import UUID import pytest -from pydantic import (DSN, BaseModel, EmailStr, NameEmail, NegativeFloat, NegativeInt, PositiveFloat, PositiveInt, - PyObject, StrictStr, ValidationError, confloat, conint, constr) +from pydantic import (DSN, UUID1, UUID3, UUID4, UUID5, BaseModel, EmailStr, NameEmail, NegativeFloat, NegativeInt, + PositiveFloat, PositiveInt, PyObject, StrictStr, ValidationError, confloat, conint, constr) try: import email_validator @@ -461,6 +462,32 @@ v: Model(v=None) +class UUIDModel(BaseModel): + a: UUID1 + b: UUID3 + c: UUID4 + d: UUID5 + + +def test_uuid_validation(): + a = uuid.uuid1() + b = uuid.uuid3(uuid.NAMESPACE_DNS, 'python.org') + c = uuid.uuid4() + d = uuid.uuid5(uuid.NAMESPACE_DNS, 'python.org') + + m = UUIDModel(a=a, b=b, c=c, d=d) + assert m.dict() == { + 'a': a, + 'b': b, + 'c': c, + 'd': d, + } + + with pytest.raises(ValidationError) as exc_info: + UUIDModel(a=d, b=c, c=b, d=a) + assert exc_info.value.message == '4 errors validating input' + + def test_anystr_strip_whitespace_enabled(): class Model(BaseModel): str_check: str