From c3870b611e1c5c287a79271f07011a758ce09a5f Mon Sep 17 00:00:00 2001 From: maximberg <35705131+maximberg@users.noreply.github.com> Date: Sat, 13 Feb 2021 20:04:49 +0300 Subject: [PATCH] Added schema generation for Generic fields (#2262) * Added schema generation for Generic fields with tests. Fixed test_assert_raises_validation_error. * Added schema generation for Generic fields with tests. Fixed test_assert_raises_validation_error. * tested on python 3.6, 3.7, 3.8, 3.9 * tested on python 3.6, 3.7, 3.8, 3.9 * restore formatting * fix mistakes * formatting * formatting * formatting * fixed coverage * changes file * changes file * remove redundant len * Update pydantic/schema.py Co-authored-by: Maz Jaleel Co-authored-by: Maxim Berg Co-authored-by: Maz Jaleel --- changes/2262-maximberg.md | 1 + pydantic/schema.py | 15 +++++++-- pydantic/typing.py | 4 +-- tests/test_schema.py | 71 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 changes/2262-maximberg.md diff --git a/changes/2262-maximberg.md b/changes/2262-maximberg.md new file mode 100644 index 0000000000..0911c71d0a --- /dev/null +++ b/changes/2262-maximberg.md @@ -0,0 +1 @@ +Support generating schema for Generic fields. \ No newline at end of file diff --git a/pydantic/schema.py b/pydantic/schema.py index e1e14912b7..6801c5fe4a 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -11,6 +11,7 @@ Callable, Dict, FrozenSet, + Generic, Iterable, List, Optional, @@ -27,6 +28,7 @@ from .fields import ( SHAPE_FROZENSET, + SHAPE_GENERIC, SHAPE_ITERABLE, SHAPE_LIST, SHAPE_MAPPING, @@ -488,7 +490,7 @@ def field_type_schema( sub_schema = sub_schema[0] # type: ignore f_schema = {'type': 'array', 'items': sub_schema} else: - assert field.shape == SHAPE_SINGLETON, field.shape + assert field.shape in {SHAPE_SINGLETON, SHAPE_GENERIC}, field.shape f_schema, f_definitions, f_nested_models = field_singleton_schema( field, by_alias=by_alias, @@ -503,7 +505,11 @@ def field_type_schema( # check field type to avoid repeated calls to the same __modify_schema__ method if field.type_ != field.outer_type_: - modify_schema = getattr(field.outer_type_, '__modify_schema__', None) + if field.shape == SHAPE_GENERIC: + field_type = field.type_ + else: + field_type = field.outer_type_ + modify_schema = getattr(field_type, '__modify_schema__', None) if modify_schema: modify_schema(f_schema) return f_schema, definitions, nested_models @@ -845,6 +851,11 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) schema_ref = get_schema_ref(model_name, ref_prefix, ref_template, schema_overrides) return schema_ref, definitions, nested_models + # For generics with no args + args = get_args(field_type) + if args is not None and not args and Generic in field_type.__bases__: + return f_schema, definitions, nested_models + raise ValueError(f'Value not declarable with JSON Schema, field: {field}') diff --git a/pydantic/typing.py b/pydantic/typing.py index cf4f25e58f..314faef43b 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -124,7 +124,7 @@ class Annotated(metaclass=_FalseMeta): if sys.version_info < (3, 7): def get_args(t: Type[Any]) -> Tuple[Any, ...]: - """Simplest get_args compatability layer possible. + """Simplest get_args compatibility layer possible. The Python 3.6 typing module does not have `_GenericAlias` so this won't work for everything. In particular this will not @@ -140,7 +140,7 @@ def get_args(t: Type[Any]) -> Tuple[Any, ...]: from typing import _GenericAlias def get_args(t: Type[Any]) -> Tuple[Any, ...]: - """Compatability version of get_args for python 3.7. + """Compatibility version of get_args for python 3.7. Mostly compatible with the python 3.8 `typing` module version and able to handle almost all use cases. diff --git a/tests/test_schema.py b/tests/test_schema.py index 661354e5b1..1235af4f40 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -7,7 +7,21 @@ from enum import Enum, IntEnum from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network from pathlib import Path -from typing import Any, Callable, Dict, FrozenSet, Iterable, List, NewType, Optional, Set, Tuple, Union +from typing import ( + Any, + Callable, + Dict, + FrozenSet, + Generic, + Iterable, + List, + NewType, + Optional, + Set, + Tuple, + TypeVar, + Union, +) from uuid import UUID import pytest @@ -2195,3 +2209,58 @@ class Model(BaseModel): f'{module_2.__name__}__MyEnum', f'{module_2.__name__}__MyModel', } + + +@pytest.mark.skipif( + sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' +) +def test_schema_for_generic_field(): + T = TypeVar('T') + + class GenModel(Generic[T]): + def __init__(self, data: Any): + self.data = data + + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, v: Any): + return v + + class Model(BaseModel): + data: GenModel[str] + data1: GenModel + + assert Model.schema() == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'data': {'title': 'Data', 'type': 'string'}, + 'data1': { + 'title': 'Data1', + }, + }, + 'required': ['data', 'data1'], + } + + class GenModelModified(GenModel, Generic[T]): + @classmethod + def __modify_schema__(cls, field_schema): + field_schema.pop('type', None) + field_schema.update(anyOf=[{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]) + + class ModelModified(BaseModel): + data: GenModelModified[str] + data1: GenModelModified + + assert ModelModified.schema() == { + 'title': 'ModelModified', + 'type': 'object', + 'properties': { + 'data': {'title': 'Data', 'anyOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}, + 'data1': {'title': 'Data1', 'anyOf': [{'type': 'string'}, {'type': 'array', 'items': {'type': 'string'}}]}, + }, + 'required': ['data', 'data1'], + }