Repository: samuelcolvin/pydantic
Pull request #3470 (2021-12-01 12:11:06)

docs: Print name classes for `__concrete_name__` example
<!-- Thank you for your contribution! -->
<!-- Unless your change is trivial, please create an issue to discuss the change before creating a PR -->
<!-- See https://pydantic-docs.helpmanual.io/contributing/ for help on Contributing -->

## Change Summary

<!-- Please give a short summary of the changes. -->

## Related issue number

<!-- Are there any issues opened that will be resolved by merging this change? -->

## Checklist

* [x] Unit tests for the changes exist
* [ ] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [ ] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)
* [ ] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


q0w: please review

Patch:
diff --git a/changes/2007-diabolo-dan.md b/changes/2007-diabolo-dan.md
new file mode 100644
index 000000000..d79c056b0
--- /dev/null
+++ b/changes/2007-diabolo-dan.md
@@ -0,0 +1,1 @@
+Add parameterised subclasses to `__bases__` when constructing new parameterised classes, so that `A <: B => A[int] <: B[int]`.
diff --git a/changes/2497-PrettyWood.md b/changes/2497-PrettyWood.md
new file mode 100644
index 000000000..0e28a966a
--- /dev/null
+++ b/changes/2497-PrettyWood.md
@@ -0,0 +1,1 @@
+Set `minItems` and `maxItems` in generated JSON schema for fixed-length tuples
diff --git a/changes/2588-uriyyo.md b/changes/2588-uriyyo.md
new file mode 100644
index 000000000..938d28267
--- /dev/null
+++ b/changes/2588-uriyyo.md
@@ -0,0 +1,1 @@
+Try to evaluate forward refs automatically at model creation.
\ No newline at end of file
diff --git a/docs/build/schema_mapping.py b/docs/build/schema_mapping.py
index 4f204b358..c9b6c018f 100755
--- a/docs/build/schema_mapping.py
+++ b/docs/build/schema_mapping.py
@@ -81,10 +81,17 @@ table = [
         'JSON Schema Validation',
         'And equivalently for any other sub type, e.g. `List[int]`.'
     ],
