Repository: samuelcolvin/pydantic
Pull request #1981 (2020-10-09 10:58:40)

Force fields.Undefined to be a singleton object


<!-- 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

In various places of the code, we compare directly to `fields.Undefined`
since we assume it to be constant.
When new models get created however, the object is deepcopied and
is no longer identical with the original object.
We therefore add `__copy__` and `__deepcopy__` methods to ensure
that the copied objects are actually the same original object.

## Related issue number

While I did does not address the exact error message #1578 had, schema generation on inherited generics that had required generic fields did not work at all,  so it might also fix this.

## Checklist

* [x] Unit tests for the changes exist
* [ ] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `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)


codecov[bot]: # [Codecov](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981?src=pr&el=h1) Report
> Merging [#1981](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981?src=pr&el=desc) into [master](https://codecov.io/gh/samuelcolvin/pydantic/commit/87aeab3672bbed83070f02b08359513ba0085d4d?el=desc) will **decrease** coverage by `2.67%`.
> The diff coverage is `90.90%`.


```diff
@@            Coverage Diff             @@
##           master    #1981      +/-   ##
==========================================
- Coverage   99.84%   97.17%   -2.68%     
==========================================
  Files          42       21      -21     
  Lines        7832     3925    -3907     
  Branches     1576      788     -788     
==========================================
- Hits         7820     3814    -4006     
- Misses         10       97      +87     
- Partials        2       14      +12     
```


| [Impacted Files](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pydantic/fields.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981/diff?src=pr&el=tree#diff-cHlkYW50aWMvZmllbGRzLnB5) | `97.54% <80.00%> (-2.46%)` | :arrow_down: |
| [pydantic/tools.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981/diff?src=pr&el=tree#diff-cHlkYW50aWMvdG9vbHMucHk=) | `100.00% <100.00%> (ø)` | |
| [pydantic/generics.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981/diff?src=pr&el=tree#diff-cHlkYW50aWMvZ2VuZXJpY3MucHk=) | `27.94% <0.00%> (-72.06%)` | :arrow_down: |
| [pydantic/typing.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981/diff?src=pr&el=tree#diff-cHlkYW50aWMvdHlwaW5nLnB5) | `80.18% <0.00%> (-19.82%)` | :arrow_down: |
| [pydantic/decorator.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981/diff?src=pr&el=tree#diff-cHlkYW50aWMvZGVjb3JhdG9yLnB5) | `87.42% <0.00%> (-12.58%)` | :arrow_down: |
| [pydantic/version.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981/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/1981/diff?src=pr&el=tree#diff-cHlkYW50aWMvZW52X3NldHRpbmdzLnB5) | `97.43% <0.00%> (-2.57%)` | :arrow_down: |
| [pydantic/error\_wrappers.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981/diff?src=pr&el=tree#diff-cHlkYW50aWMvZXJyb3Jfd3JhcHBlcnMucHk=) | `98.86% <0.00%> (-1.14%)` | :arrow_down: |
| [pydantic/networks.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981/diff?src=pr&el=tree#diff-cHlkYW50aWMvbmV0d29ya3MucHk=) | `99.22% <0.00%> (-0.78%)` | :arrow_down: |
| ... and [23 more](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981?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/1981?src=pr&el=footer). Last update [dbb1ed0...5401afe](https://codecov.io/gh/samuelcolvin/pydantic/pull/1981?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/changes/1981-daviskirk.md b/changes/1981-daviskirk.md
new file mode 100644
index 000000000..ef52cb123
--- /dev/null
+++ b/changes/1981-daviskirk.md
@@ -0,0 +1,1 @@
+Force `fields.Undefined` to be a singleton object, fixing inherited generic model schemas
diff --git a/docs/index.md b/docs/index.md
index e39638d18..adfd4702d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -27,7 +27,7 @@ What's going on here:
 * `name` is inferred as a string from the provided default; because it has a default, it is not required.
 * `signup_ts` is a datetime field which is not required (and takes the value ``None`` if it's not supplied).
   *pydantic* will process either a unix timestamp int (e.g. `1496498400`) or a string representing the date & time.
-* `friends` uses python's typing system, and requires a list of inputs. As with `id`, integer-like objects
+* `friends` uses python's typing system, and requires a list of integers. As with `id`, integer-like objects
   will be converted to integers.
 
 If validation fails pydantic will raise an error with a breakdown of what was wrong:
diff --git a/docs/mypy_plugin.md b/docs/mypy_plugin.md
index e0c3ac656..b444d2d55 100644
--- a/docs/mypy_plugin.md
+++ b/docs/mypy_plugin.md
@@ -58,7 +58,7 @@ There are other benefits too! See below for more details.
 * classes decorated with [`@pydantic.dataclasess.dataclass`](usage/dataclasses.md) are type checked the same as standard python dataclasses
 * The `@pydantic.dataclasess.dataclass` decorator accepts a `config` keyword argument which has the same meaning as [the `Config` sub-class](usage/model_config.md).
 
-### Optional Capabilites:
+### Optional Capabilities:
 #### Prevent the use of required dynamic aliases
 * If the [`warn_required_dynamic_aliases` **plugin setting**](#plugin-settings) is set to `True`, you'll get a mypy
   error any time you use a dynamically-determined alias or alias generator on a model with
diff --git a/docs/usage/validators.md b/docs/usage/validators.md
index 4db5fef4d..339238bd7 100644
--- a/docs/usage/validators.md
+++ b/docs/usage/validators.md
@@ -53,7 +53,7 @@ A few more things to note:
 ## Subclass Validators and `each_item`
 
 If using a validator with a subclass that references a `List` type field on a parent class, using `each_item=True` will
-cause the validator not to run; instead, the list must be iterated over programatically.
+cause the validator not to run; instead, the list must be iterated over programmatically.
 
 ```py
 {!.tmp_examples/validators_subclass_each_item.py!}
