Skip to content

Commit

Permalink
Added schema generation for Generic fields (#2262)
Browse files Browse the repository at this point in the history
* 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 <mazjaleel@gmail.com>

Co-authored-by: Maxim Berg <berg@petrostyle.com>
Co-authored-by: Maz Jaleel <mazjaleel@gmail.com>
  • Loading branch information
3 people committed Feb 13, 2021
1 parent b742c6f commit c3870b6
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 5 deletions.
1 change: 1 addition & 0 deletions changes/2262-maximberg.md
@@ -0,0 +1 @@
Support generating schema for Generic fields.
15 changes: 13 additions & 2 deletions pydantic/schema.py
Expand Up @@ -11,6 +11,7 @@
Callable,
Dict,
FrozenSet,
Generic,
Iterable,
List,
Optional,
Expand All @@ -27,6 +28,7 @@

from .fields import (
SHAPE_FROZENSET,
SHAPE_GENERIC,
SHAPE_ITERABLE,
SHAPE_LIST,
SHAPE_MAPPING,
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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}')


Expand Down
4 changes: 2 additions & 2 deletions pydantic/typing.py
Expand Up @@ -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
Expand All @@ -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.
Expand Down
71 changes: 70 additions & 1 deletion tests/test_schema.py
Expand Up @@ -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
Expand Down Expand Up @@ -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'],
}

0 comments on commit c3870b6

Please sign in to comment.