+    [
+        'Tuple[str, ...]',
+        'array',
+        {'items': {'type': 'string'}},
+        'JSON Schema Validation',
+        'And equivalently for any other sub type, e.g. `Tuple[int, ...]`.'
+    ],
     [
         'Tuple[str, int]',
         'array',
-        {'items': [{'type': 'string'}, {'type': 'integer'}]},
+        {'items': [{'type': 'string'}, {'type': 'integer'}], 'minItems': 2, 'maxItems': 2},
         'JSON Schema Validation',
         (
             'And equivalently for any other set of subtypes. Note: If using schemas for OpenAPI, '
diff --git a/docs/examples/models_generics_naming.py b/docs/examples/models_generics_naming.py
index e55742b40..641c90623 100644
--- a/docs/examples/models_generics_naming.py
+++ b/docs/examples/models_generics_naming.py
@@ -13,5 +13,5 @@ class Response(GenericModel, Generic[DataT]):
         return f'{params[0].__name__.title()}Response'
 
 
-print(Response[int](data=1))
-print(Response[str](data='a'))
+print(repr(Response[int](data=1)))
+print(repr(Response[str](data='a')))
diff --git a/docs/examples/postponed_annotations_self_referencing_annotations.py b/docs/examples/postponed_annotations_self_referencing_annotations.py
index 902bd62e3..aa8966c1c 100644
--- a/docs/examples/postponed_annotations_self_referencing_annotations.py
+++ b/docs/examples/postponed_annotations_self_referencing_annotations.py
@@ -8,7 +8,5 @@ class Foo(BaseModel):
     sibling: Foo = None
 
 
-Foo.update_forward_refs()
-
 print(Foo())
 print(Foo(sibling={'a': '321'}))
diff --git a/docs/examples/postponed_annotations_self_referencing_string.py b/docs/examples/postponed_annotations_self_referencing_string.py
index 2c918a2ca..3290051bb 100644
--- a/docs/examples/postponed_annotations_self_referencing_string.py
+++ b/docs/examples/postponed_annotations_self_referencing_string.py
@@ -7,7 +7,5 @@ class Foo(BaseModel):
     sibling: 'Foo' = None
 
 
-Foo.update_forward_refs()
-
 print(Foo())
 print(Foo(sibling={'a': '321'}))
diff --git a/docs/requirements.txt b/docs/requirements.txt
index b5e080824..dee12cff6 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,12 +1,12 @@
 ansi2html==1.6.0
 flake8==4.0.1
 flake8-quotes==3.3.1
-hypothesis==6.24.0
+hypothesis==6.30.1
 markdown-include==0.6.0
 mdx-truly-sane-lists==1.2
 mkdocs==1.2.3
 mkdocs-exclude==1.0.2
-mkdocs-material==7.3.4
+mkdocs-material==8.0.5
 sqlalchemy
 orjson
 ujson
diff --git a/docs/usage/postponed_annotations.md b/docs/usage/postponed_annotations.md
index cc445880b..8e975e262 100644
--- a/docs/usage/postponed_annotations.md
+++ b/docs/usage/postponed_annotations.md
@@ -45,9 +45,8 @@ Resolving this is beyond the call for *pydantic*: either remove the future impor
 
 ## Self-referencing Models
 
-Data structures with self-referencing models are also supported, provided the function
-`update_forward_refs()` is called once the model is created (you will be reminded
-with a friendly error message if you forget).
+Data structures with self-referencing models are also supported. Self-referencing fields will be automatically
+resolved after model creation.
 
 Within the model, you can refer to the not-yet-constructed model using a string:
 
diff --git a/pydantic/config.py b/pydantic/config.py
index 5cc7e3a74..317b2f80c 100644
--- a/pydantic/config.py
+++ b/pydantic/config.py
@@ -24,7 +24,6 @@ if TYPE_CHECKING:
         def __call__(self, schema: Dict[str, Any], model_class: Type[BaseModel]) -> None:
             pass
 
-
 else:
     SchemaExtraCallable = Callable[..., None]
 
diff --git a/pydantic/generics.py b/pydantic/generics.py
index 17d782feb..1fee636c3 100644
--- a/pydantic/generics.py
+++ b/pydantic/generics.py
@@ -30,6 +30,15 @@ _generic_types_cache: Dict[Tuple[Type[Any], Union[Any, Tuple[Any, ...]]], Type[B
 GenericModelT = TypeVar('GenericModelT', bound='GenericModel')
 TypeVarType = Any  # since mypy doesn't allow the use of TypeVar as a type
 
+Parametrization = Mapping[TypeVarType, Type[Any]]
+
+# _assigned_parameters is a Mapping from parametrized version of generic models to assigned types of parametrizations
+# as captured during construction of the class (not instances).
+# E.g., for generic model `Model[A, B]`, when parametrized model `Model[int, str]` is created,
+# `Model[int, str]`: {A: int, B: str}` will be stored in `_assigned_parameters`.
+# (This information is only otherwise available after creation from the class name string).
+_assigned_parameters: Dict[Type[Any], Parametrization] = {}
+
 
 class GenericModel(BaseModel):
     __slots__ = ()
@@ -86,13 +95,15 @@ class GenericModel(BaseModel):
             create_model(
                 model_name,
                 __module__=model_module or cls.__module__,
-                __base__=cls,
+                __base__=(cls,) + tuple(cls.__parameterized_bases__(typevars_map)),
                 __config__=None,
                 __validators__=validators,
                 **fields,
             ),
         )
 
+        _assigned_parameters[created_model] = typevars_map
+
         if called_globally:  # create global reference and therefore allow pickling
             object_by_reference = None
             reference_name = model_name
@@ -142,6 +153,70 @@ class GenericModel(BaseModel):
         params_component = ', '.join(param_names)
         return f'{cls.__name__}[{params_component}]'
 
+    @classmethod
+    def __parameterized_bases__(cls, typevars_map: Parametrization) -> Iterator[Type[Any]]:
+        """
+        Returns unbound bases of cls parameterised to given type variables
+
+        :param typevars_map: Dictionary of type applications for binding subclasses.
+            Given a generic class `Model` with 2 type variables [S, T]
+            and a concrete model `Model[str, int]`,
+            the value `{S: str, T: int}` would be passed to `typevars_map`.
+        :return: an iterator of generic sub classes, parameterised by `typevars_map`
+            and other assigned parameters of `cls`
+
+        e.g.:
+        ```
+        class A(GenericModel, Generic[T]):
+            ...
+
+        class B(A[V], Generic[V]):
+            ...
+
+        assert A[int] in B.__parameterized_bases__({V: int})
+        ```
+        """
+
+        def build_base_model(
+            base_model: Type[GenericModel], mapped_types: Parametrization
+        ) -> Iterator[Type[GenericModel]]:
+            base_parameters = tuple([mapped_types[param] for param in base_model.__parameters__])
+            parameterized_base = base_model.__class_getitem__(base_parameters)
+            if parameterized_base is base_model or parameterized_base is cls:
+                # Avoid duplication in MRO
+                return
+            yield parameterized_base
+
+        for base_model in cls.__bases__:
+            if not issubclass(base_model, GenericModel):
+                # not a class that can be meaningfully parameterized
+                continue
+            elif not getattr(base_model, '__parameters__', None):
+                # base_model is "GenericModel"  (and has no __parameters__)
+                # or
+                # base_model is already concrete, and will be included transitively via cls.
+                continue
+            elif cls in _assigned_parameters:
+                if base_model in _assigned_parameters:
+                    # cls is partially parameterised but not from base_model
+                    # e.g. cls = B[S], base_model = A[S]
+                    # B[S][int] should subclass A[int],  (and will be transitively via B[int])
+                    # but it's not viable to consistently subclass types with arbitrary construction
+                    # So don't attempt to include A[S][int]
+                    continue
+                else:  # base_model not in _assigned_parameters:
+                    # cls is partially parameterized, base_model is original generic
+                    # e.g.  cls = B[str, T], base_model = B[S, T]
+                    # Need to determine the mapping for the base_model parameters
+                    mapped_types: Parametrization = {
+                        key: typevars_map.get(value, value) for key, value in _assigned_parameters[cls].items()
+                    }
+                    yield from build_base_model(base_model, mapped_types)
+            else:
+                # cls is base generic, so base_class has a distinct base
+                # can construct the Parameterised base model using typevars_map directly
+                yield from build_base_model(base_model, typevars_map)
+
 
 def replace_types(type_: Any, type_map: Mapping[Any, Any]) -> Any:
     """Return type with all occurrences of `type_map` keys recursively replaced with their values.
diff --git a/pydantic/main.py b/pydantic/main.py
index a25e96efe..cf900e399 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -1,4 +1,3 @@
-import sys
 import warnings
 from abc import ABCMeta
 from copy import deepcopy
@@ -42,7 +41,7 @@ from .typing import (
     is_namedtuple,
     is_union_origin,
     resolve_annotations,
-    update_field_forward_refs,
+    update_model_forward_refs,
 )
 from .utils import (
     ROOT_KEY,
@@ -289,6 +288,8 @@ class ModelMetaclass(ABCMeta):
         cls = super().__new__(mcs, name, bases, new_namespace, **kwargs)
         # set __signature__ attr only for model class, but not for its instances
         cls.__signature__ = ClassAttribute('__signature__', generate_model_signature(cls.__init__, fields, config))
+        cls.__try_update_forward_refs__()
+
         return cls
 
 
@@ -746,15 +747,20 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
         else:
             return v
 
+    @classmethod
+    def __try_update_forward_refs__(cls) -> None:
+        """
+        Same as update_forward_refs but will not raise exception
+        when forward references are not defined.
+        """
+        update_model_forward_refs(cls, cls.__fields__.values(), {}, (NameError,))
+
     @classmethod
     def update_forward_refs(cls, **localns: Any) -> None:
         """
         Try to update ForwardRefs on fields based on this Model, globalns and localns.
         """
-        globalns = sys.modules[cls.__module__].__dict__.copy()
-        globalns.setdefault(cls.__name__, cls)
-        for f in cls.__fields__.values():
-            update_field_forward_refs(f, globalns=globalns, localns=localns)
+        update_model_forward_refs(cls, cls.__fields__.values(), localns)
 
     def __iter__(self) -> 'TupleGenerator':
         """
@@ -877,7 +883,7 @@ def create_model(
     __model_name: str,
     *,
     __config__: Optional[Type[BaseConfig]] = None,
-    __base__: Type['Model'],
+    __base__: Union[Type['Model'], Tuple[Type['Model'], ...]],
     __module__: str = __name__,
     __validators__: Dict[str, classmethod] = None,
     **field_definitions: Any,
@@ -889,7 +895,7 @@ def create_model(
     __model_name: str,
     *,
     __config__: Optional[Type[BaseConfig]] = None,
-    __base__: Optional[Type['Model']] = None,
+    __base__: Union[None, Type['Model'], Tuple[Type['Model'], ...]] = None,
     __module__: str = __name__,
     __validators__: Dict[str, classmethod] = None,
     **field_definitions: Any,
@@ -910,8 +916,10 @@ def create_model(
     if __base__ is not None:
         if __config__ is not None:
             raise ConfigError('to avoid confusion __config__ and __base__ cannot be used together')
+        if not isinstance(__base__, tuple):
+            __base__ = (__base__,)
     else:
-        __base__ = cast(Type['Model'], BaseModel)
+        __base__ = (cast(Type['Model'], BaseModel),)
 
     fields = {}
     annotations = {}
@@ -942,7 +950,7 @@ def create_model(
     if __config__:
         namespace['Config'] = inherit_config(__config__, BaseConfig)
 
-    return type(__model_name, (__base__,), namespace)
+    return type(__model_name, __base__, namespace)
 
 
 _missing = object()
diff --git a/pydantic/networks.py b/pydantic/networks.py
index 557edc835..17a0529fe 100644
--- a/pydantic/networks.py
+++ b/pydantic/networks.py
@@ -51,7 +51,6 @@ if TYPE_CHECKING:
         query: Optional[str]
         fragment: Optional[str]
 
-
 else:
     email_validator = None
 
diff --git a/pydantic/schema.py b/pydantic/schema.py
index 581b248d3..b5d357a27 100644
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -499,15 +499,19 @@ def field_type_schema(
             definitions.update(sf_definitions)
             nested_models.update(sf_nested_models)
             sub_schema.append(sf_schema)
-        if len(sub_schema) == 1:
-            if field.shape == SHAPE_GENERIC:
-                f_schema = sub_schema[0]
-            else:
-                f_schema = {'type': 'array', 'items': sub_schema[0]}
-        else:
-            f_schema = {'type': 'array', 'items': sub_schema}
+
+        sub_fields_len = len(sub_fields)
         if field.shape == SHAPE_GENERIC:
-            f_schema = {'allOf': [f_schema]}
+            all_of_schemas = sub_schema[0] if sub_fields_len == 1 else {'type': 'array', 'items': sub_schema}
+            f_schema = {'allOf': [all_of_schemas]}
+        else:
+            f_schema = {
+                'type': 'array',
+                'minItems': sub_fields_len,
+                'maxItems': sub_fields_len,
+            }
+            if sub_fields_len >= 1:
+                f_schema['items'] = sub_schema
     else:
         assert field.shape in {SHAPE_SINGLETON, SHAPE_GENERIC}, field.shape
         f_schema, f_definitions, f_nested_models = field_singleton_schema(
@@ -844,7 +848,15 @@ def field_singleton_schema(  # noqa: C901 (ignore complexity)
             ref_template=ref_template,
             known_models=known_models,
         )
-        f_schema.update({'type': 'array', 'items': list(sub_schema['properties'].values())})
+        items_schemas = list(sub_schema['properties'].values())
+        f_schema.update(
+            {
+                'type': 'array',
+                'items': items_schemas,
+                'minItems': len(items_schemas),
+                'maxItems': len(items_schemas),
+            }
+        )
     elif not hasattr(field_type, '__pydantic_model__'):
         add_field_type_to_schema(field_type, f_schema)
 
diff --git a/pydantic/typing.py b/pydantic/typing.py
index 5797432ec..788496aa0 100644
--- a/pydantic/typing.py
+++ b/pydantic/typing.py
@@ -7,6 +7,7 @@ from typing import (  # type: ignore
     ClassVar,
     Dict,
     Generator,
+    Iterable,
     List,
     Mapping,
     NewType,
@@ -56,13 +57,11 @@ if sys.version_info < (3, 7):
     def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
         return type_._eval_type(globalns, localns)
 
-
 elif sys.version_info < (3, 9):
 
     def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
         return type_._evaluate(globalns, localns)
 
-
 else:
 
     def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
@@ -108,7 +107,6 @@ if sys.version_info < (3, 8):
             return cast(Type[Any], Annotated)  # mypy complains about _SpecialForm in py3.6
         return getattr(t, '__origin__', None)
 
-
 else:
     from typing import get_origin as _typing_get_origin
 
@@ -139,7 +137,6 @@ if sys.version_info < (3, 7):  # noqa: C901 (ignore complexity)
             return t.__args__ + t.__metadata__
         return getattr(t, '__args__', ())
 
-
 elif sys.version_info < (3, 8):  # noqa: C901
     from typing import _GenericAlias
 
@@ -158,7 +155,6 @@ elif sys.version_info < (3, 8):  # noqa: C901
             return res
         return getattr(t, '__args__', ())
 
-
 else:
     from typing import get_args as _typing_get_args
 
@@ -251,6 +247,7 @@ __all__ = (
     'new_type_supertype',
     'is_classvar',
     'update_field_forward_refs',
+    'update_model_forward_refs',
     'TupleGenerator',
     'DictStrAny',
     'DictAny',
@@ -288,7 +285,6 @@ if sys.version_info < (3, 8):  # noqa: C901 (ignore complexity)
     def is_none_type(type_: Any) -> bool:
         return type_ in NONE_TYPES
 
-
 else:
 
     def is_none_type(type_: Any) -> bool:
@@ -359,7 +355,6 @@ if sys.version_info >= (3, 7):
     def literal_values(type_: Type[Any]) -> Tuple[Any, ...]:
         return get_args(type_)
 
-
 else:
 
     def is_literal_type(type_: Type[Any]) -> bool:
@@ -441,6 +436,29 @@ def update_field_forward_refs(field: 'ModelField', globalns: Any, localns: Any)
             update_field_forward_refs(sub_f, globalns=globalns, localns=localns)
 
 
+def update_model_forward_refs(
+    model: Type[Any],
+    fields: Iterable['ModelField'],
+    localns: 'DictStrAny',
+    exc_to_suppress: Tuple[Type[BaseException], ...] = (),
+) -> None:
+    """
+    Try to update model fields ForwardRefs based on model and localns.
+    """
+    if model.__module__ in sys.modules:
+        globalns = sys.modules[model.__module__].__dict__.copy()
+    else:
+        globalns = {}
+
+    globalns.setdefault(model.__name__, model)
+
+    for f in fields:
+        try:
+            update_field_forward_refs(f, globalns=globalns, localns=localns)
+        except exc_to_suppress:
+            pass
+
+
 def get_class(type_: Type[Any]) -> Union[None, bool, Type[Any]]:
     """
     Tries to get the class of a Type[T] annotation. Returns True if Type is used
diff --git a/requirements.txt b/requirements.txt
index 420636624..5a2b15ce9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,8 +1,8 @@
 # requirements for compilation and from setup.py so dependabot prompts us to test with latest version of these packages
 
-Cython==0.29.24;sys_platform!='win32'
+Cython==0.29.25;sys_platform!='win32'
 devtools==0.8.0
 email-validator==1.1.3
 dataclasses==0.6; python_version < '3.7'
-typing-extensions==3.10.0.2
-python-dotenv==0.19.1
+typing-extensions==4.0.1
+python-dotenv==0.19.2


Test patch:
diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt
index c4c98e2ea..a6fece99c 100644
--- a/tests/requirements-linting.txt
+++ b/tests/requirements-linting.txt
@@ -1,10 +1,10 @@
-black==21.9b0
+black==21.12b0
 flake8==4.0.1
 flake8-quotes==3.3.1
-hypothesis==6.24.0
-isort==5.9.3
+hypothesis==6.30.1
+isort==5.10.1
 mypy==0.910
 types-toml==0.10.1
 pycodestyle==2.8.0
 pyflakes==2.4.0
-twine==3.4.2
\ No newline at end of file
+twine==3.7.0
\ No newline at end of file
diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt
index 23e654ad8..cb76137e7 100644
--- a/tests/requirements-testing.txt
+++ b/tests/requirements-testing.txt
@@ -1,5 +1,5 @@
-coverage==6.0.2
-hypothesis==6.24.0
+coverage==6.2
+hypothesis==6.30.1
 # pin importlib-metadata as upper versions need typing-extensions to work if on python < 3.8
 importlib-metadata==3.1.0;python_version<"3.8"
 mypy==0.910
diff --git a/tests/test_annotated_types.py b/tests/test_annotated_types.py
index 4b4beab07..efff5348c 100644
--- a/tests/test_annotated_types.py
+++ b/tests/test_annotated_types.py
@@ -79,6 +79,8 @@ def test_namedtuple_schema():
                     {'title': 'X', 'type': 'integer'},
                     {'title': 'Y', 'type': 'integer'},
                 ],
+                'minItems': 2,
+                'maxItems': 2,
             },
             'pos2': {
                 'title': 'Pos2',
@@ -87,6 +89,8 @@ def test_namedtuple_schema():
                     {'title': 'X'},
                     {'title': 'Y'},
                 ],
+                'minItems': 2,
+                'maxItems': 2,
             },
             'pos3': {
                 'title': 'Pos3',
@@ -95,6 +99,8 @@ def test_namedtuple_schema():
                     {'type': 'integer'},
                     {'type': 'integer'},
                 ],
+                'minItems': 2,
+                'maxItems': 2,
             },
         },
         'required': ['pos1', 'pos2', 'pos3'],
diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py
index 692a33d2f..12a337f82 100644
--- a/tests/test_forward_ref.py
+++ b/tests/test_forward_ref.py
@@ -41,6 +41,57 @@ class Model(BaseModel):
     assert module.Model().dict() == {'a': None}
 
 
+@skip_pre_37
+def test_postponed_annotations_auto_update_forward_refs(create_module):
+    module = create_module(
+        # language=Python
+        """
+from __future__ import annotations
+from pydantic import BaseModel
+
+class Model(BaseModel):
+    a: Model
+"""
+    )
+
+    assert module.Model.__fields__['a'].type_ is module.Model
+
+
+def test_forward_ref_auto_update_no_model(create_module):
+    module = create_module(
+        # language=Python
+        """
+from pydantic import BaseModel
+
+class Foo(BaseModel):
+    a: 'Bar'
+
+class Bar(BaseModel):
+    b: 'Foo'
+"""
+    )
+
+    from pydantic.typing import ForwardRef
+
+    assert module.Foo.__fields__['a'].type_ == ForwardRef('Bar')
+    assert module.Bar.__fields__['b'].type_ is module.Foo
+
+
+def test_forward_ref_one_of_fields_not_defined(create_module):
+    @create_module
+    def module():
+        from pydantic import BaseModel
+
+        class Foo(BaseModel):
+            foo: 'Foo'
+            bar: 'Bar'  # noqa: F821
+
+    from pydantic.typing import ForwardRef
+
+    assert module.Foo.__fields__['bar'].type_ == ForwardRef('Bar')
+    assert module.Foo.__fields__['foo'].type_ is module.Foo
+
+
 def test_basic_forward_ref(create_module):
     @create_module
     def module():
@@ -509,14 +560,6 @@ def test_nested_forward_ref():
     class NestedTuple(BaseModel):
         x: Tuple[int, Optional['NestedTuple']]  # noqa: F821
 
-    with pytest.raises(ConfigError) as exc_info:
-        NestedTuple.parse_obj({'x': ('1', {'x': ('2', {'x': ('3', None)})})})
-    assert str(exc_info.value) == (
-        'field "x_1" not yet prepared so type is still a ForwardRef, '
-        'you might need to call NestedTuple.update_forward_refs().'
-    )
-
-    NestedTuple.update_forward_refs()
     obj = NestedTuple.parse_obj({'x': ('1', {'x': ('2', {'x': ('3', None)})})})
     assert obj.dict() == {'x': (1, {'x': (2, {'x': (3, None)})})}
 
diff --git a/tests/test_generics.py b/tests/test_generics.py
index 7e0cd731c..fb071b0cf 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -1160,6 +1160,92 @@ def test_generic_annotated():
     SomeGenericModel[str](the_alias='qwe')
 
 
+@skip_36
+def test_generic_subclass():
+    T = TypeVar('T')
+
+    class A(GenericModel, Generic[T]):
+        ...
+
+    class B(A[T], Generic[T]):
+        ...
+
+    assert B[int].__name__ == 'B[int]'
+    assert issubclass(B[int], B)
+    assert issubclass(B[int], A[int])
+    assert not issubclass(B[int], A[str])
+
+
+@skip_36
+def test_generic_subclass_with_partial_application():
+    T = TypeVar('T')
+    S = TypeVar('S')
+
+    class A(GenericModel, Generic[T]):
+        ...
+
+    class B(A[S], Generic[T, S]):
+        ...
+
+    PartiallyAppliedB = B[str, T]
+    assert issubclass(PartiallyAppliedB[int], A[int])
+    assert not issubclass(PartiallyAppliedB[int], A[str])
+    assert not issubclass(PartiallyAppliedB[str], A[int])
+
+
+@skip_36
+def test_multilevel_generic_binding():
+    T = TypeVar('T')
+    S = TypeVar('S')
+
+    class A(GenericModel, Generic[T, S]):
+        ...
+
+    class B(A[str, T], Generic[T]):
+        ...
+
+    assert B[int].__name__ == 'B[int]'
+    assert issubclass(B[int], A[str, int])
+    assert not issubclass(B[str], A[str, int])
+
+
+@skip_36
+def test_generic_subclass_with_extra_type():
+    T = TypeVar('T')
+    S = TypeVar('S')
+
+    class A(GenericModel, Generic[T]):
+        ...
+
+    class B(A[S], Generic[T, S]):
+        ...
+
+    assert B[int, str].__name__ == 'B[int, str]', B[int, str].__name__
+    assert issubclass(B[str, int], B)
+    assert issubclass(B[str, int], A[int])
+    assert not issubclass(B[int, str], A[int])
+
+
+@skip_36
+def test_multi_inheritance_generic_binding():
+    T = TypeVar('T')
+
+    class A(GenericModel, Generic[T]):
+        ...
+
+    class B(A[int], Generic[T]):
+        ...
+
+    class C(B[str], Generic[T]):
+        ...
+
+    assert C[float].__name__ == 'C[float]'
+    assert issubclass(C[float], B[str])
+    assert not issubclass(C[float], B[int])
+    assert issubclass(C[float], A[int])
+    assert not issubclass(C[float], A[str])
+
+
 @skip_36
 def test_parse_generic_json():
     T = TypeVar('T')
diff --git a/tests/test_schema.py b/tests/test_schema.py
index cd4e2e4f8..2b8f831f1 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -535,34 +535,36 @@ def test_const_false():
 
 
 @pytest.mark.parametrize(
-    'field_type,expected_schema',
+    'field_type,extra_props',
     [
-        (tuple, {}),
+        (tuple, {'items': {}}),
         (
             Tuple[str, int, Union[str, int, float], float],
-            [
-                {'type': 'string'},
-                {'type': 'integer'},
-                {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}]},
-                {'type': 'number'},
-            ],
+            {
+                'items': [
+                    {'type': 'string'},
+                    {'type': 'integer'},
+                    {'anyOf': [{'type': 'string'}, {'type': 'integer'}, {'type': 'number'}]},
+                    {'type': 'number'},
+                ],
+                'minItems': 4,
+                'maxItems': 4,
+            },
         ),
-        (Tuple[str], {'type': 'string'}),
+        (Tuple[str], {'items': [{'type': 'string'}], 'minItems': 1, 'maxItems': 1}),
+        (Tuple[()], {'maxItems': 0, 'minItems': 0}),
     ],
 )
-def test_tuple(field_type, expected_schema):
+def test_tuple(field_type, extra_props):
     class Model(BaseModel):
         a: field_type
 
-    base_schema = {
+    assert Model.schema() == {
         'title': 'Model',
         'type': 'object',
-        'properties': {'a': {'title': 'A', 'type': 'array'}},
+        'properties': {'a': {'title': 'A', 'type': 'array', **extra_props}},
         'required': ['a'],
     }
-    base_schema['properties']['a']['items'] = expected_schema
-
-    assert Model.schema() == base_schema
 
 
 def test_deque():
@@ -1944,6 +1946,8 @@ def test_model_with_extra_forbidden():
                     {'exclusiveMinimum': 0, 'type': 'integer'},
                     {'exclusiveMinimum': 0, 'type': 'integer'},
                 ],
+                'minItems': 3,
+                'maxItems': 3,
             },
         ),
         (
@@ -2333,6 +2337,8 @@ def test_namedtuple_default():
                 'default': Coordinates(x=0, y=0),
                 'type': 'array',
                 'items': [{'title': 'X', 'type': 'number'}, {'title': 'Y', 'type': 'number'}],
+                'minItems': 2,
+                'maxItems': 2,
             }
         },
     }
@@ -2424,11 +2430,19 @@ def test_advanced_generic_schema():
                 'examples': 'examples',
             },
             'data3': {'title': 'Data3', 'type': 'array', 'items': {}},
-            'data4': {'title': 'Data4', 'type': 'array', 'items': {'$ref': '#/definitions/CustomType'}},
+            'data4': {
+                'title': 'Data4',
+                'type': 'array',
+                'items': [{'$ref': '#/definitions/CustomType'}],
+                'minItems': 1,
+                'maxItems': 1,
+            },
             'data5': {
                 'title': 'Data5',
                 'type': 'array',
                 'items': [{'$ref': '#/definitions/CustomType'}, {'type': 'string'}],
+                'minItems': 2,
+                'maxItems': 2,
             },
         },
         'required': ['data0', 'data1', 'data2', 'data3', 'data4', 'data5'],