diff --git a/pydantic/fields.py b/pydantic/fields.py
index f9114a1aa..d25123766 100644
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -40,11 +40,19 @@ from .validators import constant_validator, dict_validator, find_validators, val
 
 Required: Any = Ellipsis
 
+T = TypeVar('T')
+
 
 class UndefinedType:
     def __repr__(self) -> str:
         return 'PydanticUndefined'
 
+    def __copy__(self: T) -> T:
+        return self
+
+    def __deepcopy__(self: T, _: Any) -> T:
+        return self
+
 
 Undefined = UndefinedType()
 
diff --git a/pydantic/tools.py b/pydantic/tools.py
index 418c56d89..d0d320b5a 100644
--- a/pydantic/tools.py
+++ b/pydantic/tools.py
@@ -69,6 +69,11 @@ def parse_raw_as(
     type_name: Optional[NameFactory] = None,
 ) -> T:
     obj = load_str_bytes(
-        b, proto=proto, content_type=content_type, encoding=encoding, allow_pickle=allow_pickle, json_loads=json_loads,
+        b,
+        proto=proto,
+        content_type=content_type,
+        encoding=encoding,
+        allow_pickle=allow_pickle,
+        json_loads=json_loads,
     )
     return parse_obj_as(type_, obj, type_name=type_name)


Test patch:
diff --git a/tests/test_generics.py b/tests/test_generics.py
index b1821e77b..452fe3bb0 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -410,6 +410,26 @@ def test_custom_schema():
     assert schema['properties']['a'].get('description') == 'Custom'
 
 
+@skip_36
+def test_child_schema():
+
+    T = TypeVar('T')
+
+    class Model(GenericModel, Generic[T]):
+        a: T
+
+    class Child(Model[T], Generic[T]):
+        pass
+
+    schema = Child[int].schema()
+    assert schema == {
+        'title': 'Child[int]',
+        'type': 'object',
+        'properties': {'a': {'title': 'A', 'type': 'integer'}},
+        'required': ['a'],
+    }
+
+
 @skip_36
 def test_custom_generic_naming():
     T = TypeVar('T')
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 25e88ac96..6e734481c 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -439,21 +439,21 @@ def test_config_file_settings_nornir(env):
     """
 
     class Settings(BaseSettings):
-        a: str
-        b: str
-        c: str
+        param_a: str
+        param_b: str
+        param_c: str
 
         def _build_values(self, init_kwargs, _env_file, _env_file_encoding):
             config_settings = init_kwargs.pop('__config_settings__')
             return {**config_settings, **init_kwargs, **self._build_environ()}
 
-    env.set('C', 'env setting c')
+    env.set('PARAM_C', 'env setting c')
 
-    config = {'a': 'config a', 'b': 'config b', 'c': 'config c'}
-    s = Settings(__config_settings__=config, b='argument b', c='argument c')
-    assert s.a == 'config a'
-    assert s.b == 'argument b'
-    assert s.c == 'env setting c'
+    config = {'param_a': 'config a', 'param_b': 'config b', 'param_c': 'config c'}
+    s = Settings(__config_settings__=config, param_b='argument b', param_c='argument c')
+    assert s.param_a == 'config a'
+    assert s.param_b == 'argument b'
+    assert s.param_c == 'env setting c'
 
 
 test_env_file = """\
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 935038f04..989176c20 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,7 +1,7 @@
 import os
 import re
 import string
-from copy import deepcopy
+from copy import copy, deepcopy
 from distutils.version import StrictVersion
 from enum import Enum
 from typing import NewType, Union
@@ -296,6 +296,11 @@ def test_undefined_repr():
     assert repr(Undefined) == 'PydanticUndefined'
 
 
+def test_undefined_copy():
+    copy(Undefined) is Undefined
+    deepcopy(Undefined) is Undefined
+
+
 def test_get_model():
     class A(BaseModel):
         a: str