Repository: samuelcolvin/pydantic
Pull request #2136 (2020-11-19 20:00:39)

Added `StrictBytes` type
<!-- 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

Added `StrictBytes` type which rejects objects which are castable to bytes (ie. `int`, `float`, `str`), but which are not natively `bytes` type. All changes are modeled after `StrictStr`, `StrictInt`, `StrictFloat`, and `StrictBool` types.

<!-- Please give a short summary of the changes. -->

- Added `strict` option to `ConstrainedBytes` class. Default value of the option (`False`) respects downstream code continuity. Any initialization or subclass of `ConstrianedBytes` behaves identically to prior implementations unless the `"strict"` option is explicitly set to `True`).

- Added `strict_bytes_validator` which rejects any object which is not `Union[bytes, bytearray]` type. [Because `bytearray` instances are just mutable versions of `bytes`](https://python.readthedocs.io/en/latest/library/stdtypes.html#bytearray-objects), we accept both as equivalent native `byte` types. This is noted explicitly since the builtin bytearray construtor creates objects with the following behavior:

```python
>>> isinstance(bytearray('foo', 'utf-8'), bytes)
False
```

## Related issue number

n/a

<!-- Are there any issues opened that will be resolved by merging this change? -->

## Checklist

* [x] Unit tests for the changes exist
* [x] 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)


