Repository: samuelcolvin/pydantic
Pull request #2306 (2021-02-01 00:29:08)

Fix `python_requires` metadata to require >=3.6.1
At least [this `typing.Deque` import](https://github.com/samuelcolvin/pydantic/blob/bd9c5723c676395363689549268738153e45a7e5/pydantic/validators.py#L13) requires Python version to be >=3.6.1.

This PR fixes `python_requires` metadata.

codecov[bot]: # [Codecov](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306?src=pr&el=h1) Report
> Merging [#2306](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306?src=pr&el=desc) (aec8fce) into [master](https://codecov.io/gh/samuelcolvin/pydantic/commit/bd9c5723c676395363689549268738153e45a7e5?el=desc) (bd9c572) will **decrease** coverage by `4.19%`.
> The diff coverage is `n/a`.


```diff
@@             Coverage Diff             @@
##            master    #2306      +/-   ##
===========================================
- Coverage   100.00%   95.80%   -4.20%     
===========================================
  Files           21       21              
  Lines         4199     4199              
  Branches       854      854              
===========================================
- Hits          4199     4023     -176     
- Misses           0      154     +154     
- Partials         0       22      +22     
```


| [Impacted Files](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pydantic/generics.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306/diff?src=pr&el=tree#diff-cHlkYW50aWMvZ2VuZXJpY3MucHk=) | `27.02% <0.00%> (-72.98%)` | :arrow_down: |
| [pydantic/typing.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306/diff?src=pr&el=tree#diff-cHlkYW50aWMvdHlwaW5nLnB5) | `68.79% <0.00%> (-31.21%)` | :arrow_down: |
| [pydantic/decorator.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306/diff?src=pr&el=tree#diff-cHlkYW50aWMvZGVjb3JhdG9yLnB5) | `87.65% <0.00%> (-12.35%)` | :arrow_down: |
| [pydantic/version.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306/diff?src=pr&el=tree#diff-cHlkYW50aWMvdmVyc2lvbi5weQ==) | `88.23% <0.00%> (-11.77%)` | :arrow_down: |
| [pydantic/env\_settings.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306/diff?src=pr&el=tree#diff-cHlkYW50aWMvZW52X3NldHRpbmdzLnB5) | `93.81% <0.00%> (-6.19%)` | :arrow_down: |
| [pydantic/fields.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306/diff?src=pr&el=tree#diff-cHlkYW50aWMvZmllbGRzLnB5) | `96.80% <0.00%> (-3.20%)` | :arrow_down: |
| [pydantic/error\_wrappers.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306/diff?src=pr&el=tree#diff-cHlkYW50aWMvZXJyb3Jfd3JhcHBlcnMucHk=) | `98.86% <0.00%> (-1.14%)` | :arrow_down: |
| [pydantic/types.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306/diff?src=pr&el=tree#diff-cHlkYW50aWMvdHlwZXMucHk=) | `99.18% <0.00%> (-0.82%)` | :arrow_down: |
| [pydantic/networks.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306/diff?src=pr&el=tree#diff-cHlkYW50aWMvbmV0d29ya3MucHk=) | `99.26% <0.00%> (-0.74%)` | :arrow_down: |
| [pydantic/utils.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306/diff?src=pr&el=tree#diff-cHlkYW50aWMvdXRpbHMucHk=) | `99.30% <0.00%> (-0.70%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306?src=pr&el=footer). Last update [bd9c572...aec8fce](https://codecov.io/gh/samuelcolvin/pydantic/pull/2306?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

samuelcolvin: seems reasonable to me. Please can you:
* add a change description
* add a link in this PR to explain why `Deque` changed in a patch version, something strange must have happened
hukkinj1: I guess the best explanation for this is given by Guido van Rossum https://bugs.python.org/issue29011#msg283660
It's still not quite clear to me personally why the addition couldn't wait until 3.7.0 release though.
hukkinj1: > - add a change description

Is there a predefined process for this? Couldn't find anything [in the docs](https://pydantic-docs.helpmanual.io/contributing/)
hukkinj1: Ok I found the changelog process [here](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md). Maybe the contribution docs should link there?

Patch:
diff --git a/changes/2262-maximberg.md b/changes/2262-maximberg.md
new file mode 100644
index 000000000..0911c71d0
--- /dev/null
+++ b/changes/2262-maximberg.md
@@ -0,0 +1,1 @@
+Support generating schema for Generic fields. 
\ No newline at end of file
diff --git a/changes/2306-hukkinj1.md b/changes/2306-hukkinj1.md
new file mode 100644
index 000000000..f2300718f
--- /dev/null
+++ b/changes/2306-hukkinj1.md
@@ -0,0 +1,1 @@
+fix: `python_requires` metadata to require >=3.6.1
\ No newline at end of file
diff --git a/pydantic/schema.py b/pydantic/schema.py
index e1e14912b..6801c5fe4 100644
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -11,6 +11,7 @@ from typing import (
     Callable,
     Dict,
     FrozenSet,
+    Generic,
     Iterable,
     List,
     Optional,
@@ -27,6 +28,7 @@ from uuid import UUID
 
 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 cf4f25e58..314faef43 100644
--- a/pydantic/typing.py
+++ b/pydantic/typing.py
@@ -124,7 +124,7 @@ if sys.version_info < (3, 8):  # noqa: C901
     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 @@ if sys.version_info < (3, 8):  # noqa: C901
         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/setup.py b/setup.py
index e6b72c446..96c735324 100644
--- a/setup.py
+++ b/setup.py
@@ -124,7 +124,7 @@ setup(
     license='MIT',
     packages=['pydantic'],
     package_data={'pydantic': ['py.typed']},
-    python_requires='>=3.6',
+    python_requires='>=3.6.1',
     zip_safe=False,  # https://mypy.readthedocs.io/en/latest/installed_packages.html
     install_requires=[
         'dataclasses>=0.6;python_version<"3.7"'


Test patch:
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 661354e5b..1235af4f4 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -7,7 +7,21 @@ from decimal import Decimal
 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 MyModel(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'],
+    }