Skip to content

Commit

Permalink
feat: add support for namedtuple type
Browse files Browse the repository at this point in the history
  • Loading branch information
PrettyWood committed Dec 22, 2020
1 parent 3496a47 commit 16d53eb
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 2 deletions.
1 change: 1 addition & 0 deletions changes/1324-PrettyWood.md
@@ -0,0 +1 @@
add basic support for `namedtuple` type
9 changes: 9 additions & 0 deletions docs/usage/types.md
Expand Up @@ -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

Expand Down
8 changes: 8 additions & 0 deletions pydantic/typing.py
Expand Up @@ -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',
Expand Down Expand Up @@ -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)


Expand Down
16 changes: 16 additions & 0 deletions pydantic/validators.py
Expand Up @@ -15,6 +15,7 @@
FrozenSet,
Generator,
List,
NamedTuple,
Pattern,
Set,
Tuple,
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
24 changes: 22 additions & 2 deletions 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

Expand Down Expand Up @@ -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')])"

0 comments on commit 16d53eb

Please sign in to comment.