rlizzo: cc @hhsecond
codecov[bot]: # [Codecov](https://codecov.io/gh/samuelcolvin/pydantic/pull/2136?src=pr&el=h1) Report
> Merging [#2136](https://codecov.io/gh/samuelcolvin/pydantic/pull/2136?src=pr&el=desc) (28b6a11) into [master](https://codecov.io/gh/samuelcolvin/pydantic/commit/31bc2435d7968fdf8d1f0c5c67f0f851c1bef54e?el=desc) (31bc243) will **decrease** coverage by `0.36%`.
> The diff coverage is `100.00%`.


```diff
@@             Coverage Diff             @@
##            master    #2136      +/-   ##
===========================================
- Coverage   100.00%   99.63%   -0.37%     
===========================================
  Files           21       21              
  Lines         4093     4102       +9     
  Branches       823      825       +2     
===========================================
- Hits          4093     4087       -6     
- Misses           0       12      +12     
- Partials         0        3       +3     
```


| [Impacted Files](https://codecov.io/gh/samuelcolvin/pydantic/pull/2136?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pydantic/types.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2136/diff?src=pr&el=tree#diff-cHlkYW50aWMvdHlwZXMucHk=) | `100.00% <100.00%> (ø)` | |
| [pydantic/validators.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2136/diff?src=pr&el=tree#diff-cHlkYW50aWMvdmFsaWRhdG9ycy5weQ==) | `100.00% <100.00%> (ø)` | |
| [pydantic/version.py](https://codecov.io/gh/samuelcolvin/pydantic/pull/2136/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/2136/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/2136/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/2136/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/2136/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/2136?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/2136?src=pr&el=footer). Last update [31bc243...28b6a11](https://codecov.io/gh/samuelcolvin/pydantic/pull/2136?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/Makefile b/Makefile
index cc5d66bcb..2a5bacb75 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,7 @@ install-benchmarks: install-pydantic
 	pip install -U -r benchmarks/requirements.txt
 
 .PHONY: install
-install: install-testing install-linting
+install: install-testing install-linting install-docs
 	@echo 'installed development requirements'
 
 .PHONY: build-trace
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/changes/2111-aimestereo.md b/changes/2111-aimestereo.md
new file mode 100644
index 000000000..d930dfc85
--- /dev/null
+++ b/changes/2111-aimestereo.md
@@ -0,0 +1,1 @@
+Allow pickling of `pydantic.dataclasses.dataclass` dynamically created from a built-in `dataclasses.dataclass`.
\ No newline at end of file
diff --git a/changes/2116-PrettyWood.md b/changes/2116-PrettyWood.md
new file mode 100644
index 000000000..d2c81cbf7
--- /dev/null
+++ b/changes/2116-PrettyWood.md
@@ -0,0 +1,1 @@
+fix: update all modified field values in `root_validator` when `validate_assignment` is on
\ No newline at end of file
diff --git a/changes/2132-PrettyWood.md b/changes/2132-PrettyWood.md
new file mode 100644
index 000000000..ed05c0037
--- /dev/null
+++ b/changes/2132-PrettyWood.md
@@ -0,0 +1,1 @@
+Support plain `typing.Tuple` type
\ No newline at end of file
diff --git a/changes/2136-rlizzo.md b/changes/2136-rlizzo.md
new file mode 100644
index 000000000..ecae6a74c
--- /dev/null
+++ b/changes/2136-rlizzo.md
@@ -0,0 +1,1 @@
+Added `StrictBytes` type as well as `strict=False` option to `ConstrainedBytes`.
diff --git a/changes/2138-PrettyWood.md b/changes/2138-PrettyWood.md
new file mode 100644
index 000000000..e58d0ed16
--- /dev/null
+++ b/changes/2138-PrettyWood.md
@@ -0,0 +1,1 @@
+fix: support `underscore_attrs_are_private` with generic models
\ No newline at end of file
diff --git a/changes/2142-PrettyWood.md b/changes/2142-PrettyWood.md
new file mode 100644
index 000000000..ddba01523
--- /dev/null
+++ b/changes/2142-PrettyWood.md
@@ -0,0 +1,1 @@
+fix: set right default value for required (optional) fields
\ 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/contributing.md b/docs/contributing.md
index c154c2851..2ddfd118f 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -33,7 +33,7 @@ To make contributing as easy and fast as possible, you'll want to run tests and
 *pydantic* has few dependencies, doesn't require compiling and tests don't need access to databases, etc.
 Because of this, setting up and running the tests should be very simple.
 
-You'll need to have **python 3.6**, **3.7**, or **3.8**, **virtualenv**, **git**, and **make** installed.
+You'll need to have **python 3.6**, **3.7**, **3.8**, or **3.9**, **virtualenv**, **git**, and **make** installed.
 
 ```bash
 # 1. clone your fork and cd into the repo directory
@@ -45,7 +45,7 @@ virtualenv -p `which python3.7` env
 source env/bin/activate
 # (or however you prefer to setup a python environment, 3.6 will work too)
 
-# 3. Install pydantic, dependencies and test dependencies
+# 3. Install pydantic, dependencies, test dependencies and doc dependencies
 make install
 
 # 4. Checkout a new branch and make your changes
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/examples/types_strict.py b/docs/examples/types_strict.py
index 28f607b2a..6612dc551 100644
--- a/docs/examples/types_strict.py
+++ b/docs/examples/types_strict.py
@@ -1,4 +1,21 @@
-from pydantic import BaseModel, StrictBool, StrictInt, ValidationError, confloat
+from pydantic import (
+    BaseModel,
+    StrictBytes,
+    StrictBool,
+    StrictInt,
+    ValidationError,
+    confloat,
+)
+
+
+class StrictBytesModel(BaseModel):
+    strict_bytes: StrictBytes
+
+
+try:
+    StrictBytesModel(strict_bytes='hello world')
+except ValidationError as e:
+    print(e)
 
 
 class StrictIntModel(BaseModel):
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/types.md b/docs/usage/types.md
index 03fcb25fc..9a41fb1a7 100644
--- a/docs/usage/types.md
+++ b/docs/usage/types.md
@@ -721,14 +721,16 @@ Where `Field` refers to the [field function](schema.md#field-customisation).
 
 ## Strict Types
 
-You can use the `StrictStr`, `StrictInt`, `StrictFloat`, and `StrictBool` types
+You can use the `StrictStr`, `StrictBytes`, `StrictInt`, `StrictFloat`, and `StrictBool` types 
 to prevent coercion from compatible types.
 These types will only pass validation when the validated value is of the respective type or is a subtype of that type.
-This behavior is also exposed via the `strict` field of the `ConstrainedStr`, `ConstrainedFloat` and
-`ConstrainedInt` classes and can be combined with a multitude of complex validation rules.
+This behavior is also exposed via the `strict` field of the `ConstrainedStr`, `ConstrainedBytes`, 
+`ConstrainedFloat` and `ConstrainedInt` classes and can be combined with a multitude of complex validation rules.
 
 The following caveats apply:
 
+- `StrictBytes` (and the `strict` option of `ConstrainedBytes`) will accept both `bytes`,
+   and `bytearray` types. 
 - `StrictInt` (and the `strict` option of `ConstrainedInt`) will not accept `bool` types,
     even though `bool` is a subclass of `int` in Python. Other subclasses will work.
 - `StrictFloat` (and the `strict` option of `ConstrainedFloat`) will not accept `int`.
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..8f67da54b 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',
@@ -93,6 +97,7 @@ __all__ = [
     'SecretStr',
     'SecretBytes',
     'StrictBool',
+    'StrictBytes',
     'StrictInt',
     'StrictFloat',
     'PaymentCardNumber',
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
index 0d28e1b20..53f1427d8 100644
--- a/pydantic/dataclasses.py
+++ b/pydantic/dataclasses.py
@@ -119,10 +119,23 @@ def _process_class(
     #   __post_init__ = _pydantic_post_init
     # ```
     # with the exact same fields as the base dataclass
+    # and register it on module level to address pickle problem:
+    # https://github.com/samuelcolvin/pydantic/issues/2111
     if is_builtin_dataclass(_cls):
+        uniq_class_name = f'_Pydantic_{_cls.__name__}_{id(_cls)}'
         _cls = type(
-            _cls.__name__, (_cls,), {'__annotations__': _cls.__annotations__, '__post_init__': _pydantic_post_init}
+            # for pretty output new class will have the name as original
+            _cls.__name__,
+            (_cls,),
+            {
+                '__annotations__': _cls.__annotations__,
+                '__post_init__': _pydantic_post_init,
+                # attrs for pickle to find this class
+                '__module__': __name__,
+                '__qualname__': uniq_class_name,
+            },
         )
+        globals()[uniq_class_name] = _cls
     else:
         _cls.__post_init__ = _pydantic_post_init
     cls: Type['Dataclass'] = dataclasses.dataclass(  # type: ignore
diff --git a/pydantic/fields.py b/pydantic/fields.py
index 36ea690a2..7d848359d 100644
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -307,7 +307,7 @@ class ModelField(Representation):
         required: 'BoolUndefined' = Undefined
         if value is Required:
             required = True
-            value = None
+            value = Ellipsis
         elif value is not Undefined:
             required = False
         field_info.alias = field_info.alias or field_info_from_config.get('alias')
@@ -346,7 +346,6 @@ class ModelField(Representation):
         Note: this method is **not** idempotent (because _type_analysis is not idempotent),
         e.g. calling it it multiple times may modify the field and configure it incorrectly.
         """
-
         self._set_default_and_type()
         if self.type_.__class__ == ForwardRef:
             # self.type_ is currently a ForwardRef and there's nothing we can do now,
@@ -448,14 +447,19 @@ class ModelField(Representation):
             return
 
         if issubclass(origin, Tuple):  # type: ignore
-            self.shape = SHAPE_TUPLE
-            self.sub_fields = []
-            for i, t in enumerate(get_args(self.type_)):
-                if t is Ellipsis:
-                    self.type_ = get_args(self.type_)[0]
-                    self.shape = SHAPE_TUPLE_ELLIPSIS
-                    return
-                self.sub_fields.append(self._create_sub_type(t, f'{self.name}_{i}'))
+            # origin == Tuple without item type
+            if not get_args(self.type_):
+                self.type_ = Any
+                self.shape = SHAPE_TUPLE_ELLIPSIS
+            else:
+                self.shape = SHAPE_TUPLE
+                self.sub_fields = []
+                for i, t in enumerate(get_args(self.type_)):
+                    if t is Ellipsis:
+                        self.type_ = get_args(self.type_)[0]
+                        self.shape = SHAPE_TUPLE_ELLIPSIS
+                        return
+                    self.sub_fields.append(self._create_sub_type(t, f'{self.name}_{i}'))
             return
 
         if issubclass(origin, List):
@@ -605,6 +609,8 @@ class ModelField(Representation):
             e: errors_.PydanticTypeError
             if self.shape == SHAPE_LIST:
                 e = errors_.ListError()
+            elif self.shape in (SHAPE_TUPLE, SHAPE_TUPLE_ELLIPSIS):
+                e = errors_.TupleError()
             elif self.shape == SHAPE_SET:
                 e = errors_.SetError()
             elif self.shape == SHAPE_FROZENSET:
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..4834cbf12 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -406,7 +406,12 @@ class BaseModel(Representation, metaclass=ModelMetaclass):
             if errors:
                 raise ValidationError(errors, self.__class__)
 
-        self.__dict__[name] = value
+            # update the whole __dict__ as other values than just `value`
+            # may be changed (e.g. with `root_validator`)
+            object_setattr(self, '__dict__', new_values)
+        else:
+            self.__dict__[name] = value
+
         self.__fields_set__.add(name)
 
     def __getstate__(self) -> 'DictAny':
@@ -870,11 +875,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 +892,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..b54182c7d 100644
--- a/pydantic/types.py
+++ b/pydantic/types.py
@@ -39,6 +39,7 @@ from .validators import (
     path_validator,
     set_validator,
     str_validator,
+    strict_bytes_validator,
     strict_float_validator,
     strict_int_validator,
     strict_str_validator,
@@ -63,10 +64,14 @@ __all__ = [
     'conint',
     'PositiveInt',
     'NegativeInt',
+    'NonNegativeInt',
+    'NonPositiveInt',
     'ConstrainedFloat',
     'confloat',
     'PositiveFloat',
     'NegativeFloat',
+    'NonNegativeFloat',
+    'NonPositiveFloat',
     'ConstrainedDecimal',
     'condecimal',
     'UUID1',
@@ -80,6 +85,7 @@ __all__ = [
     'SecretStr',
     'SecretBytes',
     'StrictBool',
+    'StrictBytes',
     'StrictInt',
     'StrictFloat',
     'PaymentCardNumber',
@@ -108,6 +114,7 @@ class ConstrainedBytes(bytes):
     strip_whitespace = False
     min_length: OptionalInt = None
     max_length: OptionalInt = None
+    strict: bool = False
 
     @classmethod
     def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
@@ -115,11 +122,15 @@ class ConstrainedBytes(bytes):
 
     @classmethod
     def __get_validators__(cls) -> 'CallableGenerator':
-        yield bytes_validator
+        yield strict_bytes_validator if cls.strict else bytes_validator
         yield constr_strip_whitespace
         yield constr_length_validator
 
 
+class StrictBytes(ConstrainedBytes):
+    strict = True
+
+
 def conbytes(*, strip_whitespace: bool = False, min_length: int = None, max_length: int = None) -> Type[bytes]:
     # use kwargs then define conf in a dict to aid with IDE type hinting
     namespace = dict(strip_whitespace=strip_whitespace, min_length=min_length, max_length=max_length)
@@ -379,6 +390,14 @@ class NegativeInt(ConstrainedInt):
     lt = 0
 
 
+class NonPositiveInt(ConstrainedInt):
+    le = 0
+
+
+class NonNegativeInt(ConstrainedInt):
+    ge = 0
+
+
 class StrictInt(ConstrainedInt):
     strict = True
 
@@ -440,6 +459,14 @@ class NegativeFloat(ConstrainedFloat):
     lt = 0
 
 
+class NonPositiveFloat(ConstrainedFloat):
+    le = 0
+
+
+class NonNegativeFloat(ConstrainedFloat):
+    ge = 0
+
+
 class StrictFloat(ConstrainedFloat):
     strict = True
 
diff --git a/pydantic/utils.py b/pydantic/utils.py
index e3e613ca3..c75f4e1cf 100644
--- a/pydantic/utils.py
+++ b/pydantic/utils.py
@@ -636,5 +636,6 @@ def is_valid_private_name(name: str) -> bool:
         '__classcell__',
         '__doc__',
         '__module__',
+        '__orig_bases__',
         '__qualname__',
     }
diff --git a/pydantic/validators.py b/pydantic/validators.py
index 5beb4ea7e..c47ee2f4f 100644
--- a/pydantic/validators.py
+++ b/pydantic/validators.py
@@ -82,6 +82,15 @@ def bytes_validator(v: Any) -> bytes:
         raise errors.BytesError()
 
 
+def strict_bytes_validator(v: Any) -> Union[bytes]:
+    if isinstance(v, bytes):
+        return v
+    elif isinstance(v, bytearray):
+        return bytes(v)
+    else:
+        raise errors.BytesError()
+
+
 BOOL_FALSE = {0, '0', 'off', 'f', 'false', 'n', 'no'}
 BOOL_TRUE = {1, '1', 'on', 't', 'true', 'y', 'yes'}
 
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_dataclasses.py b/tests/test_dataclasses.py
index e213ff567..e087455b6 100644
--- a/tests/test_dataclasses.py
+++ b/tests/test_dataclasses.py
@@ -1,4 +1,5 @@
 import dataclasses
+import pickle
 from collections.abc import Hashable
 from datetime import datetime
 from pathlib import Path
@@ -406,7 +407,7 @@ def test_fields():
     fields = user.__pydantic_model__.__fields__
 
     assert fields['id'].required is True
-    assert fields['id'].default is None
+    assert fields['id'].default is Ellipsis
 
     assert fields['name'].required is False
     assert fields['name'].default == 'John Doe'
@@ -425,7 +426,7 @@ def test_default_factory_field():
     fields = user.__pydantic_model__.__fields__
 
     assert fields['id'].required is True
-    assert fields['id'].default is None
+    assert fields['id'].default is Ellipsis
 
     assert fields['aliases'].required is False
     assert fields['aliases'].default == {'John': 'Joey'}
@@ -733,7 +734,10 @@ def test_override_builtin_dataclass_nested_schema():
                 'type': 'object',
             }
         },
-        'properties': {'filename': {'title': 'Filename', 'type': 'string'}, 'meta': {'$ref': '#/definitions/Meta'}},
+        'properties': {
+            'filename': {'title': 'Filename', 'type': 'string'},
+            'meta': {'$ref': '#/definitions/Meta'},
+        },
         'required': ['filename', 'meta'],
         'title': 'File',
         'type': 'object',
@@ -795,3 +799,37 @@ def test_forward_stdlib_dataclass_params():
     e.other = 'bulbi2'
     with pytest.raises(dataclasses.FrozenInstanceError):
         e.item.name = 'pika2'
+
+
+def test_pickle_overriden_builtin_dataclass(create_module):
+    module = create_module(
+        # language=Python
+        """\
+import dataclasses
+import pydantic
+
+
+@dataclasses.dataclass
+class BuiltInDataclassForPickle:
+    value: int
+
+class ModelForPickle(pydantic.BaseModel):
+    # pickle can only work with top level classes as it imports them
+
+    dataclass: BuiltInDataclassForPickle
+
+    class Config:
+        validate_assignment = True
+        """
+    )
+    obj = module.ModelForPickle(dataclass=module.BuiltInDataclassForPickle(value=5))
+
+    pickled_obj = pickle.dumps(obj)
+    restored_obj = pickle.loads(pickled_obj)
+
+    assert restored_obj.dataclass.value == 5
+    assert restored_obj == obj
+
+    # ensure the restored dataclass is still a pydantic dataclass
+    with pytest.raises(ValidationError, match='value\n +value is not a valid integer'):
+        restored_obj.dataclass.value = 'value of a wrong type'
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_main.py b/tests/test_main.py
index f7ad7ee05..e026f59d0 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -666,6 +666,28 @@ def test_validating_assignment_post_root_validator_fail():
     ]
 
 
+def test_root_validator_many_values_change():
+    """It should run root_validator on assignment and update ALL concerned fields"""
+
+    class Rectangle(BaseModel):
+        width: float
+        height: float
+        area: float = None
+
+        class Config:
+            validate_assignment = True
+
+        @root_validator
+        def set_area(cls, values):
+            values['area'] = values['width'] * values['height']
+            return values
+
+    r = Rectangle(width=1, height=1)
+    assert r.area == 1
+    r.height = 5
+    assert r.area == 5
+
+
 def test_enum_values():
     FooEnum = Enum('FooEnum', {'foo': 'foo', 'bar': 'bar'})
 
@@ -958,6 +980,35 @@ def test_dict_exclude_unset_populated_by_alias_with_extra():
     assert m.dict(exclude_unset=True, by_alias=True) == {'alias_a': 'a', 'c': 'c'}
 
 
+def test_exclude_defaults():
+    class Model(BaseModel):
+        mandatory: str
+        nullable_mandatory: Optional[str] = ...
+        facultative: str = 'x'
+        nullable_facultative: Optional[str] = None
+
+    m = Model(mandatory='a', nullable_mandatory=None)
+    assert m.dict(exclude_defaults=True) == {
+        'mandatory': 'a',
+        'nullable_mandatory': None,
+    }
+
+    m = Model(mandatory='a', nullable_mandatory=None, facultative='y', nullable_facultative=None)
+    assert m.dict(exclude_defaults=True) == {
+        'mandatory': 'a',
+        'nullable_mandatory': None,
+        'facultative': 'y',
+    }
+
+    m = Model(mandatory='a', nullable_mandatory=None, facultative='y', nullable_facultative='z')
+    assert m.dict(exclude_defaults=True) == {
+        'mandatory': 'a',
+        'nullable_mandatory': None,
+        'facultative': 'y',
+        'nullable_facultative': 'z',
+    }
+
+
 def test_dir_fields():
     class MyModel(BaseModel):
         attribute_a: int
diff --git a/tests/test_private_attributes.py b/tests/test_private_attributes.py
index 21f52090b..d87e572ab 100644
--- a/tests/test_private_attributes.py
+++ b/tests/test_private_attributes.py
@@ -1,9 +1,13 @@
-from typing import ClassVar
+import sys
+from typing import ClassVar, Generic, TypeVar
 
 import pytest
 
 from pydantic import BaseModel, Extra, PrivateAttr
 from pydantic.fields import Undefined
+from pydantic.generics import GenericModel
+
+skip_36 = pytest.mark.skipif(sys.version_info < (3, 7), reason='generics only supported for python 3.7 and above')
 
 
 def test_private_attribute():
@@ -180,3 +184,19 @@ def test_config_override_init():
     m = MyModel(x='hello')
     assert m.dict() == {'x': 'hello'}
     assert m._private_attr == 123
+
+
+@skip_36
+def test_generic_private_attribute():
+    T = TypeVar('T')
+
+    class Model(GenericModel, Generic[T]):
+        value: T
+        _private_value: T
+
+        class Config:
+            underscore_attrs_are_private = True
+
+    m = Model[int](value=1, _private_attr=3)
+    m._private_value = 3
+    assert m.dict() == {'value': 1}
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..457817c8f 100644
--- a/tests/test_types.py
+++ b/tests/test_types.py
@@ -43,12 +43,17 @@ from pydantic import (
     NameEmail,
     NegativeFloat,
     NegativeInt,
+    NonNegativeFloat,
+    NonNegativeInt,
+    NonPositiveFloat,
+    NonPositiveInt,
     PositiveFloat,
     PositiveInt,
     PyObject,
     SecretBytes,
     SecretStr,
     StrictBool,
+    StrictBytes,
     StrictFloat,
     StrictInt,
     StrictStr,
@@ -1178,15 +1183,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 +1209,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 +1244,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 +1270,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},
@@ -1268,6 +1301,39 @@ def test_float_validation():
     ]
 
 
+def test_strict_bytes():
+    class Model(BaseModel):
+        v: StrictBytes
+
+    assert Model(v=b'foobar').v == b'foobar'
+    assert Model(v=bytearray('foobar', 'utf-8')).v == b'foobar'
+
+    with pytest.raises(ValidationError):
+        Model(v='foostring')
+
+    with pytest.raises(ValidationError):
+        Model(v=42)
+
+    with pytest.raises(ValidationError):
+        Model(v=0.42)
+
+
+def test_strict_bytes_subclass():
+    class MyStrictBytes(StrictBytes):
+        pass
+
+    class Model(BaseModel):
+        v: MyStrictBytes
+
+    a = Model(v=MyStrictBytes(b'foobar'))
+    assert isinstance(a.v, MyStrictBytes)
+    assert a.v == b'foobar'
+
+    b = Model(v=MyStrictBytes(bytearray('foobar', 'utf-8')))
+    assert isinstance(b.v, MyStrictBytes)
+    assert b.v == 'foobar'.encode()
+
+
 def test_strict_str():
     class Model(BaseModel):
         v: StrictStr
@@ -2309,21 +2375,24 @@ def test_generic_without_params():
     class Model(BaseModel):
         generic_list: List
         generic_dict: Dict
+        generic_tuple: Tuple
 
-    m = Model(generic_list=[0, 'a'], generic_dict={0: 'a', 'a': 0})
-    assert m.dict() == {'generic_list': [0, 'a'], 'generic_dict': {0: 'a', 'a': 0}}
+    m = Model(generic_list=[0, 'a'], generic_dict={0: 'a', 'a': 0}, generic_tuple=(1, 'q'))
+    assert m.dict() == {'generic_list': [0, 'a'], 'generic_dict': {0: 'a', 'a': 0}, 'generic_tuple': (1, 'q')}
 
 
 def test_generic_without_params_error():
     class Model(BaseModel):
         generic_list: List
         generic_dict: Dict
+        generic_tuple: Tuple
 
     with pytest.raises(ValidationError) as exc_info:
-        Model(generic_list=0, generic_dict=0)
+        Model(generic_list=0, generic_dict=0, generic_tuple=0)
     assert exc_info.value.errors() == [
         {'loc': ('generic_list',), 'msg': 'value is not a valid list', 'type': 'type_error.list'},
         {'loc': ('generic_dict',), 'msg': 'value is not a valid dict', 'type': 'type_error.dict'},
+        {'loc': ('generic_tuple',), 'msg': 'value is not a valid tuple', 'type': 'type_error.tuple'},
     ]