Repository: samuelcolvin/pydantic
Pull request #2110 (2020-11-10 09:49:45)

Updated validators.md with the type of field
As someone started using pydantic, it can be very convenient to know what type we can rely the returned field is, so we know what we can check on it and what fields it has.
After searching the code I found out it has to be of type ModelField and that's why I'm offering it here as something that I think should be clarified in the documentation.

<!-- 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

* [ ] Unit tests for the changes exist
* [ ] Tests pass on CI and coverage remains at 100%
* [ ] 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)


codecov[bot]: # [Codecov](https://codecov.io/gh/samuelcolvin/pydantic/pull/2110?src=pr&el=h1) Report
> Merging [#2110](https://codecov.io/gh/samuelcolvin/pydantic/pull/2110?src=pr&el=desc) (2877c6a) into [master](https://codecov.io/gh/samuelcolvin/pydantic/commit/31bc2435d7968fdf8d1f0c5c67f0f851c1bef54e?el=desc) (31bc243) will **decrease** coverage by `0.36%`.
> The diff coverage is `n/a`.


```diff
@@             Coverage Diff             @@
##            master    #2110      +/-   ##
===========================================
- Coverage   100.00%   99.63%   -0.37%     
===========================================
  Files           21       21              
  Lines         4093     4093              
  Branches       823      823              
===========================================
- Hits          4093     4078      -15     
- Misses           0       12      +12     
- Partials         0        3       +3     
```


| [Impacted Files](https://codecov.io/gh/samuelcolvin/pydantic/pull/2110?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pydantic/version.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2110/diff?src=pr&el=tree#diff-cHlkYW50aWMvdmVyc2lvbi5weQ==) | `88.23% <0.00%> (-11.77%)` | :arrow_down: |
| [pydantic/typing.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2110/diff?src=pr&el=tree#diff-cHlkYW50aWMvdHlwaW5nLnB5) | `94.30% <0.00%> (-5.70%)` | :arrow_down: |
| [pydantic/env\_settings.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2110/diff?src=pr&el=tree#diff-cHlkYW50aWMvZW52X3NldHRpbmdzLnB5) | `97.93% <0.00%> (-2.07%)` | :arrow_down: |
| [pydantic/networks.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2110/diff?src=pr&el=tree#diff-cHlkYW50aWMvbmV0d29ya3MucHk=) | `99.22% <0.00%> (-0.78%)` | :arrow_down: |
| [pydantic/utils.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2110/diff?src=pr&el=tree#diff-cHlkYW50aWMvdXRpbHMucHk=) | `99.29% <0.00%> (-0.71%)` | :arrow_down: |

------

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


Patch:
diff --git a/changes/1975-mdavis-xyz.md b/changes/1975-mdavis-xyz.md
new file mode 100644
index 000000000..82cf2eb29
--- /dev/null
+++ b/changes/1975-mdavis-xyz.md
@@ -0,0 +1,1 @@
+Add `NonNegativeInt`, `NonPositiveInt`, `NonNegativeFloat`, `NonPositiveFloat`
diff --git a/changes/2045-PrettyWood.md b/changes/2045-PrettyWood.md
new file mode 100644
index 000000000..1fdf47c1e
--- /dev/null
+++ b/changes/2045-PrettyWood.md
@@ -0,0 +1,1 @@
+add default encoder for `Pattern` type
\ No newline at end of file
diff --git a/changes/2071-uriyyo.md b/changes/2071-uriyyo.md
new file mode 100644
index 000000000..36b9a6aa1
--- /dev/null
+++ b/changes/2071-uriyyo.md
@@ -0,0 +1,1 @@
+Updated `create_model` return type annotation to return type which inherits from `__base__` argument.
\ No newline at end of file
diff --git a/changes/2109-bm424.md b/changes/2109-bm424.md
new file mode 100644
index 000000000..0d3c29cc6
--- /dev/null
+++ b/changes/2109-bm424.md
@@ -0,0 +1,1 @@
+Fix a regression where Enum fields would not propagate keyword arguments to the schema
\ No newline at end of file
diff --git a/docs/build/schema_mapping.py b/docs/build/schema_mapping.py
index f9731c0a0..578172746 100755
--- a/docs/build/schema_mapping.py
+++ b/docs/build/schema_mapping.py
@@ -382,6 +382,20 @@ table = [
         'JSON Schema Validation',
         ''
     ],
+    [
+        'NonNegativeInt',
+        'integer',
+        {'minimum': 0},
+        'JSON Schema Validation',
+        ''
+    ],
+    [
+        'NonPositiveInt',
+        'integer',
+        {'maximum': 0},
+        'JSON Schema Validation',
+        ''
+    ],
     [
         'ConstrainedFloat',
         'number',
@@ -413,6 +427,20 @@ table = [
         'JSON Schema Validation',
         ''
     ],
+    [
+        'NonNegativeFloat',
+        'number',
+        {'minimum': 0},
+        'JSON Schema Validation',
+        ''
+    ],
+    [
+        'NonPositiveFloat',
+        'number',
+        {'maximum': 0},
+        'JSON Schema Validation',
+        ''
+    ],
     [
         'ConstrainedDecimal',
         'number',
diff --git a/docs/examples/types_constrained.py b/docs/examples/types_constrained.py
index c97fe24ae..53addddb1 100644
--- a/docs/examples/types_constrained.py
+++ b/docs/examples/types_constrained.py
@@ -6,6 +6,10 @@ from pydantic import (
     NegativeInt,
     PositiveFloat,
     PositiveInt,
+    NonNegativeFloat,
+    NonNegativeInt,
+    NonPositiveFloat,
+    NonPositiveInt,
     conbytes,
     condecimal,
     confloat,
@@ -29,12 +33,16 @@ class Model(BaseModel):
     mod_int: conint(multiple_of=5)
     pos_int: PositiveInt
     neg_int: NegativeInt
+    non_neg_int: NonNegativeInt
+    non_pos_int: NonPositiveInt
 
     big_float: confloat(gt=1000, lt=1024)
     unit_interval: confloat(ge=0, le=1)
     mod_float: confloat(multiple_of=0.5)
     pos_float: PositiveFloat
     neg_float: NegativeFloat
+    non_neg_float: NonNegativeFloat
+    non_pos_float: NonPositiveFloat
 
     short_list: conlist(int, min_items=1, max_items=4)
     short_set: conset(int, min_items=1, max_items=4)
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 8f3b0d73d..70bc1d5f7 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,9 +1,9 @@
-ansi2html==1.5.2
+ansi2html==1.6.0
 flake8==3.8.4
 flake8-quotes==3.2.0
 mkdocs==1.1.2
 mkdocs-exclude==1.0.2
-mkdocs-material==6.1.0
+mkdocs-material==6.1.6
 markdown-include==0.6.0
 sqlalchemy
 orjson
diff --git a/docs/usage/validators.md b/docs/usage/validators.md
index 339238bd7..53018c3e3 100644
--- a/docs/usage/validators.md
+++ b/docs/usage/validators.md
@@ -13,7 +13,7 @@ A few things to note on validators:
 * you can also add any subset of the following arguments to the signature (the names **must** match):
     * `values`: a dict containing the name-to-value mapping of any previously-validated fields
     * `config`: the model config
-    * `field`: the field being validated
+    * `field`: the field being validated. Type of object is `pydantic.fields.ModelField`.
     * `**kwargs`: if provided, this will include the arguments above not explicitly listed in the signature
 * validators should either return the parsed value or raise a `ValueError`, `TypeError`, or `AssertionError`
   (``assert`` statements may be used).
diff --git a/pydantic/__init__.py b/pydantic/__init__.py
index 5cfc0583f..7b8a609b6 100644
--- a/pydantic/__init__.py
+++ b/pydantic/__init__.py
@@ -76,10 +76,14 @@ __all__ = [
     'conint',
     'PositiveInt',
     'NegativeInt',
+    'NonNegativeInt',
+    'NonPositiveInt',
     'ConstrainedFloat',
     'confloat',
     'PositiveFloat',
     'NegativeFloat',
+    'NonNegativeFloat',
+    'NonPositiveFloat',
     'ConstrainedDecimal',
     'condecimal',
     'UUID1',
diff --git a/pydantic/json.py b/pydantic/json.py
index 9ce9d57d5..143ae877b 100644
--- a/pydantic/json.py
+++ b/pydantic/json.py
@@ -5,7 +5,7 @@ from enum import Enum
 from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
 from pathlib import Path
 from types import GeneratorType
-from typing import Any, Callable, Dict, Type, Union
+from typing import Any, Callable, Dict, Pattern, Type, Union
 from uuid import UUID
 
 from .color import Color
@@ -54,6 +54,9 @@ def pydantic_encoder(obj: Any) -> Any:
     elif is_dataclass(obj):
         return asdict(obj)
 
+    if isinstance(obj, Pattern):
+        return obj.pattern
+
     # Check the class type and its superclasses for a matching encoder
     for base in obj.__class__.__mro__[:-1]:
         try:
diff --git a/pydantic/main.py b/pydantic/main.py
index 3d57b1bbc..b63c6cbaf 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -870,11 +870,11 @@ def create_model(
     __model_name: str,
     *,
     __config__: Type[BaseConfig] = None,
-    __base__: Type[BaseModel] = None,
+    __base__: Type['Model'] = None,
     __module__: str = __name__,
     __validators__: Dict[str, classmethod] = None,
     **field_definitions: Any,
-) -> Type[BaseModel]:
+) -> Type['Model']:
     """
     Dynamically create a model.
     :param __model_name: name of the created model
@@ -887,11 +887,12 @@ def create_model(
         `foobar=(str, ...)` or `foobar=123`, or, for complex use-cases, in the format
         `<name>=<FieldInfo>`, e.g. `foo=Field(default_factory=datetime.utcnow, alias='bar')`
     """
-    if __base__:
+
+    if __base__ is not None:
         if __config__ is not None:
             raise ConfigError('to avoid confusion __config__ and __base__ cannot be used together')
     else:
-        __base__ = BaseModel
+        __base__ = cast(Type['Model'], BaseModel)
 
     fields = {}
     annotations = {}
diff --git a/pydantic/schema.py b/pydantic/schema.py
index a6a2777fd..e51d74ce1 100644
--- a/pydantic/schema.py
+++ b/pydantic/schema.py
@@ -260,7 +260,9 @@ def get_field_schema_validations(field: ModelField) -> Dict[str, Any]:
     f_schema: Dict[str, Any] = {}
 
     if lenient_issubclass(field.type_, Enum):
-        # schema is already updated by `enum_process_schema`
+        # schema is already updated by `enum_process_schema`; just update with field extra
+        if field.field_info.extra:
+            f_schema.update(field.field_info.extra)
         return f_schema
 
     if lenient_issubclass(field.type_, (str, bytes)):
diff --git a/pydantic/types.py b/pydantic/types.py
index 9c462e124..9c3279b99 100644
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -63,10 +63,14 @@ __all__ = [
     'conint',
     'PositiveInt',
     'NegativeInt',
+    'NonNegativeInt',
+    'NonPositiveInt',
     'ConstrainedFloat',
     'confloat',
     'PositiveFloat',
     'NegativeFloat',
+    'NonNegativeFloat',
+    'NonPositiveFloat',
     'ConstrainedDecimal',
     'condecimal',
     'UUID1',
@@ -379,6 +383,14 @@ class NegativeInt(ConstrainedInt):
     lt = 0
 
 
+class NonPositiveInt(ConstrainedInt):
+    le = 0
+
+
+class NonNegativeInt(ConstrainedInt):
+    ge = 0
+
+
 class StrictInt(ConstrainedInt):
     strict = True
 
@@ -440,6 +452,14 @@ class NegativeFloat(ConstrainedFloat):
     lt = 0
 
 
+class NonPositiveFloat(ConstrainedFloat):
+    le = 0
+
+
+class NonNegativeFloat(ConstrainedFloat):
+    ge = 0
+
+
 class StrictFloat(ConstrainedFloat):
     strict = True
 
diff --git a/requirements.txt b/requirements.txt
index 80e8d9c1a..065d6b6d7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@
 
 Cython==0.29.21;sys_platform!='win32'
 devtools==0.6.1
-email-validator==1.1.1
+email-validator==1.1.2
 dataclasses==0.6; python_version < '3.7'
 typing-extensions==3.7.4.1; python_version < '3.8'
-python-dotenv==0.14.0
+python-dotenv==0.15.0


Test patch:
diff --git a/tests/mypy/modules/plugin_success.py b/tests/mypy/modules/plugin_success.py
index af4b8e376..c173bc9e0 100644
--- a/tests/mypy/modules/plugin_success.py
+++ b/tests/mypy/modules/plugin_success.py
@@ -1,6 +1,6 @@
 from typing import ClassVar, Optional, Union
 
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, create_model
 from pydantic.dataclasses import dataclass
 
 
@@ -133,3 +133,9 @@ class NestedModel(BaseModel):
 
 
 _ = NestedModel.Model
+
+
+DynamicModel = create_model('DynamicModel', __base__=Model)
+
+dynamic_model = DynamicModel(x=1, y='y')
+dynamic_model.x = 2
diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt
index 31ea534ef..63e56b335 100644
--- a/tests/requirements-testing.txt
+++ b/tests/requirements-testing.txt
@@ -1,6 +1,6 @@
 coverage==5.3
 mypy==0.790
-pytest==6.1.1
+pytest==6.1.2
 pytest-cov==2.10.1
 pytest-mock==3.3.1
 pytest-sugar==0.9.4
diff --git a/tests/test_json.py b/tests/test_json.py
index cf916f6ae..07bfc04f7 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -1,5 +1,6 @@
 import datetime
 import json
+import re
 import sys
 from dataclasses import dataclass as vanilla_dataclass
 from decimal import Decimal
@@ -51,6 +52,7 @@ class MyEnum(Enum):
         (Decimal('12.34'), '12.34'),
         (create_model('BarModel', a='b', c='d')(), '{"a": "b", "c": "d"}'),
         (MyEnum.foo, '"bar"'),
+        (re.compile('^regex$'), '"^regex$"'),
     ],
 )
 def test_encoding(input, output):
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 9da7d14cc..ddea9ede3 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -42,6 +42,10 @@ from pydantic.types import (
     NoneBytes,
     NoneStr,
     NoneStrBytes,
+    NonNegativeFloat,
+    NonNegativeInt,
+    NonPositiveFloat,
+    NonPositiveInt,
     PositiveFloat,
     PositiveInt,
     PyObject,
@@ -374,6 +378,33 @@ def test_enum_and_model_have_same_behaviour():
     }
 
 
+def test_list_enum_schema_extras():
+    class FoodChoice(str, Enum):
+        spam = 'spam'
+        egg = 'egg'
+        chips = 'chips'
+
+    class Model(BaseModel):
+        foods: List[FoodChoice] = Field(examples=[['spam', 'egg']])
+
+    assert Model.schema() == {
+        'definitions': {
+            'FoodChoice': {
+                'description': 'An enumeration.',
+                'enum': ['spam', 'egg', 'chips'],
+                'title': 'FoodChoice',
+                'type': 'string',
+            }
+        },
+        'properties': {
+            'foods': {'type': 'array', 'items': {'$ref': '#/definitions/FoodChoice'}, 'examples': [['spam', 'egg']]},
+        },
+        'required': ['foods'],
+        'title': 'Model',
+        'type': 'object',
+    }
+
+
 def test_json_schema():
     class Model(BaseModel):
         a = b'foobar'
@@ -754,6 +785,8 @@ def test_secret_types(field_type, inner_type):
         (conint(multiple_of=5), {'multipleOf': 5}),
         (PositiveInt, {'exclusiveMinimum': 0}),
         (NegativeInt, {'exclusiveMaximum': 0}),
+        (NonNegativeInt, {'minimum': 0}),
+        (NonPositiveInt, {'maximum': 0}),
     ],
 )
 def test_special_int_types(field_type, expected_schema):
@@ -780,6 +813,8 @@ def test_special_int_types(field_type, expected_schema):
         (confloat(multiple_of=5), {'multipleOf': 5}),
         (PositiveFloat, {'exclusiveMinimum': 0}),
         (NegativeFloat, {'exclusiveMaximum': 0}),
+        (NonNegativeFloat, {'minimum': 0}),
+        (NonPositiveFloat, {'maximum': 0}),
         (ConstrainedDecimal, {}),
         (condecimal(gt=5, lt=10), {'exclusiveMinimum': 5, 'exclusiveMaximum': 10}),
         (condecimal(ge=5, le=10), {'minimum': 5, 'maximum': 10}),
diff --git a/tests/test_types.py b/tests/test_types.py
index 7e4c7e0bd..b07d1408d 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -43,6 +43,10 @@ from pydantic import (
     NameEmail,
     NegativeFloat,
     NegativeInt,
+    NonNegativeFloat,
+    NonNegativeInt,
+    NonPositiveFloat,
+    NonPositiveInt,
     PositiveFloat,
     PositiveInt,
     PyObject,
@@ -1178,15 +1182,17 @@ def test_int_validation():
     class Model(BaseModel):
         a: PositiveInt = None
         b: NegativeInt = None
-        c: conint(gt=4, lt=10) = None
-        d: conint(ge=0, le=10) = None
-        e: conint(multiple_of=5) = None
+        c: NonNegativeInt = None
+        d: NonPositiveInt = None
+        e: conint(gt=4, lt=10) = None
+        f: conint(ge=0, le=10) = None
+        g: conint(multiple_of=5) = None
 
-    m = Model(a=5, b=-5, c=5, d=0, e=25)
-    assert m == {'a': 5, 'b': -5, 'c': 5, 'd': 0, 'e': 25}
+    m = Model(a=5, b=-5, c=0, d=0, e=5, f=0, g=25)
+    assert m == {'a': 5, 'b': -5, 'c': 0, 'd': 0, 'e': 5, 'f': 0, 'g': 25}
 
     with pytest.raises(ValidationError) as exc_info:
-        Model(a=-5, b=5, c=-5, d=11, e=42)
+        Model(a=-5, b=5, c=-5, d=5, e=-5, f=11, g=42)
     assert exc_info.value.errors() == [
         {
             'loc': ('a',),
@@ -1202,18 +1208,30 @@ def test_int_validation():
         },
         {
             'loc': ('c',),
+            'msg': 'ensure this value is greater than or equal to 0',
+            'type': 'value_error.number.not_ge',
+            'ctx': {'limit_value': 0},
+        },
+        {
+            'loc': ('d',),
+            'msg': 'ensure this value is less than or equal to 0',
+            'type': 'value_error.number.not_le',
+            'ctx': {'limit_value': 0},
+        },
+        {
+            'loc': ('e',),
             'msg': 'ensure this value is greater than 4',
             'type': 'value_error.number.not_gt',
             'ctx': {'limit_value': 4},
         },
         {
-            'loc': ('d',),
+            'loc': ('f',),
             'msg': 'ensure this value is less than or equal to 10',
             'type': 'value_error.number.not_le',
             'ctx': {'limit_value': 10},
         },
         {
-            'loc': ('e',),
+            'loc': ('g',),
             'msg': 'ensure this value is a multiple of 5',
             'type': 'value_error.number.not_multiple',
             'ctx': {'multiple_of': 5},
@@ -1225,15 +1243,17 @@ def test_float_validation():
     class Model(BaseModel):
         a: PositiveFloat = None
         b: NegativeFloat = None
-        c: confloat(gt=4, lt=12.2) = None
-        d: confloat(ge=0, le=9.9) = None
-        e: confloat(multiple_of=0.5) = None
+        c: NonNegativeFloat = None
+        d: NonPositiveFloat = None
+        e: confloat(gt=4, lt=12.2) = None
+        f: confloat(ge=0, le=9.9) = None
+        g: confloat(multiple_of=0.5) = None
 
-    m = Model(a=5.1, b=-5.2, c=5.3, d=9.9, e=2.5)
-    assert m.dict() == {'a': 5.1, 'b': -5.2, 'c': 5.3, 'd': 9.9, 'e': 2.5}
+    m = Model(a=5.1, b=-5.2, c=0, d=0, e=5.3, f=9.9, g=2.5)
+    assert m.dict() == {'a': 5.1, 'b': -5.2, 'c': 0, 'd': 0, 'e': 5.3, 'f': 9.9, 'g': 2.5}
 
     with pytest.raises(ValidationError) as exc_info:
-        Model(a=-5.1, b=5.2, c=-5.3, d=9.91, e=4.2)
+        Model(a=-5.1, b=5.2, c=-5.1, d=5.1, e=-5.3, f=9.91, g=4.2)
     assert exc_info.value.errors() == [
         {
             'loc': ('a',),
@@ -1249,18 +1269,30 @@ def test_float_validation():
         },
         {
             'loc': ('c',),
+            'msg': 'ensure this value is greater than or equal to 0',
+            'type': 'value_error.number.not_ge',
+            'ctx': {'limit_value': 0},
+        },
+        {
+            'loc': ('d',),
+            'msg': 'ensure this value is less than or equal to 0',
+            'type': 'value_error.number.not_le',
+            'ctx': {'limit_value': 0},
+        },
+        {
+            'loc': ('e',),
             'msg': 'ensure this value is greater than 4',
             'type': 'value_error.number.not_gt',
             'ctx': {'limit_value': 4},
         },
         {
-            'loc': ('d',),
+            'loc': ('f',),
             'msg': 'ensure this value is less than or equal to 9.9',
             'type': 'value_error.number.not_le',
             'ctx': {'limit_value': 9.9},
         },
         {
-            'loc': ('e',),
+            'loc': ('g',),
             'msg': 'ensure this value is a multiple of 0.5',
             'type': 'value_error.number.not_multiple',
             'ctx': {'multiple_of': 0.5},