diff --git a/changes/1324-PrettyWood.md b/changes/1324-PrettyWood.md new file mode 100644 index 00000000000..3d3c4f9ca96 --- /dev/null +++ b/changes/1324-PrettyWood.md @@ -0,0 +1 @@ +add basic support for `namedtuple` type \ No newline at end of file diff --git a/docs/usage/types.md b/docs/usage/types.md index 9a41fb1a735..0e10b850e9b 100644 --- a/docs/usage/types.md +++ b/docs/usage/types.md @@ -54,6 +54,15 @@ with custom properties and validation. : allows `list`, `tuple`, `set`, `frozenset`, `deque`, or generators and casts to a deque; see `typing.Deque` below for sub-type constraints +`namedtuple` +: Same as `tuple` but instantiates with the given namedtuple + +!!! warning + _pydantic_ doesn't validate the type of the fields inside the `namedtuple`! + Even if you use `typing.NamedTuple` and declare the expected types, _pydantic_ will just + make sure you get an instance of `namedtuple`. + Please use a `BaseModel` or `dataclass` if you expect validation + `datetime.date` : see [Datetime Types](#datetime-types) below for more detail on parsing and validation diff --git a/pydantic/typing.py b/pydantic/typing.py index e71228f67c6..94dc47b610d 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -155,6 +155,7 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]: 'is_literal_type', 'literal_values', 'Literal', + 'is_namedtuple_class', 'is_new_type', 'new_type_supertype', 'is_classvar', @@ -258,6 +259,13 @@ def all_literal_values(type_: Type[Any]) -> Tuple[Any, ...]: return tuple(x for value in values for x in all_literal_values(value)) +def is_namedtuple_class(type_: Type[Any]) -> bool: + try: + return issubclass(type_, tuple) and hasattr(type_, '_fields') + except TypeError: + return False + + test_type = NewType('test_type', str) diff --git a/pydantic/validators.py b/pydantic/validators.py index c47ee2f4f08..91f3073a56f 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -15,6 +15,7 @@ FrozenSet, Generator, List, + NamedTuple, Pattern, Set, Tuple, @@ -34,6 +35,7 @@ get_class, is_callable_type, is_literal_type, + is_namedtuple_class, ) from .utils import almost_equal_floats, lenient_issubclass, sequence_like @@ -523,6 +525,16 @@ def pattern_validator(v: Any) -> Pattern[str]: raise errors.PatternError() +N = TypeVar('N', bound=NamedTuple) + + +def make_namedtuple_validator(namedtuple_type: Type[N]) -> Callable[[Tuple[Any, ...]], N]: + def namedtuple_validator(v: Tuple[Any, ...]) -> N: + return namedtuple_type(*v) + + return namedtuple_validator + + class IfConfig: def __init__(self, validator: AnyCallable, *config_attr_names: str) -> None: self.validator = validator @@ -610,6 +622,10 @@ def find_validators( # noqa: C901 (ignore complexity) if type_ is IntEnum: yield int_enum_validator return + if is_namedtuple_class(type_): + yield tuple_validator + yield make_namedtuple_validator(type_) + return class_ = get_class(type_) if class_ is not None: diff --git a/tests/test_validators.py b/tests/test_validators.py index 62a68b031aa..44e54bc2df8 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -1,7 +1,7 @@ -from collections import deque +from collections import deque, namedtuple from datetime import datetime from itertools import product -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple import pytest @@ -1211,3 +1211,23 @@ def validate_foo(cls, v): with pytest.raises(RuntimeError, match='test error'): model.foo = 'raise_exception' assert model.foo == 'foo' + + +def test_namedtuple(): + Position = namedtuple('Pos', 'x y') + + class Event(NamedTuple): + a: int + b: int + c: str + + class Model(BaseModel): + pos: Position + events: List[Event] + + model = Model(pos=(1, 2), events=[[1, 2, 'qwe']]) + assert isinstance(model.pos, Position) + assert isinstance(model.events[0], Event) + assert model.pos.x == 1 + assert model.events[0].c == 'qwe' + assert repr(model) == "Model(pos=Pos(x=1, y=2), events=[Event(a=1, b=2, c='qwe')])"