From 567bf4d6962a8820f48d676a4aa4f4201a611618 Mon Sep 17 00:00:00 2001 From: PrettyWood Date: Thu, 21 Jan 2021 22:32:03 +0100 Subject: [PATCH] feat: support schema for NamedTuple --- pydantic/annotated_types.py | 2 +- pydantic/schema.py | 11 ++++++++ pydantic/validators.py | 1 + tests/test_annotated_types.py | 47 ++++++++++++++++++++++++++++++++++- 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/pydantic/annotated_types.py b/pydantic/annotated_types.py index 436f1db0a7..9049ca0dd6 100644 --- a/pydantic/annotated_types.py +++ b/pydantic/annotated_types.py @@ -56,4 +56,4 @@ def create_model_from_namedtuple(namedtuple_cls: Type['NamedTuple'], **kwargs: A field_definitions: Dict[str, Any] = { field_name: (field_type, Required) for field_name, field_type in namedtuple_annotations.items() } - return create_model(f'{namedtuple_cls.__name__}Model', **kwargs, **field_definitions) + return create_model(namedtuple_cls.__name__, **kwargs, **field_definitions) diff --git a/pydantic/schema.py b/pydantic/schema.py index b40ed64830..e72d801d09 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -65,6 +65,7 @@ get_origin, is_callable_type, is_literal_type, + is_namedtuple, literal_values, ) from .utils import ROOT_KEY, get_model, lenient_issubclass, sequence_like @@ -795,6 +796,16 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) f_schema, schema_overrides = get_field_info_schema(field) f_schema.update(get_schema_ref(enum_name, ref_prefix, ref_template, schema_overrides)) definitions[enum_name] = enum_process_schema(field_type) + elif is_namedtuple(field_type): + sub_schema, *_ = model_process_schema( + field_type.__pydantic_model__, + by_alias=by_alias, + model_name_map=model_name_map, + ref_prefix=ref_prefix, + ref_template=ref_template, + known_models=known_models, + ) + f_schema.update({'type': 'array', 'items': list(sub_schema['properties'].values())}) elif not hasattr(field_type, '__pydantic_model__'): add_field_type_to_schema(field_type, f_schema) diff --git a/pydantic/validators.py b/pydantic/validators.py index 43ba2a37e3..6e73444133 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -547,6 +547,7 @@ def make_namedtuple_validator(namedtuple_cls: Type[NamedTupleT]) -> Callable[[Tu from .annotated_types import create_model_from_namedtuple NamedTupleModel = create_model_from_namedtuple(namedtuple_cls) + namedtuple_cls.__pydantic_model__ = NamedTupleModel # type: ignore[attr-defined] def namedtuple_validator(values: Tuple[Any, ...]) -> NamedTupleT: dict_values: Dict[str, Any] = dict(zip(NamedTupleModel.__annotations__, values)) diff --git a/tests/test_annotated_types.py b/tests/test_annotated_types.py index 6e763af083..aa29909e8d 100644 --- a/tests/test_annotated_types.py +++ b/tests/test_annotated_types.py @@ -5,7 +5,7 @@ """ import sys from collections import namedtuple -from typing import List, NamedTuple +from typing import List, NamedTuple, Tuple if sys.version_info < (3, 9): try: @@ -59,6 +59,51 @@ class Model(BaseModel): ] +def test_namedtuple_schema(): + class Position1(NamedTuple): + x: int + y: int + + Position2 = namedtuple('Position2', 'x y') + + class Model(BaseModel): + pos1: Position1 + pos2: Position2 + pos3: Tuple[int, int] + + assert Model.schema() == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'pos1': { + 'title': 'Pos1', + 'type': 'array', + 'items': [ + {'title': 'X', 'type': 'integer'}, + {'title': 'Y', 'type': 'integer'}, + ], + }, + 'pos2': { + 'title': 'Pos2', + 'type': 'array', + 'items': [ + {'title': 'X'}, + {'title': 'Y'}, + ], + }, + 'pos3': { + 'title': 'Pos3', + 'type': 'array', + 'items': [ + {'type': 'integer'}, + {'type': 'integer'}, + ], + }, + }, + 'required': ['pos1', 'pos2', 'pos3'], + } + + @pytest.mark.skipif(not TypedDict, reason='typing_extensions not installed') def test_typeddict(): class TD(TypedDict):