From 6f46a5a1460728868f860840e3f6186aa40403d8 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sun, 2 Jan 2022 13:53:45 +0000 Subject: [PATCH 01/62] drop python3.6 support (#3605) * drop python3.6 support * revert small change * fix 3.7 failures * more cases and cleanup * add change description --- .github/workflows/ci.yml | 6 +- README.md | 4 +- changes/3605-samuelcolvin.md | 1 + docs/contributing.md | 4 +- docs/install.md | 7 +- docs/usage/dataclasses.md | 2 - docs/usage/models.md | 4 - mkdocs.yml | 2 +- pydantic/config.py | 5 +- pydantic/fields.py | 1 - pydantic/json.py | 9 +- pydantic/typing.py | 109 +++++-------------- setup.py | 6 +- tests/mypy/modules/plugin_fail.py | 2 +- tests/mypy/modules/plugin_success.py | 14 ++- tests/mypy/modules/success.py | 28 ++--- tests/mypy/outputs/plugin-success-strict.txt | 6 +- tests/mypy/outputs/plugin_success.txt | 6 +- tests/mypy/test_mypy.py | 2 +- tests/test_decorator.py | 4 +- tests/test_discrimated_union.py | 2 - tests/test_edge_cases.py | 7 +- tests/test_errors.py | 2 - tests/test_forward_ref.py | 14 --- tests/test_generics.py | 66 ----------- tests/test_main.py | 2 - tests/test_private_attributes.py | 4 - tests/test_schema.py | 15 --- tests/test_types.py | 13 +-- tests/test_utils.py | 1 - 30 files changed, 85 insertions(+), 263 deletions(-) create mode 100644 changes/3605-samuelcolvin.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c33c64d3a0c..d7760d88fd3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10'] env: PYTHON: ${{ matrix.python-version }} OS: ubuntu @@ -131,7 +131,7 @@ jobs: fail-fast: false matrix: os: [macos, windows] - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10'] env: PYTHON: ${{ matrix.python-version }} OS: ${{ matrix.os }} @@ -283,7 +283,7 @@ jobs: fail-fast: false matrix: os: [ubuntu , macos , windows] - python-version: ['6', '7', '8', '9', '10'] + python-version: ['7', '8', '9', '10'] include: - os: ubuntu platform: linux diff --git a/README.md b/README.md index 98e022ac602..c4ddb13ad25 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ [![versions](https://img.shields.io/pypi/pyversions/pydantic.svg)](https://github.com/samuelcolvin/pydantic) [![license](https://img.shields.io/github/license/samuelcolvin/pydantic.svg)](https://github.com/samuelcolvin/pydantic/blob/master/LICENSE) -Data validation and settings management using Python type hinting. +Data validation and settings management using Python type hints. Fast and extensible, *pydantic* plays nicely with your linters/IDE/brain. -Define how data should be in pure, canonical Python 3.6+; validate it with *pydantic*. +Define how data should be in pure, canonical Python 3.7+; validate it with *pydantic*. ## Help diff --git a/changes/3605-samuelcolvin.md b/changes/3605-samuelcolvin.md new file mode 100644 index 00000000000..305c118e9df --- /dev/null +++ b/changes/3605-samuelcolvin.md @@ -0,0 +1 @@ +Drop support for python3.6, associated cleanup diff --git a/docs/contributing.md b/docs/contributing.md index 01c3b441aa6..2fd4fa6ea77 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 a version between **python 3.6 and 3.10**, **virtualenv**, **git**, and **make** installed. +You'll need to have a version between **python 3.7 and 3.10**, **virtualenv**, **git**, and **make** installed. ```bash # 1. clone your fork and cd into the repo directory @@ -44,7 +44,7 @@ cd pydantic virtualenv -p `which python3.8` env source env/bin/activate # Building docs requires 3.8. If you don't need to build docs you can use -# whichever version; 3.6 will work too. +# whichever version; 3.7 will work too. # 3. Install pydantic, dependencies, test dependencies and doc dependencies make install diff --git a/docs/install.md b/docs/install.md index 15341596b8b..30b3c0fa4ee 100644 --- a/docs/install.md +++ b/docs/install.md @@ -4,10 +4,9 @@ Installation is as simple as: pip install pydantic ``` -*pydantic* has no required dependencies except python 3.6, 3.7, 3.8, 3.9 or 3.10, -[`typing-extensions`](https://pypi.org/project/typing-extensions/), and the -[`dataclasses`](https://pypi.org/project/dataclasses/) backport package for python 3.6. -If you've got python 3.6+ and `pip` installed, you're good to go. +*pydantic* has no required dependencies except python 3.7, 3.8, 3.9 or 3.10 and +[`typing-extensions`](https://pypi.org/project/typing-extensions/). +If you've got python 3.7+ and `pip` installed, you're good to go. Pydantic is also available on [conda](https://www.anaconda.com) under the [conda-forge](https://conda-forge.org) channel: diff --git a/docs/usage/dataclasses.md b/docs/usage/dataclasses.md index ef07de3a689..3b302d65bb6 100644 --- a/docs/usage/dataclasses.md +++ b/docs/usage/dataclasses.md @@ -1,8 +1,6 @@ If you don't want to use _pydantic_'s `BaseModel` you can instead get the same data validation on standard [dataclasses](https://docs.python.org/3/library/dataclasses.html) (introduced in python 3.7). -Dataclasses work in python 3.6 using the [dataclasses backport package](https://github.com/ericvsmith/dataclasses). - ```py {!.tmp_examples/dataclasses_main.py!} ``` diff --git a/docs/usage/models.md b/docs/usage/models.md index 072038d4152..8f7bc8237b9 100644 --- a/docs/usage/models.md +++ b/docs/usage/models.md @@ -296,10 +296,6 @@ For example, in the example above, if `_fields_set` was not provided, Pydantic supports the creation of generic models to make it easier to reuse a common model structure. -!!! warning - Generic models are only supported with python `>=3.7`, this is because of numerous subtle changes in how - generics are implemented between python 3.6 and python 3.7. - In order to declare a generic model, you perform the following steps: * Declare one or more `typing.TypeVar` instances to use to parameterize your model. diff --git a/mkdocs.yml b/mkdocs.yml index 537bca4e3e7..ff51c5b7a68 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: pydantic -site_description: Data validation and settings management using python 3.6 type hinting +site_description: Data validation and settings management using python type hints strict: true site_url: https://pydantic-docs.helpmanual.io/ diff --git a/pydantic/config.py b/pydantic/config.py index b37cd98ff17..ef4b3c008fc 100644 --- a/pydantic/config.py +++ b/pydantic/config.py @@ -1,6 +1,6 @@ import json from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, ForwardRef, Optional, Tuple, Type, Union from .typing import AnyCallable from .utils import GetterDict @@ -59,8 +59,7 @@ class BaseConfig: schema_extra: Union[Dict[str, Any], 'SchemaExtraCallable'] = {} json_loads: Callable[[str], Any] = json.loads json_dumps: Callable[..., str] = json.dumps - # key type should include ForwardRef, but that breaks with python3.6 - json_encoders: Dict[Union[Type[Any], str], AnyCallable] = {} + json_encoders: Dict[Union[Type[Any], str, ForwardRef], AnyCallable] = {} underscore_attrs_are_private: bool = False # whether inherited models as fields should be reconstructed as base model diff --git a/pydantic/fields.py b/pydantic/fields.py index 7f0094cc462..6f169903235 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -1135,7 +1135,6 @@ def is_complex(self) -> bool: def _type_display(self) -> PyObjectStr: t = display_as_type(self.type_) - # have to do this since display_as_type(self.outer_type_) is different (and wrong) on python 3.6 if self.shape in MAPPING_LIKE_SHAPES: t = f'Mapping[{display_as_type(self.key_field.type_)}, {t}]' # type: ignore elif self.shape == SHAPE_TUPLE: diff --git a/pydantic/json.py b/pydantic/json.py index ce956fea263..b732cc0a2a7 100644 --- a/pydantic/json.py +++ b/pydantic/json.py @@ -1,21 +1,14 @@ import datetime -import re -import sys from collections import deque from decimal import Decimal from enum import Enum from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network from pathlib import Path +from re import Pattern from types import GeneratorType from typing import Any, Callable, Dict, Type, Union from uuid import UUID -if sys.version_info >= (3, 7): - Pattern = re.Pattern -else: - # python 3.6 - Pattern = re.compile('a').__class__ - from .color import Color from .networks import NameEmail from .types import SecretBytes, SecretStr diff --git a/pydantic/typing.py b/pydantic/typing.py index 730dc46442c..c6e9da18ff7 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -1,11 +1,14 @@ import sys +from collections.abc import Callable from os import PathLike from typing import ( # type: ignore TYPE_CHECKING, AbstractSet, Any, + Callable as TypingCallable, ClassVar, Dict, + ForwardRef, Generator, Iterable, List, @@ -36,28 +39,7 @@ TypingGenericAlias = () -if sys.version_info < (3, 7): - if TYPE_CHECKING: - - class ForwardRef: - def __init__(self, arg: Any): - pass - - def _eval_type(self, globalns: Any, localns: Any) -> Any: - pass - - else: - from typing import _ForwardRef as ForwardRef -else: - from typing import ForwardRef - - -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): +if sys.version_info < (3, 9): def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: return type_._evaluate(globalns, localns) @@ -72,7 +54,7 @@ def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: if sys.version_info < (3, 9): # Ensure we always get all the whole `Annotated` hint, not just the annotated type. - # For 3.6 to 3.8, `get_type_hints` doesn't recognize `typing_extensions.Annotated`, + # For 3.7 to 3.8, `get_type_hints` doesn't recognize `typing_extensions.Annotated`, # so it already returns the full annotation get_all_type_hints = get_type_hints @@ -82,17 +64,8 @@ def get_all_type_hints(obj: Any, globalns: Any = None, localns: Any = None) -> A return get_type_hints(obj, globalns, localns, include_extras=True) -if sys.version_info < (3, 7): - from typing import Callable as Callable - - AnyCallable = Callable[..., Any] - NoArgAnyCallable = Callable[[], Any] -else: - from collections.abc import Callable as Callable - from typing import Callable as TypingCallable - - AnyCallable = TypingCallable[..., Any] - NoArgAnyCallable = TypingCallable[[], Any] +AnyCallable = TypingCallable[..., Any] +NoArgAnyCallable = TypingCallable[[], Any] # Annotated[...] is implemented by returning an instance of one of these classes, depending on @@ -104,7 +77,8 @@ def get_all_type_hints(obj: Any, globalns: Any = None, localns: Any = None) -> A def get_origin(t: Type[Any]) -> Optional[Type[Any]]: if type(t).__name__ in AnnotatedTypeNames: - return cast(Type[Any], Annotated) # mypy complains about _SpecialForm in py3.6 + # weirdly this is a runtime requirement, as well as for mypy + return cast(Type[Any], Annotated) return getattr(t, '__origin__', None) else: @@ -122,22 +96,7 @@ def get_origin(tp: Type[Any]) -> Optional[Type[Any]]: return _typing_get_origin(tp) or getattr(tp, '__origin__', None) -if sys.version_info < (3, 7): # noqa: C901 (ignore complexity) - - def get_args(t: Type[Any]) -> Tuple[Any, ...]: - """Simplest get_args compatibility layer possible. - - The Python 3.6 typing module does not have `_GenericAlias` so - this won't work for everything. In particular this will not - support the `generics` module (we don't support generic models in - python 3.6). - - """ - if type(t).__name__ in AnnotatedTypeNames: - return t.__args__ + t.__metadata__ - return getattr(t, '__args__', ()) - -elif sys.version_info < (3, 8): # noqa: C901 +if sys.version_info < (3, 8): from typing import _GenericAlias def get_args(t: Type[Any]) -> Tuple[Any, ...]: @@ -279,8 +238,8 @@ def is_union(tp: Optional[Type[Any]]) -> bool: if sys.version_info < (3, 8): - # Even though this implementation is slower, we need it for python 3.6/3.7: - # In python 3.6/3.7 "Literal" is not a builtin type and uses a different + # Even though this implementation is slower, we need it for python 3.7: + # In python 3.7 "Literal" is not a builtin type and uses a different # mechanism. # for this reason `Literal[None] is Literal[None]` evaluates to `False`, # breaking the faster implementation used for the other python versions. @@ -348,10 +307,8 @@ def resolve_annotations(raw_annotations: Dict[str, Type[Any]], module_name: Opti if isinstance(value, str): if (3, 10) > sys.version_info >= (3, 9, 8) or sys.version_info >= (3, 10, 1): value = ForwardRef(value, is_argument=False, is_class=True) - elif sys.version_info >= (3, 7): - value = ForwardRef(value, is_argument=False) else: - value = ForwardRef(value) + value = ForwardRef(value, is_argument=False) try: value = _eval_type(value, base_globals, None) except NameError: @@ -365,21 +322,12 @@ def is_callable_type(type_: Type[Any]) -> bool: return type_ is Callable or get_origin(type_) is Callable -if sys.version_info >= (3, 7): +def is_literal_type(type_: Type[Any]) -> bool: + return Literal is not None and get_origin(type_) is Literal - def is_literal_type(type_: Type[Any]) -> bool: - return Literal is not None and get_origin(type_) is Literal - def literal_values(type_: Type[Any]) -> Tuple[Any, ...]: - return get_args(type_) - -else: - - def is_literal_type(type_: Type[Any]) -> bool: - return Literal is not None and hasattr(type_, '__values__') and type_ == Literal[type_.__values__] - - def literal_values(type_: Type[Any]) -> Tuple[Any, ...]: - return type_.__values__ +def literal_values(type_: Type[Any]) -> Tuple[Any, ...]: + return get_args(type_) def all_literal_values(type_: Type[Any]) -> Tuple[Any, ...]: @@ -435,7 +383,7 @@ def _check_classvar(v: Optional[Type[Any]]) -> bool: if v is None: return False - return v.__class__ == ClassVar.__class__ and (sys.version_info < (3, 7) or getattr(v, '_name', None) == 'ClassVar') + return v.__class__ == ClassVar.__class__ and getattr(v, '_name', None) == 'ClassVar' def is_classvar(ann_type: Type[Any]) -> bool: @@ -461,7 +409,7 @@ def update_field_forward_refs(field: 'ModelField', globalns: Any, localns: Any) def update_model_forward_refs( model: Type[Any], fields: Iterable['ModelField'], - json_encoders: Dict[Union[Type[Any], str], AnyCallable], + json_encoders: Dict[Union[Type[Any], str, ForwardRef], AnyCallable], localns: 'DictStrAny', exc_to_suppress: Tuple[Type[BaseException], ...] = (), ) -> None: @@ -502,17 +450,14 @@ 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 without brackets. Otherwise returns None. """ - try: - origin = get_origin(type_) - if origin is None: # Python 3.6 - origin = type_ - if issubclass(origin, Type): # type: ignore - if not get_args(type_) or not isinstance(get_args(type_)[0], type): - return True - return get_args(type_)[0] - except (AttributeError, TypeError): - pass - return None + if get_origin(type_) is None: + return None + + args = get_args(type_) + if not args or not isinstance(args[0], type): + return True + else: + return args[0] def get_sub_types(tp: Any) -> List[Any]: diff --git a/setup.py b/setup.py index fb22f35fbb6..ae91cf9a0dd 100644 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ def extra(self): return '\n\n' + '\n'.join(sorted(self.links)) + '\n' -description = 'Data validation and settings management using python 3.6 type hinting' +description = 'Data validation and settings management using python type hints' THIS_DIR = Path(__file__).resolve().parent try: history = (THIS_DIR / 'HISTORY.md').read_text() @@ -104,7 +104,6 @@ def extra(self): 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', @@ -127,10 +126,9 @@ def extra(self): license='MIT', packages=['pydantic'], package_data={'pydantic': ['py.typed']}, - python_requires='>=3.6.1', + python_requires='>=3.7', zip_safe=False, # https://mypy.readthedocs.io/en/latest/installed_packages.html install_requires=[ - 'dataclasses>=0.6;python_version<"3.7"', 'typing-extensions>=3.7.4.3' ], extras_require={ diff --git a/tests/mypy/modules/plugin_fail.py b/tests/mypy/modules/plugin_fail.py index 7dd992a6daa..4c57862f0ac 100644 --- a/tests/mypy/modules/plugin_fail.py +++ b/tests/mypy/modules/plugin_fail.py @@ -114,7 +114,7 @@ class Blah(BaseModel): fields_set: Optional[Set[str]] = None -# Need to test generic checking here since generics don't work in 3.6, and plugin-success.py is executed +# (comment to keep line numbers unchanged) T = TypeVar('T') diff --git a/tests/mypy/modules/plugin_success.py b/tests/mypy/modules/plugin_success.py index 217eebcd32e..ee383c526fb 100644 --- a/tests/mypy/modules/plugin_success.py +++ b/tests/mypy/modules/plugin_success.py @@ -1,7 +1,8 @@ -from typing import ClassVar, Optional, Union +from typing import ClassVar, Generic, Optional, TypeVar, Union from pydantic import BaseModel, BaseSettings, Field, create_model, validator from pydantic.dataclasses import dataclass +from pydantic.generics import GenericModel class Model(BaseModel): @@ -181,3 +182,14 @@ class ModelWithAllowReuseValidator(BaseModel): model_with_allow_reuse_validator = ModelWithAllowReuseValidator(name='xyz') + + +T = TypeVar('T') + + +class Response(GenericModel, Generic[T]): + data: T + error: Optional[str] + + +response = Response[Model](data=model, error=None) diff --git a/tests/mypy/modules/success.py b/tests/mypy/modules/success.py index 51611e373ab..11e3db10ce1 100644 --- a/tests/mypy/modules/success.py +++ b/tests/mypy/modules/success.py @@ -5,7 +5,6 @@ """ import json import os -import sys from datetime import date, datetime, timedelta from pathlib import Path, PurePath from typing import Any, Dict, Generic, List, Optional, TypeVar @@ -134,23 +133,24 @@ def day_of_week(dt: datetime) -> int: assert m_copy.list_of_ints == m_from_obj.list_of_ints -if sys.version_info >= (3, 7): - T = TypeVar('T') +T = TypeVar('T') - class WrapperModel(GenericModel, Generic[T]): - payload: T - int_instance = WrapperModel[int](payload=1) - int_instance.payload += 1 - assert int_instance.payload == 2 +class WrapperModel(GenericModel, Generic[T]): + payload: T - str_instance = WrapperModel[str](payload='a') - str_instance.payload += 'a' - assert str_instance.payload == 'aa' - model_instance = WrapperModel[Model](payload=m) - model_instance.payload.list_of_ints.append(4) - assert model_instance.payload.list_of_ints == [1, 2, 3, 4] +int_instance = WrapperModel[int](payload=1) +int_instance.payload += 1 +assert int_instance.payload == 2 + +str_instance = WrapperModel[str](payload='a') +str_instance.payload += 'a' +assert str_instance.payload == 'aa' + +model_instance = WrapperModel[Model](payload=m) +model_instance.payload.list_of_ints.append(4) +assert model_instance.payload.list_of_ints == [1, 2, 3, 4] class WithField(BaseModel): diff --git a/tests/mypy/outputs/plugin-success-strict.txt b/tests/mypy/outputs/plugin-success-strict.txt index 80365a9df88..662ad6828de 100644 --- a/tests/mypy/outputs/plugin-success-strict.txt +++ b/tests/mypy/outputs/plugin-success-strict.txt @@ -1,3 +1,3 @@ -29: error: Unexpected keyword argument "z" for "Model" [call-arg] -64: error: Untyped fields disallowed [pydantic-field] -79: error: Argument "x" to "OverrideModel" has incompatible type "float"; expected "int" [arg-type] \ No newline at end of file +30: error: Unexpected keyword argument "z" for "Model" [call-arg] +65: error: Untyped fields disallowed [pydantic-field] +80: error: Argument "x" to "OverrideModel" has incompatible type "float"; expected "int" [arg-type] diff --git a/tests/mypy/outputs/plugin_success.txt b/tests/mypy/outputs/plugin_success.txt index 26ee50c39e1..5b782e4396a 100644 --- a/tests/mypy/outputs/plugin_success.txt +++ b/tests/mypy/outputs/plugin_success.txt @@ -1,3 +1,3 @@ -121: error: Unexpected keyword argument "name" for "AddProject" [call-arg] -121: error: Unexpected keyword argument "slug" for "AddProject" [call-arg] -121: error: Unexpected keyword argument "description" for "AddProject" [call-arg] \ No newline at end of file +122: error: Unexpected keyword argument "name" for "AddProject" [call-arg] +122: error: Unexpected keyword argument "slug" for "AddProject" [call-arg] +122: error: Unexpected keyword argument "description" for "AddProject" [call-arg] diff --git a/tests/mypy/test_mypy.py b/tests/mypy/test_mypy.py index 5f772eb72e5..4e6d173ae44 100644 --- a/tests/mypy/test_mypy.py +++ b/tests/mypy/test_mypy.py @@ -73,7 +73,7 @@ def test_mypy_results(config_filename: str, python_filename: str, output_filenam output_path.write_text(actual_out) raise RuntimeError(f'wrote actual output to {output_path} since file did not exist') - expected_out = Path(output_path).read_text() if output_path else '' + expected_out = Path(output_path).read_text().rstrip('\n') if output_path else '' # fix for compatibility between mypy versions: (this can be dropped once we drop support for mypy<0.930) if actual_out and float(mypy_version) < 0.930: diff --git a/tests/test_decorator.py b/tests/test_decorator.py index 97b78f88e88..c2503dd72f2 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -87,9 +87,7 @@ def foo_bar(a: int, b: int): assert foo_bar.model.__fields__.keys() == {'a', 'b', 'args', 'kwargs', 'v__duplicate_kwargs'} assert foo_bar.model.__name__ == 'FooBar' assert foo_bar.model.schema()['title'] == 'FooBar' - # signature is slightly different on 3.6 - if sys.version_info >= (3, 7): - assert repr(inspect.signature(foo_bar)) == '' + assert repr(inspect.signature(foo_bar)) == '' def test_kwargs(): diff --git a/tests/test_discrimated_union.py b/tests/test_discrimated_union.py index c7cd5f4e5ad..120f9d2f16b 100644 --- a/tests/test_discrimated_union.py +++ b/tests/test_discrimated_union.py @@ -1,5 +1,4 @@ import re -import sys from enum import Enum from typing import Generic, TypeVar, Union @@ -365,7 +364,6 @@ class Model(BaseModel): assert isinstance(Model(**{'pet': {'pet_type': 'dog', 'name': 'Milou'}, 'n': 5}).pet, Dog) -@pytest.mark.skipif(sys.version_info < (3, 7), reason='generics only supported for python 3.7 and above') def test_generic(): T = TypeVar('T') diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 227881b8f30..dd07eb3d37b 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -1298,7 +1298,6 @@ def validator(v): yield validator -@pytest.mark.skipif(sys.version_info < (3, 7), reason='output slightly different for 3.6') @pytest.mark.parametrize( 'type_,expected', [ @@ -1419,10 +1418,8 @@ def check_something(cls, value): class Bar(Foo): pass - # output is slightly different for 3.6 - if sys.version_info >= (3, 7): - assert repr(Foo.__fields__['foo']) == "ModelField(name='foo', type=List[List[int]], required=True)" - assert repr(Bar.__fields__['foo']) == "ModelField(name='foo', type=List[List[int]], required=True)" + assert repr(Foo.__fields__['foo']) == "ModelField(name='foo', type=List[List[int]], required=True)" + assert repr(Bar.__fields__['foo']) == "ModelField(name='foo', type=List[List[int]], required=True)" assert Foo(foo=[[0, 1]]).foo == [[0, 1]] assert Bar(foo=[[0, 1]]).foo == [[0, 1]] diff --git a/tests/test_errors.py b/tests/test_errors.py index 0f901b4edd1..7224372df68 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,5 +1,4 @@ import pickle -import sys from typing import Dict, List, Optional, Union from uuid import UUID, uuid4 @@ -72,7 +71,6 @@ def check_action(cls, v): ] -@pytest.mark.skipif(sys.version_info < (3, 7), reason='output slightly different for 3.6') def test_error_on_optional(): class Foobar(BaseModel): foo: Optional[str] = None diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py index df378810cd2..61e7be5cfb2 100644 --- a/tests/test_forward_ref.py +++ b/tests/test_forward_ref.py @@ -1,14 +1,10 @@ -import sys from typing import Optional, Tuple import pytest from pydantic import BaseModel, ConfigError, ValidationError -skip_pre_37 = pytest.mark.skipif(sys.version_info < (3, 7), reason='testing >= 3.7 behaviour only') - -@skip_pre_37 def test_postponed_annotations(create_module): module = create_module( # language=Python @@ -24,7 +20,6 @@ class Model(BaseModel): assert m.dict() == {'a': 123} -@skip_pre_37 def test_postponed_annotations_optional(create_module): module = create_module( # language=Python @@ -41,7 +36,6 @@ 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 @@ -215,7 +209,6 @@ class Dataclass: assert m.url == 'http://example.com' -@skip_pre_37 def test_forward_ref_dataclass_with_future_annotations(create_module): module = create_module( # language=Python @@ -330,7 +323,6 @@ class Account(BaseModel): } -@skip_pre_37 def test_self_reference_json_schema_with_future_annotations(create_module): module = create_module( # language=Python @@ -415,7 +407,6 @@ class Account(BaseModel): } -@skip_pre_37 def test_circular_reference_json_schema_with_future_annotations(create_module): module = create_module( # language=Python @@ -485,7 +476,6 @@ class Foo(BaseModel): c: List[Foo] = Field(..., gt=0) -@skip_pre_37 def test_forward_ref_optional(create_module): module = create_module( # language=Python @@ -531,7 +521,6 @@ def module(): assert instance.sub.dict() == {'foo': 'bar'} -@skip_pre_37 def test_resolve_forward_ref_dataclass(create_module): module = create_module( # language=Python @@ -611,7 +600,6 @@ class Dog(BaseModel): } -@skip_pre_37 def test_class_var_as_string(create_module): module = create_module( # language=Python @@ -628,7 +616,6 @@ class Model(BaseModel): assert module.Model.__class_vars__ == {'a'} -@skip_pre_37 def test_json_encoder_str(create_module): module = create_module( # language=Python @@ -662,7 +649,6 @@ class Config: assert m.json(models_as_dict=False) == '{"foo_user": {"x": "user1"}, "user": "User(user2)"}' -@skip_pre_37 def test_json_encoder_forward_ref(create_module): module = create_module( # language=Python diff --git a/tests/test_generics.py b/tests/test_generics.py index fb071b0cf82..d65c0196a87 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -23,10 +23,7 @@ from pydantic import BaseModel, Field, Json, ValidationError, root_validator, validator from pydantic.generics import GenericModel, _generic_types_cache, iter_contained_typevars, replace_types -skip_36 = pytest.mark.skipif(sys.version_info < (3, 7), reason='generics only supported for python 3.7 and above') - -@skip_36 def test_generic_name(): data_type = TypeVar('data_type') @@ -39,7 +36,6 @@ class Result(GenericModel, Generic[data_type]): assert Result[int].__name__ == 'Result[int]' -@skip_36 def test_double_parameterize_error(): data_type = TypeVar('data_type') @@ -52,7 +48,6 @@ class Result(GenericModel, Generic[data_type]): assert str(exc_info.value) == 'Cannot parameterize a concrete instantiation of a generic model' -@skip_36 def test_value_validation(): T = TypeVar('T') @@ -87,7 +82,6 @@ def validate_sum(cls, values): assert exc_info.value.errors() == [{'loc': ('__root__',), 'msg': 'sum too large', 'type': 'value_error'}] -@skip_36 def test_methods_are_inherited(): class CustomGenericModel(GenericModel): def method(self): @@ -103,7 +97,6 @@ class Model(CustomGenericModel, Generic[T]): assert instance.method() == 1 -@skip_36 def test_config_is_inherited(): class CustomGenericModel(GenericModel): class Config: @@ -122,7 +115,6 @@ class Model(CustomGenericModel, Generic[T]): assert str(exc_info.value) == '"Model[int]" is immutable and does not support item assignment' -@skip_36 def test_default_argument(): T = TypeVar('T') @@ -134,7 +126,6 @@ class Result(GenericModel, Generic[T]): assert result.other is True -@skip_36 def test_default_argument_for_typevar(): T = TypeVar('T') @@ -151,7 +142,6 @@ class Result(GenericModel, Generic[T]): assert result.data == 1 -@skip_36 def test_classvar(): T = TypeVar('T') @@ -165,7 +155,6 @@ class Result(GenericModel, Generic[T]): assert 'other' not in Result.__fields__ -@skip_36 def test_non_annotated_field(): T = TypeVar('T') @@ -180,7 +169,6 @@ class Result(GenericModel, Generic[T]): assert result.other is True -@skip_36 def test_must_inherit_from_generic(): with pytest.raises(TypeError) as exc_info: @@ -192,7 +180,6 @@ class Result(GenericModel): assert str(exc_info.value) == 'Type Result must inherit from typing.Generic before being parameterized' -@skip_36 def test_parameters_placed_on_generic(): T = TypeVar('T') with pytest.raises(TypeError, match='Type parameters should be placed on typing.Generic, not GenericModel'): @@ -201,7 +188,6 @@ class Result(GenericModel[T]): pass -@skip_36 def test_parameters_must_be_typevar(): with pytest.raises(TypeError, match='Type GenericModel must inherit from typing.Generic before being '): @@ -209,7 +195,6 @@ class Result(GenericModel[int]): pass -@skip_36 def test_subclass_can_be_genericized(): T = TypeVar('T') @@ -219,7 +204,6 @@ class Result(GenericModel, Generic[T]): Result[T] -@skip_36 def test_parameter_count(): T = TypeVar('T') S = TypeVar('S') @@ -237,7 +221,6 @@ class Model(GenericModel, Generic[T, S]): assert str(exc_info.value) == 'Too few parameters for Model; actual 1, expected 2' -@skip_36 def test_cover_cache(): cache_size = len(_generic_types_cache) T = TypeVar('T') @@ -251,7 +234,6 @@ class Model(GenericModel, Generic[T]): assert len(_generic_types_cache) == cache_size + 2 -@skip_36 def test_generic_config(): data_type = TypeVar('data_type') @@ -267,7 +249,6 @@ class Config: result.data = 2 -@skip_36 def test_enum_generic(): T = TypeVar('T') @@ -282,7 +263,6 @@ class Model(GenericModel, Generic[T]): Model[MyEnum](enum=2) -@skip_36 def test_generic(): data_type = TypeVar('data_type') error_type = TypeVar('error_type') @@ -337,7 +317,6 @@ class Data(BaseModel): ] -@skip_36 def test_alongside_concrete_generics(): from pydantic.generics import GenericModel @@ -352,7 +331,6 @@ class MyModel(GenericModel, Generic[T]): assert model.metadata == {} -@skip_36 def test_complex_nesting(): from pydantic.generics import GenericModel @@ -366,7 +344,6 @@ class MyModel(GenericModel, Generic[T]): assert model.item == item -@skip_36 def test_required_value(): T = TypeVar('T') @@ -378,7 +355,6 @@ class MyModel(GenericModel, Generic[T]): assert exc_info.value.errors() == [{'loc': ('a',), 'msg': 'field required', 'type': 'value_error.missing'}] -@skip_36 def test_optional_value(): T = TypeVar('T') @@ -389,7 +365,6 @@ class MyModel(GenericModel, Generic[T]): assert model.dict() == {'a': 1} -@skip_36 def test_custom_schema(): T = TypeVar('T') @@ -400,7 +375,6 @@ class MyModel(GenericModel, Generic[T]): assert schema['properties']['a'].get('description') == 'Custom' -@skip_36 def test_child_schema(): T = TypeVar('T') @@ -419,7 +393,6 @@ class Child(Model[T], Generic[T]): } -@skip_36 def test_custom_generic_naming(): T = TypeVar('T') @@ -436,7 +409,6 @@ def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str: assert repr(MyModel[str](value=None)) == 'OptionalStrWrapper(value=None)' -@skip_36 def test_nested(): AT = TypeVar('AT') @@ -468,7 +440,6 @@ class OuterT_SameType(GenericModel, Generic[AT]): ] -@skip_36 def test_partial_specification(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -488,7 +459,6 @@ class Model(GenericModel, Generic[AT, BT]): ] -@skip_36 def test_partial_specification_with_inner_typevar(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -508,7 +478,6 @@ class Model(GenericModel, Generic[AT, BT]): assert nested_resolved.b == [456] -@skip_36 def test_partial_specification_name(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -523,7 +492,6 @@ class Model(GenericModel, Generic[AT, BT]): assert concrete_model.__name__ == 'Model[int, BT][str]' -@skip_36 def test_partial_specification_instantiation(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -544,7 +512,6 @@ class Model(GenericModel, Generic[AT, BT]): ] -@skip_36 def test_partial_specification_instantiation_bounded(): AT = TypeVar('AT') BT = TypeVar('BT', bound=int) @@ -569,7 +536,6 @@ class Model(GenericModel, Generic[AT, BT]): ] -@skip_36 def test_typevar_parametrization(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -589,7 +555,6 @@ class Model(GenericModel, Generic[AT, BT]): ] -@skip_36 def test_multiple_specification(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -610,7 +575,6 @@ class Model(GenericModel, Generic[AT, BT]): ] -@skip_36 def test_generic_subclass_of_concrete_generic(): T = TypeVar('T') U = TypeVar('U') @@ -632,7 +596,6 @@ class GenericSub(GenericBaseModel[int], Generic[U]): ConcreteSub(data=2, extra=3) -@skip_36 def test_generic_model_pickle(create_module): # Using create_module because pickle doesn't support # objects with in their __qualname__ (e. g. defined in function) @@ -661,7 +624,6 @@ class MyGeneric(GenericModel, Generic[t]): assert loaded == original -@skip_36 def test_generic_model_from_function_pickle_fail(create_module): @create_module def module(): @@ -690,7 +652,6 @@ def get_generic(t): pickle.dumps(original) -@skip_36 def test_generic_model_redefined_without_cache_fail(create_module, monkeypatch): # match identity checker otherwise we never get to the redefinition check @@ -772,7 +733,6 @@ def test_get_caller_frame_info_when_sys_getframe_undefined(): sys._getframe = getframe -@skip_36 def test_iter_contained_typevars(): T = TypeVar('T') T2 = TypeVar('T2') @@ -786,7 +746,6 @@ class Model(GenericModel, Generic[T]): assert list(iter_contained_typevars(Optional[List[Union[str, Model[T], Callable[[T2, T], str]]]])) == [T, T2, T] -@skip_36 def test_nested_identity_parameterization(): T = TypeVar('T') T2 = TypeVar('T2') @@ -799,7 +758,6 @@ class Model(GenericModel, Generic[T]): assert Model[T2] is not Model -@skip_36 def test_replace_types(): T = TypeVar('T') @@ -823,7 +781,6 @@ class Model(GenericModel, Generic[T]): assert replace_types(list[Union[str, list, T]], {T: int}) == list[Union[str, list, int]] -@skip_36 def test_replace_types_with_user_defined_generic_type_field(): """Test that using user defined generic types as generic model fields are handled correctly.""" @@ -847,7 +804,6 @@ class Model(GenericModel, Generic[T, KT, VT]): assert replace_types(Model[T, VT, KT], {T: bool, KT: str, VT: int}) == Model[T, VT, KT][bool, int, str] -@skip_36 def test_replace_types_identity_on_unchanged(): T = TypeVar('T') U = TypeVar('U') @@ -856,7 +812,6 @@ def test_replace_types_identity_on_unchanged(): assert replace_types(type_, {T: int}) is type_ -@skip_36 def test_deep_generic(): T = TypeVar('T') S = TypeVar('S') @@ -888,7 +843,6 @@ class NormalModel(BaseModel): assert inner_model.__concrete__ is True -@skip_36 def test_deep_generic_with_inner_typevar(): T = TypeVar('T') @@ -906,7 +860,6 @@ class InnerModel(OuterModel[T], Generic[T]): assert InnerModel[int](a=['1']).a == [1] -@skip_36 def test_deep_generic_with_referenced_generic(): T = TypeVar('T') R = TypeVar('R') @@ -928,7 +881,6 @@ class InnerModel(OuterModel[T], Generic[T]): assert InnerModel[int](a={'a': 1}).a.a == 1 -@skip_36 def test_deep_generic_with_referenced_inner_generic(): T = TypeVar('T') @@ -952,7 +904,6 @@ class InnerModel(OuterModel[T], Generic[T]): assert (InnerModel[int].__fields__['a'].sub_fields[0].sub_fields[0].outer_type_.__fields__['a'].outer_type_) == int -@skip_36 def test_deep_generic_with_multiple_typevars(): T = TypeVar('T') U = TypeVar('U') @@ -970,7 +921,6 @@ class InnerModel(OuterModel[T], Generic[U, T]): assert ConcreteInnerModel(data=['1'], extra='2').dict() == {'data': [1.0], 'extra': 2} -@skip_36 def test_deep_generic_with_multiple_inheritance(): K = TypeVar('K') V = TypeVar('V') @@ -998,7 +948,6 @@ class InnerModel(OuterModelA[K, V], OuterModelB[T], Generic[K, V, T]): } -@skip_36 def test_generic_with_referenced_generic_type_1(): T = TypeVar('T') @@ -1013,7 +962,6 @@ class ReferenceModel(GenericModel, Generic[T]): ReferenceModel[int] -@skip_36 def test_generic_with_referenced_nested_typevar(): T = TypeVar('T') @@ -1029,7 +977,6 @@ class ReferenceModel(GenericModel, Generic[T]): ReferenceModel[int] -@skip_36 def test_generic_with_callable(): T = TypeVar('T') @@ -1041,7 +988,6 @@ class Model(GenericModel, Generic[T]): Model.__concrete__ is False -@skip_36 def test_generic_with_partial_callable(): T = TypeVar('T') U = TypeVar('U') @@ -1057,7 +1003,6 @@ class Model(GenericModel, Generic[T, U]): Model[str, int].__concrete__ is False -@skip_36 def test_generic_recursive_models(create_module): @create_module def module(): @@ -1081,7 +1026,6 @@ class Model2(GenericModel, Generic[T]): assert result == Model1(ref=Model2(ref=Model1(ref=Model2(ref='123')))) -@skip_36 def test_generic_enum(): T = TypeVar('T') @@ -1099,7 +1043,6 @@ class MyModel(BaseModel): assert m.my_gen.some_field is SomeStringEnum.A -@skip_36 def test_generic_literal(): FieldType = TypeVar('FieldType') ValueType = TypeVar('ValueType') @@ -1112,7 +1055,6 @@ class GModel(GenericModel, Generic[FieldType, ValueType]): assert m.dict() == {'field': {'foo': 'x'}} -@skip_36 def test_generic_enums(): T = TypeVar('T') @@ -1132,7 +1074,6 @@ class Model(BaseModel): assert set(Model.schema()['definitions']) == {'EnumA', 'EnumB', 'GModel_EnumA_', 'GModel_EnumB_'} -@skip_36 def test_generic_with_user_defined_generic_field(): T = TypeVar('T') @@ -1150,7 +1091,6 @@ class Model(GenericModel, Generic[T]): model = Model[int](field=['a']) -@skip_36 def test_generic_annotated(): T = TypeVar('T') @@ -1160,7 +1100,6 @@ class SomeGenericModel(GenericModel, Generic[T]): SomeGenericModel[str](the_alias='qwe') -@skip_36 def test_generic_subclass(): T = TypeVar('T') @@ -1176,7 +1115,6 @@ class B(A[T], Generic[T]): assert not issubclass(B[int], A[str]) -@skip_36 def test_generic_subclass_with_partial_application(): T = TypeVar('T') S = TypeVar('S') @@ -1193,7 +1131,6 @@ class B(A[S], Generic[T, S]): assert not issubclass(PartiallyAppliedB[str], A[int]) -@skip_36 def test_multilevel_generic_binding(): T = TypeVar('T') S = TypeVar('S') @@ -1209,7 +1146,6 @@ class B(A[str, T], Generic[T]): assert not issubclass(B[str], A[str, int]) -@skip_36 def test_generic_subclass_with_extra_type(): T = TypeVar('T') S = TypeVar('S') @@ -1226,7 +1162,6 @@ class B(A[S], Generic[T, S]): assert not issubclass(B[int, str], A[int]) -@skip_36 def test_multi_inheritance_generic_binding(): T = TypeVar('T') @@ -1246,7 +1181,6 @@ class C(B[str], Generic[T]): assert not issubclass(C[float], A[str]) -@skip_36 def test_parse_generic_json(): T = TypeVar('T') diff --git a/tests/test_main.py b/tests/test_main.py index 71da72544b2..642b5610b3a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1867,7 +1867,6 @@ class Outer(BaseModel): assert repr(parsed) == 'Outer(inner_1=Inner(val=0), inner_2=Inner(val=0))' -@pytest.mark.skipif(sys.version_info < (3, 7), reason='field constraints are set but not enforced with python 3.6') def test_none_min_max_items(): # None default class Foo(BaseModel): @@ -2039,7 +2038,6 @@ class Model(BaseModel): assert repr(m) == "Model(x={'one': 1, 'two': 2})" -@pytest.mark.skipif(sys.version_info < (3, 7), reason='generic classes need 3.7') def test_typing_non_coercion_of_dict_subclasses(): KT = TypeVar('KT') VT = TypeVar('VT') diff --git a/tests/test_private_attributes.py b/tests/test_private_attributes.py index 0d3a0978efe..5655ef72fba 100644 --- a/tests/test_private_attributes.py +++ b/tests/test_private_attributes.py @@ -1,4 +1,3 @@ -import sys from typing import ClassVar, Generic, TypeVar import pytest @@ -7,8 +6,6 @@ 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(): default = {'a': {}} @@ -186,7 +183,6 @@ class Config: assert m._private_attr == 123 -@skip_36 def test_generic_private_attribute(): T = TypeVar('T') diff --git a/tests/test_schema.py b/tests/test_schema.py index eb1f081e828..ea0290f09e1 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -2316,9 +2316,6 @@ class Model(BaseModel): } -@pytest.mark.skipif( - sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' -) def test_schema_for_generic_field(): T = TypeVar('T') @@ -2395,9 +2392,6 @@ class LocationBase(BaseModel): } -@pytest.mark.skipif( - sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' -) def test_advanced_generic_schema(): T = TypeVar('T') K = TypeVar('K') @@ -2508,9 +2502,6 @@ class Model(BaseModel): } -@pytest.mark.skipif( - sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' -) def test_nested_generic(): """ Test a nested BaseModel that is also a Generic @@ -2545,9 +2536,6 @@ class Model(BaseModel): } -@pytest.mark.skipif( - sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' -) def test_nested_generic_model(): """ Test a nested GenericModel @@ -2576,9 +2564,6 @@ class Model(BaseModel): } -@pytest.mark.skipif( - sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' -) def test_complex_nested_generic(): """ Handle a union of a generic. diff --git a/tests/test_types.py b/tests/test_types.py index bbf4c23b1e4..a7b7db89f0a 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -2459,8 +2459,7 @@ class Foobar(BaseModel): pattern: Pattern f = Foobar(pattern=r'^whatev.r\d$') - # SRE_Pattern for 3.6, Pattern for 3.7 - assert f.pattern.__class__.__name__ in {'SRE_Pattern', 'Pattern'} + assert f.pattern.__class__.__name__ == 'Pattern' # check it's really a proper pattern assert f.pattern.match('whatever1') assert not f.pattern.match(' whatever1') @@ -2991,13 +2990,10 @@ class DefaultModel(BaseModel): assert DefaultModel(v=1).dict() == {'v': 1} assert DefaultModel(v='1').dict() == {'v': 1} - # In 3.6, Union[int, bool, str] == Union[int, str] - allowed_json_types = ('integer', 'string') if sys.version_info[:2] == (3, 6) else ('integer', 'boolean', 'string') - assert DefaultModel.schema() == { 'title': 'DefaultModel', 'type': 'object', - 'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in allowed_json_types]}}, + 'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in ('integer', 'boolean', 'string')]}}, 'required': ['v'], } @@ -3013,13 +3009,10 @@ class Config: assert SmartModel(v=True).dict() == {'v': True} assert SmartModel(v='1').dict() == {'v': '1'} - # In 3.6, Union[int, bool, str] == Union[int, str] - allowed_json_types = ('integer', 'string') if sys.version_info[:2] == (3, 6) else ('integer', 'boolean', 'string') - assert SmartModel.schema() == { 'title': 'SmartModel', 'type': 'object', - 'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in allowed_json_types]}}, + 'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in ('integer', 'boolean', 'string')]}}, 'required': ['v'], } diff --git a/tests/test_utils.py b/tests/test_utils.py index 9c0ab4fb942..132081a7052 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -463,7 +463,6 @@ def test_smart_deepcopy_collection(collection, mocker): T = TypeVar('T') -@pytest.mark.skipif(sys.version_info < (3, 7), reason='get_origin is only consistent for python >= 3.7') @pytest.mark.parametrize( 'input_value,output_value', [ From 9d631a3429a66f30742c1a52c94ac18ec6ba848d Mon Sep 17 00:00:00 2001 From: Eric Jolibois Date: Mon, 17 Jan 2022 22:30:39 +0100 Subject: [PATCH 02/62] fix(ci): update fastapi tests (#3690) * use python 3.10 to run fastapi tests * fix fastapi test call --- .github/workflows/ci.yml | 2 +- tests/test_fastapi.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7760d88fd3..cc81d4bfc6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -244,7 +244,7 @@ jobs: - name: set up python uses: actions/setup-python@v2 with: - python-version: '3.7' + python-version: '3.10' - name: install run: make install-testing diff --git a/tests/test_fastapi.sh b/tests/test_fastapi.sh index cfaca2c5859..ff5a3e3ad84 100755 --- a/tests/test_fastapi.sh +++ b/tests/test_fastapi.sh @@ -12,4 +12,4 @@ git checkout "${latest_tag}" pip install -U flit flit install -PYTHONPATH=./docs/src pytest +./scripts/test.sh From 2c980eaf607894068d34a5901fb11a62f868457f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 22:33:04 +0000 Subject: [PATCH 03/62] build(deps): bump actions/setup-python from 2 to 3 (#3868) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 2 to 3. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/setup-python dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 20 ++++++++++---------- .github/workflows/upload-previews.yml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc81d4bfc6d..51dad3909fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - uses: actions/checkout@v2 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: 3.9 @@ -43,7 +43,7 @@ jobs: - uses: actions/checkout@v2 - name: set up python - uses: actions/setup-python@v1 + uses: actions/setup-python@v3 with: python-version: 3.8 @@ -74,7 +74,7 @@ jobs: - uses: actions/checkout@v2 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} @@ -143,7 +143,7 @@ jobs: - uses: actions/checkout@v2 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python-version }} @@ -176,7 +176,7 @@ jobs: - uses: actions/checkout@v2 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.10' @@ -212,7 +212,7 @@ jobs: steps: - uses: actions/checkout@v2 - - uses: actions/setup-python@v1 + - uses: actions/setup-python@v3 with: python-version: '3.8' @@ -242,7 +242,7 @@ jobs: - uses: actions/checkout@v2 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.10' @@ -262,7 +262,7 @@ jobs: - uses: actions/checkout@v2 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.8' @@ -295,7 +295,7 @@ jobs: - uses: actions/checkout@v2 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.8' @@ -346,7 +346,7 @@ jobs: - uses: actions/checkout@v2 - name: set up python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: '3.8' diff --git a/.github/workflows/upload-previews.yml b/.github/workflows/upload-previews.yml index 5325c4939db..0bec37843b5 100644 --- a/.github/workflows/upload-previews.yml +++ b/.github/workflows/upload-previews.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v1 + - uses: actions/setup-python@v3 with: python-version: '3.8' From 2b0d4d90eb0ee8672029950e493c235714713e72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 22:33:43 +0000 Subject: [PATCH 04/62] build(deps): bump actions/checkout from 2 to 3 (#3869) Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51dad3909fa..ab71fec76cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v3 @@ -40,7 +40,7 @@ jobs: docs-build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v3 @@ -71,7 +71,7 @@ jobs: OS: ubuntu steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v3 @@ -140,7 +140,7 @@ jobs: runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v3 @@ -173,7 +173,7 @@ jobs: mypy-version: ['0.910', '0.920', '0.921'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v3 @@ -210,7 +210,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: @@ -239,7 +239,7 @@ jobs: name: test fastAPI runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v3 @@ -259,7 +259,7 @@ jobs: BENCHMARK_REPEATS: 1 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v3 @@ -292,7 +292,7 @@ jobs: runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v3 @@ -343,7 +343,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set up python uses: actions/setup-python@v3 From 1fb438bc0bf8a75eece1490a34e53cf6057ed489 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 22:34:35 +0000 Subject: [PATCH 05/62] build(deps): bump mkdocs-material from 8.1.3 to 8.2.3 (#3865) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.1.3 to 8.2.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.1.3...8.2.3) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 88f06ea3f75..8fdf9268b2a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,7 +6,7 @@ markdown-include==0.6.0 mdx-truly-sane-lists==1.2 mkdocs==1.2.3 mkdocs-exclude==1.0.2 -mkdocs-material==8.1.3 +mkdocs-material==8.2.3 sqlalchemy orjson ujson From b90e128175c1391d92f0691659230387e863835c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 22:35:04 +0000 Subject: [PATCH 06/62] build(deps): bump coverage from 6.2 to 6.3.2 (#3839) Bumps [coverage](https://github.com/nedbat/coveragepy) from 6.2 to 6.3.2. - [Release notes](https://github.com/nedbat/coveragepy/releases) - [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst) - [Commits](https://github.com/nedbat/coveragepy/compare/6.2...6.3.2) --- updated-dependencies: - dependency-name: coverage dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index 1ee135251bb..fc2158a06ac 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -1,4 +1,4 @@ -coverage==6.2 +coverage==6.3.2 hypothesis==6.31.6 # 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" From f3f2408e2d43130c122c30994e5c996fea6f0fb7 Mon Sep 17 00:00:00 2001 From: Gin Cui Date: Tue, 1 Mar 2022 17:37:09 -0500 Subject: [PATCH 07/62] Fixed a typo in decimal_encoder's doc. (#3820) --- pydantic/json.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic/json.py b/pydantic/json.py index b732cc0a2a7..0769228e416 100644 --- a/pydantic/json.py +++ b/pydantic/json.py @@ -26,7 +26,7 @@ def decimal_encoder(dec_value: Decimal) -> Union[int, float]: This is useful when we use ConstrainedDecimal to represent Numeric(x,0) where a integer (but not int typed) is used. Encoding this as a float - results in failed round-tripping between encode and prase. + results in failed round-tripping between encode and parse. Our Id type is a prime example of this. >>> decimal_encoder(Decimal("1.0")) From 9967f0ebe3ff7c543ce8254c46cf32b5ddfb4b2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 22:37:59 +0000 Subject: [PATCH 08/62] build(deps): bump pre-commit from 2.16.0 to 2.17.0 (#3731) Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.16.0 to 2.17.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/master/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.16.0...v2.17.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/requirements-linting.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index 9a3f0464b33..7c16057f720 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -4,7 +4,7 @@ flake8-quotes==3.3.1 hypothesis==6.31.6 isort==5.10.1 mypy==0.930 -pre-commit==2.16.0 +pre-commit==2.17.0 pycodestyle==2.8.0 pyflakes==2.4.0 twine==3.7.1 From d7a8272d7e0c151b0bd43df596be02e0d436ebdf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Mar 2022 23:30:57 +0000 Subject: [PATCH 09/62] build(deps): bump mypy from 0.930 to 0.931 (#3656) Bumps [mypy](https://github.com/python/mypy) from 0.930 to 0.931. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.930...v0.931) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/requirements-linting.txt | 2 +- tests/requirements-testing.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index 7c16057f720..ea4b168a3f9 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -3,7 +3,7 @@ flake8==4.0.1 flake8-quotes==3.3.1 hypothesis==6.31.6 isort==5.10.1 -mypy==0.930 +mypy==0.931 pre-commit==2.17.0 pycodestyle==2.8.0 pyflakes==2.4.0 diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index fc2158a06ac..2e60bcf128b 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -2,7 +2,7 @@ coverage==6.3.2 hypothesis==6.31.6 # 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.930 +mypy==0.931 pytest==6.2.5 pytest-cov==3.0.0 pytest-mock==3.6.1 From 90628e1c2b8d59fd71dd77ccdd16a5262a7ac64c Mon Sep 17 00:00:00 2001 From: Guilhem C Date: Fri, 1 Apr 2022 20:47:05 +0200 Subject: [PATCH 10/62] docs: fix typo in settings management page (#3781) --- docs/usage/settings.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/settings.md b/docs/usage/settings.md index 1de49c0a583..3ab58213909 100644 --- a/docs/usage/settings.md +++ b/docs/usage/settings.md @@ -71,7 +71,7 @@ by treating the environment variable's value as a JSON-encoded string. Another way to populate nested complex variables is to configure your model with the `env_nested_delimiter` config setting, then use an env variable with a name pointing to the nested module fields. -What it does is simply explodes yor variable into nested models or dicts. +What it does is simply explodes your variable into nested models or dicts. So if you define a variable `FOO__BAR__BAZ=123` it will convert it into `FOO={'BAR': {'BAZ': 123}}` If you have multiple variables with the same structure they will be merged. From bf5fdfc6183921fc89fb84f84a48199689f9e53e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 08:35:55 +0100 Subject: [PATCH 11/62] build(deps): bump black from 21.12b0 to 22.3.0 (#3950) * build(deps): bump black from 21.12b0 to 22.3.0 Bumps [black](https://github.com/psf/black) from 21.12b0 to 22.3.0. - [Release notes](https://github.com/psf/black/releases) - [Changelog](https://github.com/psf/black/blob/main/CHANGES.md) - [Commits](https://github.com/psf/black/commits/22.3.0) --- updated-dependencies: - dependency-name: black dependency-type: direct:production ... Signed-off-by: dependabot[bot] * apply new black styles, fix docs * try upgrading pip before fastapi tests Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Samuel Colvin --- .github/workflows/ci.yml | 3 +++ docs/requirements.txt | 4 ++-- pydantic/networks.py | 4 ++-- pydantic/types.py | 24 ++++++++++++------------ tests/requirements-linting.txt | 2 +- tests/test_construction.py | 4 ++-- tests/test_main.py | 4 ++-- tests/test_networks_ipaddress.py | 30 +++++++++++++++--------------- tests/test_schema.py | 2 +- tests/test_types.py | 8 ++++---- 10 files changed, 44 insertions(+), 41 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab71fec76cd..595c863d2a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -246,6 +246,9 @@ jobs: with: python-version: '3.10' + - name: upgrade pip + run: pip install -U pip setuptools wheel + - name: install run: make install-testing diff --git a/docs/requirements.txt b/docs/requirements.txt index 8fdf9268b2a..ecd399cc53e 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,9 +4,9 @@ flake8-quotes==3.3.1 hypothesis==6.31.6 markdown-include==0.6.0 mdx-truly-sane-lists==1.2 -mkdocs==1.2.3 +mkdocs==1.3.0 mkdocs-exclude==1.0.2 -mkdocs-material==8.2.3 +mkdocs-material==8.2.8 sqlalchemy orjson ujson diff --git a/pydantic/networks.py b/pydantic/networks.py index 18a042ca979..50a01d42bb5 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -122,7 +122,7 @@ def int_domain_regex() -> Pattern[str]: class AnyUrl(str): strip_whitespace = True min_length = 1 - max_length = 2 ** 16 + max_length = 2**16 allowed_schemes: Optional[Collection[str]] = None tld_required: bool = False user_required: bool = False @@ -386,7 +386,7 @@ def stricturl( *, strip_whitespace: bool = True, min_length: int = 1, - max_length: int = 2 ** 16, + max_length: int = 2**16, tld_required: bool = True, host_required: bool = True, allowed_schemes: Optional[Collection[str]] = None, diff --git a/pydantic/types.py b/pydantic/types.py index e3d6c1277dd..2d0cc18f8d7 100644 --- a/pydantic/types.py +++ b/pydantic/types.py @@ -1010,18 +1010,18 @@ def _get_brand(card_number: str) -> PaymentCardBrand: BYTE_SIZES = { 'b': 1, - 'kb': 10 ** 3, - 'mb': 10 ** 6, - 'gb': 10 ** 9, - 'tb': 10 ** 12, - 'pb': 10 ** 15, - 'eb': 10 ** 18, - 'kib': 2 ** 10, - 'mib': 2 ** 20, - 'gib': 2 ** 30, - 'tib': 2 ** 40, - 'pib': 2 ** 50, - 'eib': 2 ** 60, + 'kb': 10**3, + 'mb': 10**6, + 'gb': 10**9, + 'tb': 10**12, + 'pb': 10**15, + 'eb': 10**18, + 'kib': 2**10, + 'mib': 2**20, + 'gib': 2**30, + 'tib': 2**40, + 'pib': 2**50, + 'eib': 2**60, } BYTE_SIZES.update({k.lower()[0]: v for k, v in BYTE_SIZES.items() if 'i' not in k}) byte_string_re = re.compile(r'^\s*(\d*\.?\d+)\s*(\w+)?', re.IGNORECASE) diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index ea4b168a3f9..2f12ae948e5 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -1,4 +1,4 @@ -black==21.12b0 +black==22.3.0 flake8==4.0.1 flake8-quotes==3.3.1 hypothesis==6.31.6 diff --git a/tests/test_construction.py b/tests/test_construction.py index 19e912b398e..e7b12a0a76d 100644 --- a/tests/test_construction.py +++ b/tests/test_construction.py @@ -63,8 +63,8 @@ class Model(BaseModel): a: bytes b: str - content_bytes = b'x' * (2 ** 16 + 1) - content_str = 'x' * (2 ** 16 + 1) + content_bytes = b'x' * (2**16 + 1) + content_str = 'x' * (2**16 + 1) m = Model(a=content_bytes, b=content_str) assert m.a == content_bytes assert m.b == content_str diff --git a/tests/test_main.py b/tests/test_main.py index 642b5610b3a..c00d70688cb 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -433,13 +433,13 @@ class Foo(BaseModel): x: int def __hash__(self): - return self.x ** 2 + return self.x**2 class Bar(Foo): y: int def __hash__(self): - return self.y ** 3 + return self.y**3 class Buz(Bar): z: int diff --git a/tests/test_networks_ipaddress.py b/tests/test_networks_ipaddress.py index cd1351b5928..c73c512307d 100644 --- a/tests/test_networks_ipaddress.py +++ b/tests/test_networks_ipaddress.py @@ -114,7 +114,7 @@ class Model(BaseModel): [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 address', 'type': 'value_error.ipvanyaddress'}], ), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 address', 'type': 'value_error.ipvanyaddress'}], ), ], @@ -141,7 +141,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}]), ( - 2 ** 32 + 1, + 2**32 + 1, [{'loc': ('ipv4',), 'msg': 'value is not a valid IPv4 address', 'type': 'value_error.ipv4address'}], ), ( @@ -172,7 +172,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}]), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ipv6',), 'msg': 'value is not a valid IPv6 address', 'type': 'value_error.ipv6address'}], ), ( @@ -203,7 +203,7 @@ class Model(BaseModel): ('192.168.0.0/24', IPv4Network), ('192.168.128.0/30', IPv4Network), ('2001:db00::0/120', IPv6Network), - (2 ** 32 - 1, IPv4Network), # no mask equals to mask /32 + (2**32 - 1, IPv4Network), # no mask equals to mask /32 (20_282_409_603_651_670_423_947_251_286_015, IPv6Network), # /128 (b'\xff\xff\xff\xff', IPv4Network), # /32 (b'\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff', IPv6Network), @@ -224,7 +224,7 @@ class Model(BaseModel): [ ('192.168.0.0/24', IPv4Network), ('192.168.128.0/30', IPv4Network), - (2 ** 32 - 1, IPv4Network), # no mask equals to mask /32 + (2**32 - 1, IPv4Network), # no mask equals to mask /32 (b'\xff\xff\xff\xff', IPv4Network), # /32 (('192.168.0.0', 24), IPv4Network), (IPv4Network('192.168.0.0/24'), IPv4Network), @@ -270,7 +270,7 @@ class Model(BaseModel): [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 network', 'type': 'value_error.ipvanynetwork'}], ), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 or IPv6 network', 'type': 'value_error.ipvanynetwork'}], ), ], @@ -297,7 +297,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}]), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 network', 'type': 'value_error.ipv4network'}], ), ( @@ -328,7 +328,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}]), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 network', 'type': 'value_error.ipv6network'}], ), ( @@ -362,8 +362,8 @@ class Model(BaseModel): ('192.168.128.1/30', IPv4Interface), ('2001:db00::0/120', IPv6Interface), ('2001:db00::1/120', IPv6Interface), - (2 ** 32 - 1, IPv4Interface), # no mask equals to mask /32 - (2 ** 32 - 1, IPv4Interface), # so ``strict`` has no effect + (2**32 - 1, IPv4Interface), # no mask equals to mask /32 + (2**32 - 1, IPv4Interface), # so ``strict`` has no effect (20_282_409_603_651_670_423_947_251_286_015, IPv6Interface), # /128 (20_282_409_603_651_670_423_947_251_286_014, IPv6Interface), (b'\xff\xff\xff\xff', IPv4Interface), # /32 @@ -394,8 +394,8 @@ class Model(BaseModel): ('192.168.0.1/24', IPv4Interface), ('192.168.128.0/30', IPv4Interface), ('192.168.128.1/30', IPv4Interface), - (2 ** 32 - 1, IPv4Interface), # no mask equals to mask /32 - (2 ** 32 - 1, IPv4Interface), # so ``strict`` has no effect + (2**32 - 1, IPv4Interface), # no mask equals to mask /32 + (2**32 - 1, IPv4Interface), # so ``strict`` has no effect (b'\xff\xff\xff\xff', IPv4Interface), # /32 (b'\xff\xff\xff\xff', IPv4Interface), (('192.168.0.0', 24), IPv4Interface), @@ -467,7 +467,7 @@ class Model(BaseModel): ], ), ( - 2 ** 128 + 1, + 2**128 + 1, [ { 'loc': ('ip',), @@ -500,7 +500,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 interface', 'type': 'value_error.ipv4interface'}]), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv4 interface', 'type': 'value_error.ipv4interface'}], ), ], @@ -527,7 +527,7 @@ class Model(BaseModel): ), (-1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 interface', 'type': 'value_error.ipv6interface'}]), ( - 2 ** 128 + 1, + 2**128 + 1, [{'loc': ('ip',), 'msg': 'value is not a valid IPv6 interface', 'type': 'value_error.ipv6interface'}], ), ], diff --git a/tests/test_schema.py b/tests/test_schema.py index ea0290f09e1..83888310923 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -799,7 +799,7 @@ class Model(BaseModel): @pytest.mark.parametrize( 'field_type,expected_schema', [ - (AnyUrl, {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2 ** 16}), + (AnyUrl, {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 1, 'maxLength': 2**16}), ( stricturl(min_length=5, max_length=10), {'title': 'A', 'type': 'string', 'format': 'uri', 'minLength': 5, 'maxLength': 10}, diff --git a/tests/test_types.py b/tests/test_types.py index a7b7db89f0a..8e5481719c2 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1144,7 +1144,7 @@ class Model(BaseModel): ([1, 2, '3'], [1, 2, '3']), ((1, 2, '3'), [1, 2, '3']), ({1, 2, '3'}, list({1, 2, '3'})), - ((i ** 2 for i in range(5)), [0, 1, 4, 9, 16]), + ((i**2 for i in range(5)), [0, 1, 4, 9, 16]), ((deque((1, 2, 3)), list(deque((1, 2, 3))))), ), ) @@ -1184,7 +1184,7 @@ class Model(BaseModel): ([1, 2, '3'], (1, 2, '3')), ((1, 2, '3'), (1, 2, '3')), ({1, 2, '3'}, tuple({1, 2, '3'})), - ((i ** 2 for i in range(5)), (0, 1, 4, 9, 16)), + ((i**2 for i in range(5)), (0, 1, 4, 9, 16)), (deque([1, 2, 3]), (1, 2, 3)), ), ) @@ -1210,7 +1210,7 @@ class Model(BaseModel): ( ([1, 2, '3'], int, (1, 2, 3)), ((1, 2, '3'), int, (1, 2, 3)), - ((i ** 2 for i in range(5)), int, (0, 1, 4, 9, 16)), + ((i**2 for i in range(5)), int, (0, 1, 4, 9, 16)), (('a', 'b', 'c'), str, ('a', 'b', 'c')), ), ) @@ -1250,7 +1250,7 @@ class Model(BaseModel): ({1, 2, 2, '3'}, {1, 2, '3'}), ((1, 2, 2, '3'), {1, 2, '3'}), ([1, 2, 2, '3'], {1, 2, '3'}), - ({i ** 2 for i in range(5)}, {0, 1, 4, 9, 16}), + ({i**2 for i in range(5)}, {0, 1, 4, 9, 16}), ), ) def test_set_success(value, result): From 29b8052e4edb988612a2ffda149e118ea26b26e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 08:56:28 +0100 Subject: [PATCH 12/62] build(deps): bump python-dotenv from 0.19.2 to 0.20.0 (#3963) Bumps [python-dotenv](https://github.com/theskumar/python-dotenv) from 0.19.2 to 0.20.0. - [Release notes](https://github.com/theskumar/python-dotenv/releases) - [Changelog](https://github.com/theskumar/python-dotenv/blob/master/CHANGELOG.md) - [Commits](https://github.com/theskumar/python-dotenv/compare/v0.19.2...v0.20.0) --- updated-dependencies: - dependency-name: python-dotenv dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 95aef085232..3a10caab46d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ devtools==0.8.0 email-validator==1.1.3 dataclasses==0.6; python_version < '3.7' typing-extensions==4.0.1 -python-dotenv==0.19.2 +python-dotenv==0.20.0 From 19064138e465c0c6365a89cde0107d8e50872321 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 08:56:39 +0100 Subject: [PATCH 13/62] build(deps): bump hypothesis from 6.31.6 to 6.41.0 (#3964) Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.31.6 to 6.41.0. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.31.6...hypothesis-python-6.41.0) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- tests/requirements-linting.txt | 2 +- tests/requirements-testing.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index ecd399cc53e..8c28433d5f9 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ ansi2html==1.6.0 flake8==4.0.1 flake8-quotes==3.3.1 -hypothesis==6.31.6 +hypothesis==6.41.0 markdown-include==0.6.0 mdx-truly-sane-lists==1.2 mkdocs==1.3.0 diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index 2f12ae948e5..e1e9a81dd32 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -1,7 +1,7 @@ black==22.3.0 flake8==4.0.1 flake8-quotes==3.3.1 -hypothesis==6.31.6 +hypothesis==6.41.0 isort==5.10.1 mypy==0.931 pre-commit==2.17.0 diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index 2e60bcf128b..a07b20e0b13 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -1,5 +1,5 @@ coverage==6.3.2 -hypothesis==6.31.6 +hypothesis==6.41.0 # 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.931 From d8054380d3a096cb41b12c1eb5e3b0c04664025e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 08:57:08 +0100 Subject: [PATCH 14/62] build(deps): bump pytest from 6.2.5 to 7.1.1 (#3926) Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 7.1.1. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.5...7.1.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index a07b20e0b13..a222e4f9159 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -3,7 +3,7 @@ hypothesis==6.41.0 # 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.931 -pytest==6.2.5 +pytest==7.1.1 pytest-cov==3.0.0 pytest-mock==3.6.1 pytest-sugar==0.9.4 From c12382d00fc32b9d5a90b2b8d3a0887d3a893198 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 08:57:22 +0100 Subject: [PATCH 15/62] build(deps): bump twine from 3.7.1 to 4.0.0 (#3965) Bumps [twine](https://github.com/pypa/twine) from 3.7.1 to 4.0.0. - [Release notes](https://github.com/pypa/twine/releases) - [Changelog](https://github.com/pypa/twine/blob/main/docs/changelog.rst) - [Commits](https://github.com/pypa/twine/compare/3.7.1...4.0.0) --- updated-dependencies: - dependency-name: twine dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/requirements-linting.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index e1e9a81dd32..69487560e87 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -7,4 +7,4 @@ mypy==0.931 pre-commit==2.17.0 pycodestyle==2.8.0 pyflakes==2.4.0 -twine==3.7.1 +twine==4.0.0 From 9d29aabb9c17efee0c2239416c30fc4862d659fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 13:25:43 +0100 Subject: [PATCH 16/62] build(deps): bump cython from 0.29.26 to 0.29.28 (#3871) Bumps [cython](https://github.com/cython/cython) from 0.29.26 to 0.29.28. - [Release notes](https://github.com/cython/cython/releases) - [Changelog](https://github.com/cython/cython/blob/master/CHANGES.rst) - [Commits](https://github.com/cython/cython/compare/0.29.26...0.29.28) --- updated-dependencies: - dependency-name: cython dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3a10caab46d..110cd8e1a4d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # requirements for compilation and from setup.py so dependabot prompts us to test with latest version of these packages -Cython==0.29.26;sys_platform!='win32' +Cython==0.29.28;sys_platform!='win32' devtools==0.8.0 email-validator==1.1.3 dataclasses==0.6; python_version < '3.7' From b785f87aea565b85d7a065d7ea57a7f9eea38844 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 13:25:51 +0100 Subject: [PATCH 17/62] build(deps): bump typing-extensions from 4.0.1 to 4.1.1 (#3874) Bumps [typing-extensions](https://github.com/python/typing) from 4.0.1 to 4.1.1. - [Release notes](https://github.com/python/typing/releases) - [Changelog](https://github.com/python/typing/blob/master/typing_extensions/CHANGELOG) - [Commits](https://github.com/python/typing/compare/4.0.1...4.1.1) --- updated-dependencies: - dependency-name: typing-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 110cd8e1a4d..3beeb0fdbf4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ Cython==0.29.28;sys_platform!='win32' devtools==0.8.0 email-validator==1.1.3 dataclasses==0.6; python_version < '3.7' -typing-extensions==4.0.1 +typing-extensions==4.1.1 python-dotenv==0.20.0 From 7c05a61e4af612990893544fe895f2547c90f58f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 13:25:58 +0100 Subject: [PATCH 18/62] build(deps): bump pytest-mock from 3.6.1 to 3.7.0 (#3967) Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.6.1 to 3.7.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.6.1...v3.7.0) --- updated-dependencies: - dependency-name: pytest-mock dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index a222e4f9159..e4f808bf8a9 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -5,5 +5,5 @@ importlib-metadata==3.1.0;python_version<"3.8" mypy==0.931 pytest==7.1.1 pytest-cov==3.0.0 -pytest-mock==3.6.1 +pytest-mock==3.7.0 pytest-sugar==0.9.4 From df8b2e100f51d67a915e95621a7e6ea1aa2afb82 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 2 Apr 2022 13:44:30 +0100 Subject: [PATCH 19/62] Combine dependabot prs (#3969) * allow combining of dependabot PRs * add combine-dependabot.yml --- .github/dependabot.yml | 2 +- .github/workflows/combine-dependabot.yml | 137 +++++++++++++++++++++++ .gitignore | 5 +- 3 files changed, 139 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/combine-dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 3acffa71cec..7a42e082063 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ updates: - package-ecosystem: pip directory: / schedule: - interval: weekly + interval: monthly - package-ecosystem: github-actions directory: / diff --git a/.github/workflows/combine-dependabot.yml b/.github/workflows/combine-dependabot.yml new file mode 100644 index 00000000000..1b141a75f0b --- /dev/null +++ b/.github/workflows/combine-dependabot.yml @@ -0,0 +1,137 @@ +# from https://github.com/hrvey/combine-prs-workflow/blob/master/combine-prs.yml +name: 'Combine Dependabot PRs' + +on: + workflow_dispatch: + inputs: + branchPrefix: + description: 'Branch prefix to find combinable PRs based on' + required: true + default: 'dependabot/' + mustBeGreen: + description: 'Only combine PRs that are green' + required: true + default: true + combineBranchName: + description: 'Name of the branch to combine PRs into' + required: true + default: 'combine-dependabot-bumps' + ignoreLabel: + description: 'Exclude PRs with this label' + required: true + default: 'nocombine' + +jobs: + combine-prs: + runs-on: ubuntu-latest + + steps: + - uses: actions/github-script@v6 + id: fetch-branch-names + name: Fetch branch names + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const pulls = await github.paginate('GET /repos/:owner/:repo/pulls', { + owner: context.repo.owner, + repo: context.repo.repo + }); + branches = []; + prs = []; + base_branch = null; + for (const pull of pulls) { + const branch = pull['head']['ref']; + console.log('Pull for branch: ' + branch); + if (branch.startsWith('${{ github.event.inputs.branchPrefix }}')) { + console.log('Branch matched: ' + branch); + statusOK = true; + if(${{ github.event.inputs.mustBeGreen }}) { + console.log('Checking green status: ' + branch); + const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{ref}/status', { + owner: context.repo.owner, + repo: context.repo.repo, + ref: branch + }); + if(statuses.length > 0) { + const latest_status = statuses[0]['state']; + console.log('Validating status: ' + latest_status); + if(latest_status != 'success') { + console.log('Discarding ' + branch + ' with status ' + latest_status); + statusOK = false; + } + } + } + console.log('Checking labels: ' + branch); + const labels = pull['labels']; + for(const label of labels) { + const labelName = label['name']; + console.log('Checking label: ' + labelName); + if(labelName == '${{ github.event.inputs.ignoreLabel }}') { + console.log('Discarding ' + branch + ' with label ' + labelName); + statusOK = false; + } + } + if (statusOK) { + console.log('Adding branch to array: ' + branch); + branches.push(branch); + prs.push('#' + pull['number'] + ' ' + pull['title']); + base_branch = pull['base']['ref']; + } + } + } + if (branches.length == 0) { + core.setFailed('No PRs/branches matched criteria'); + return; + } + core.setOutput('base-branch', base_branch); + core.setOutput('prs-string', prs.join('\n')); + + combined = branches.join(' ') + console.log('Combined: ' + combined); + return combined + + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + # Creates a branch with other PR branches merged together + - name: Created combined branch + env: + BASE_BRANCH: ${{ steps.fetch-branch-names.outputs.base-branch }} + BRANCHES_TO_COMBINE: ${{ steps.fetch-branch-names.outputs.result }} + COMBINE_BRANCH_NAME: ${{ github.event.inputs.combineBranchName }} + run: | + echo "$BRANCHES_TO_COMBINE" + sourcebranches="${BRANCHES_TO_COMBINE%\"}" + sourcebranches="${sourcebranches#\"}" + + basebranch="${BASE_BRANCH%\"}" + basebranch="${basebranch#\"}" + + git config pull.rebase false + git config user.name github-actions + git config user.email github-actions@github.com + + git branch $COMBINE_BRANCH_NAME $basebranch + git checkout $COMBINE_BRANCH_NAME + git pull origin $sourcebranches --no-edit + git push origin $COMBINE_BRANCH_NAME + + # Creates a PR with the new combined branch + - uses: actions/github-script@v6 + name: Create Combined Pull Request + env: + PRS_STRING: ${{ steps.fetch-branch-names.outputs.prs-string }} + with: + github-token: ${{secrets.GITHUB_TOKEN}} + script: | + const prString = process.env.PRS_STRING; + const body = 'This PR was created by the Combine PRs action by combining the following PRs:\n' + prString; + await github.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Combined Dependabot Bumps', + head: '${{ github.event.inputs.combineBranchName }}', + base: '${{ steps.fetch-branch-names.outputs.base-branch }}', + body: body + }); diff --git a/.gitignore b/.gitignore index e0c46182de1..55c22395276 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,7 @@ .idea/ env/ venv/ -env36/ -env37/ -env38/ -env39/ +env3*/ Pipfile *.lock *.py[cod] From 6e0f81c73f8951bf6b981c71eae001b531bde3e9 Mon Sep 17 00:00:00 2001 From: Wong Hoi Sing Edison Date: Sat, 2 Apr 2022 21:05:12 +0800 Subject: [PATCH 20/62] CentOS 7: `read_text(encoding='utf-8')` (#3625) With CentOS 7 Python 3.6, running install from source with pip failed: sudo podman run -ti --rm centos:7 yum -y update yum -y install epel-release yum -y install git python3 python3-devel python3-pip python3-setuptools python3-wheel git clone https://github.com/samuelcolvin/pydantic.git cd pydantic pip3 install . With following error message: [root@c99d0585636c pydantic]# pip3 install . Processing /pydantic Complete output from command python setup.py egg_info: Traceback (most recent call last): File "", line 1, in File "/tmp/pip-91v_ixvz-build/setup.py", line 62, in history = (THIS_DIR / 'HISTORY.md').read_text() File "/usr/lib64/python3.6/pathlib.py", line 1197, in read_text return f.read() File "/usr/lib64/python3.6/encodings/ascii.py", line 26, in decode return codecs.ascii_decode(input, self.errors)[0] UnicodeDecodeError: 'ascii' codec can't decode byte 0xe2 in position 14648: ordinal not in range(128) ---------------------------------------- Command "python setup.py egg_info" failed with error code 1 in /tmp/pip-91v_ixvz-build/ This PR add the required `read_text(encoding='utf-8')` for `setup.py`. Signed-off-by: Wong Hoi Sing Edison --- changes/3625-read_text-encoding-utf-8.md | 1 + setup.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changes/3625-read_text-encoding-utf-8.md diff --git a/changes/3625-read_text-encoding-utf-8.md b/changes/3625-read_text-encoding-utf-8.md new file mode 100644 index 00000000000..a9365e5206d --- /dev/null +++ b/changes/3625-read_text-encoding-utf-8.md @@ -0,0 +1 @@ +Add `read_text(encoding='utf-8')` for `setup.py` diff --git a/setup.py b/setup.py index ae91cf9a0dd..c59056e0e56 100644 --- a/setup.py +++ b/setup.py @@ -59,12 +59,12 @@ def extra(self): description = 'Data validation and settings management using python type hints' THIS_DIR = Path(__file__).resolve().parent try: - history = (THIS_DIR / 'HISTORY.md').read_text() + history = (THIS_DIR / 'HISTORY.md').read_text(encoding='utf-8') history = re.sub(r'#(\d+)', r'[#\1](https://github.com/samuelcolvin/pydantic/issues/\1)', history) history = re.sub(r'( +)@([\w\-]+)', r'\1[@\2](https://github.com/\2)', history, flags=re.I) history = re.sub('@@', '@', history) - long_description = (THIS_DIR / 'README.md').read_text() + '\n\n' + history + long_description = (THIS_DIR / 'README.md').read_text(encoding='utf-8') + '\n\n' + history except FileNotFoundError: long_description = description + '.\n\nSee https://pydantic-docs.helpmanual.io/ for documentation.' From 02eb182db08f373d96a35ec525ec4f86497b3b11 Mon Sep 17 00:00:00 2001 From: Tom Milligan Date: Sat, 2 Apr 2022 14:06:11 +0100 Subject: [PATCH 21/62] fix: clarify that discriminated unions do not support singletons (#3639) --- changes/3636-tommilligan.md | 1 + docs/usage/types.md | 6 ++++++ pydantic/fields.py | 2 +- tests/test_discrimated_union.py | 13 ++++++++++++- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 changes/3636-tommilligan.md diff --git a/changes/3636-tommilligan.md b/changes/3636-tommilligan.md new file mode 100644 index 00000000000..ec10fce7ea4 --- /dev/null +++ b/changes/3636-tommilligan.md @@ -0,0 +1 @@ +fix: clarify that discriminated unions do not support singletons diff --git a/docs/usage/types.md b/docs/usage/types.md index 3fcd61d318a..86a437ba6e1 100644 --- a/docs/usage/types.md +++ b/docs/usage/types.md @@ -297,6 +297,12 @@ _(This script is complete, it should run "as is")_ Using the [Annotated Fields syntax](../schema/#typingannotated-fields) can be handy to regroup the `Union` and `discriminator` information. See below for an example! +!!! warning + Discriminated unions cannot be used with only a single variant, such as `Union[Cat]`. + + Python changes `Union[T]` into `T` at interpretation time, so it is not possible for `pydantic` to + distinguish fields of `Union[T]` from `T`. + #### Nested Discriminated Unions Only one discriminator can be set for a field but sometimes you want to combine multiple discriminators. diff --git a/pydantic/fields.py b/pydantic/fields.py index 6f169903235..e6736f0239b 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -600,7 +600,7 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) return if self.discriminator_key is not None and not is_union(origin): - raise TypeError('`discriminator` can only be used with `Union` type') + raise TypeError('`discriminator` can only be used with `Union` type with more than one variant') # add extra check for `collections.abc.Hashable` for python 3.10+ where origin is not `None` if origin is None or origin is CollectionsHashable: diff --git a/tests/test_discrimated_union.py b/tests/test_discrimated_union.py index 120f9d2f16b..a4dd501bc97 100644 --- a/tests/test_discrimated_union.py +++ b/tests/test_discrimated_union.py @@ -11,12 +11,23 @@ def test_discriminated_union_only_union(): - with pytest.raises(TypeError, match='`discriminator` can only be used with `Union` type'): + with pytest.raises( + TypeError, match='`discriminator` can only be used with `Union` type with more than one variant' + ): class Model(BaseModel): x: str = Field(..., discriminator='qwe') +def test_discriminated_union_single_variant(): + with pytest.raises( + TypeError, match='`discriminator` can only be used with `Union` type with more than one variant' + ): + + class Model(BaseModel): + x: Union[str] = Field(..., discriminator='qwe') + + def test_discriminated_union_invalid_type(): with pytest.raises(TypeError, match="Type 'str' is not a valid `BaseModel` or `dataclass`"): From 55b34ef946d7bcc39d779c0667c3e5acbe16a02f Mon Sep 17 00:00:00 2001 From: Natan Yellin Date: Sat, 2 Apr 2022 16:42:51 +0300 Subject: [PATCH 22/62] Add Robusta.dev to list of Pydantic users (#3715) * add robusta.dev to pydantic users * update robusta.dev description and fix typo --- docs/index.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/index.md b/docs/index.md index 772a6ea9993..f06b555831a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -114,6 +114,11 @@ Hundreds of organisations and packages are using *pydantic*, including: : trusts *pydantic* (via FastAPI) and [*arq*](https://github.com/samuelcolvin/arq) (Samuel's excellent asynchronous task queue) to reliably power multiple mission-critical microservices. +[Robusta.dev](https://robusta.dev/) +: are using *pydantic* to automate Kubernetes troubleshooting and maintenance. For example, their open source + [tools to debug and profile Python applications on Kubernetes](https://home.robusta.dev/python/) use + *pydantic* models. + For a more comprehensive list of open-source projects using *pydantic* see the [list of dependents on github](https://github.com/samuelcolvin/pydantic/network/dependents). From f0acf6efe7001ffc8c9b8d8a7229153ae17f151f Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 2 Apr 2022 14:45:31 +0100 Subject: [PATCH 23/62] Prevent subclasses of bytes being converted to bytes (#3707) * adding a test * fix and add change description --- changes/3706-samuelcolvin.md | 1 + pydantic/validators.py | 2 +- tests/test_edge_cases.py | 26 ++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changes/3706-samuelcolvin.md diff --git a/changes/3706-samuelcolvin.md b/changes/3706-samuelcolvin.md new file mode 100644 index 00000000000..3a22afee678 --- /dev/null +++ b/changes/3706-samuelcolvin.md @@ -0,0 +1 @@ +Prevent subclasses of bytes being converted to bytes diff --git a/pydantic/validators.py b/pydantic/validators.py index 63b7a59e080..d4783d97b12 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -76,7 +76,7 @@ def strict_str_validator(v: Any) -> Union[str]: raise errors.StrError() -def bytes_validator(v: Any) -> bytes: +def bytes_validator(v: Any) -> Union[bytes]: if isinstance(v, bytes): return v elif isinstance(v, bytearray): diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index dd07eb3d37b..5da62257040 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -1906,3 +1906,29 @@ class Config: arbitrary_types_allowed = True assert Model().x == Foo() + + +def test_bytes_subclass(): + class MyModel(BaseModel): + my_bytes: bytes + + class BytesSubclass(bytes): + def __new__(cls, data: bytes): + self = bytes.__new__(cls, data) + return self + + m = MyModel(my_bytes=BytesSubclass(b'foobar')) + assert m.my_bytes.__class__ == BytesSubclass + + +def test_int_subclass(): + class MyModel(BaseModel): + my_int: int + + class IntSubclass(int): + def __new__(cls, data: int): + self = int.__new__(cls, data) + return self + + m = MyModel(my_int=IntSubclass(123)) + assert m.my_int.__class__ == IntSubclass From f96a6131a3ad569b1f1ccbb073a4042209d472f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 Apr 2022 14:50:07 +0100 Subject: [PATCH 24/62] build(deps): bump mypy from 0.931 to 0.942 (#3968) * build(deps): bump mypy from 0.931 to 0.942 Bumps [mypy](https://github.com/python/mypy) from 0.931 to 0.942. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.931...v0.942) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * build(deps): bump mypy from 0.931 to 0.942 Bumps [mypy](https://github.com/python/mypy) from 0.931 to 0.942. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.931...v0.942) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * fix mypy Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Samuel Colvin --- pydantic/networks.py | 6 +++--- tests/requirements-linting.txt | 2 +- tests/requirements-testing.txt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pydantic/networks.py b/pydantic/networks.py index 50a01d42bb5..cbc9e21f4c5 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -265,7 +265,7 @@ def validate_parts(cls, parts: 'Parts') -> 'Parts': def validate_host(cls, parts: 'Parts') -> Tuple[str, Optional[str], str, bool]: host, tld, host_type, rebuild = None, None, None, False for f in ('domain', 'ipv4', 'ipv6'): - host = parts[f] # type: ignore[misc] + host = parts[f] # type: ignore[literal-required] if host: host_type = f break @@ -310,8 +310,8 @@ def get_default_parts(parts: 'Parts') -> 'Parts': @classmethod def apply_default_parts(cls, parts: 'Parts') -> 'Parts': for key, value in cls.get_default_parts(parts).items(): - if not parts[key]: # type: ignore[misc] - parts[key] = value # type: ignore[misc] + if not parts[key]: # type: ignore[literal-required] + parts[key] = value # type: ignore[literal-required] return parts def __repr__(self) -> str: diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index 69487560e87..f33bda7f724 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -3,7 +3,7 @@ flake8==4.0.1 flake8-quotes==3.3.1 hypothesis==6.41.0 isort==5.10.1 -mypy==0.931 +mypy==0.942 pre-commit==2.17.0 pycodestyle==2.8.0 pyflakes==2.4.0 diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index e4f808bf8a9..f29e6454b8b 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -2,7 +2,7 @@ coverage==6.3.2 hypothesis==6.41.0 # 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.931 +mypy==0.942 pytest==7.1.1 pytest-cov==3.0.0 pytest-mock==3.7.0 From 36c53ceaa3e72876d4b438e124fc90a2cbc4ecef Mon Sep 17 00:00:00 2001 From: Stefan Scherfke Date: Sat, 2 Apr 2022 16:11:48 +0200 Subject: [PATCH 25/62] Fix pytest crashes with hypothesis and pydantic (#3727) Pytest (sometimes?) crashes when it is invoked with `-vv` and pydantic and hypthesis are installed. This is because `_registered(typ)` modifies `_DEFINED_TYPES` while it is being iterated: ``` INTERNALERROR> File ".../lib/python3.9/site-packages/pydantic/_hypothesis_plugin.py", line 361, in INTERNALERROR> for typ in pydantic.types._DEFINED_TYPES: INTERNALERROR> File ".../lib/python3.9/_weakrefset.py", line 65, in __iter__ INTERNALERROR> for itemref in self.data: INTERNALERROR> RuntimeError: Set changed size during iteration ``` --- pydantic/_hypothesis_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic/_hypothesis_plugin.py b/pydantic/_hypothesis_plugin.py index 79d787e9c63..890e192ccaf 100644 --- a/pydantic/_hypothesis_plugin.py +++ b/pydantic/_hypothesis_plugin.py @@ -358,7 +358,7 @@ def resolve_constr(cls): # type: ignore[no-untyped-def] # pragma: no cover # Finally, register all previously-defined types, and patch in our new function -for typ in pydantic.types._DEFINED_TYPES: +for typ in list(pydantic.types._DEFINED_TYPES): _registered(typ) pydantic.types._registered = _registered st.register_type_strategy(pydantic.Json, resolve_json) From 7f90b2f342ba338957bb8dfff3ead760edcdd9bf Mon Sep 17 00:00:00 2001 From: Gary Donovan Date: Sun, 3 Apr 2022 01:25:43 +1100 Subject: [PATCH 26/62] Remove incorrect comment about lazy evaluation of setting sources (#3806) * Remove incorrect comment about lazy evaluation of setting sources It looks like the current implementation always evaluates every source (https://github.com/samuelcolvin/pydantic/blob/9d631a3429a66f30742c1a52c94ac18ec6ba848d/pydantic/env_settings.py#L73) before coalescing them into a single dictionary to pass to `BaseModel`. So the comment about lazy evaluation is incorrect and should be removed. * Add changelog --- changes/3806-garyd203.md | 1 + docs/usage/settings.md | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 changes/3806-garyd203.md diff --git a/changes/3806-garyd203.md b/changes/3806-garyd203.md new file mode 100644 index 00000000000..25b2217bb47 --- /dev/null +++ b/changes/3806-garyd203.md @@ -0,0 +1 @@ +Update documentation about lazy evaluation of sources for Settings (it's not actually done). diff --git a/docs/usage/settings.md b/docs/usage/settings.md index 3ab58213909..6bb752a1ea3 100644 --- a/docs/usage/settings.md +++ b/docs/usage/settings.md @@ -278,6 +278,3 @@ You might also want to disable a source: {!.tmp_examples/settings_disable_source.py!} ``` _(This script is complete, it should run "as is", here you might need to set the `my_api_key` environment variable)_ - -Because of the callables approach of `customise_sources`, evaluation of sources is lazy so unused sources don't -have an adverse effect on performance. From b25e22f7d412612681fb9b217dd63a20a336f809 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 4 Apr 2022 13:22:07 +0100 Subject: [PATCH 27/62] [no ci] correct name of change file --- changes/{3625-read_text-encoding-utf-8.md => 3625-hswong3i.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changes/{3625-read_text-encoding-utf-8.md => 3625-hswong3i.md} (100%) diff --git a/changes/3625-read_text-encoding-utf-8.md b/changes/3625-hswong3i.md similarity index 100% rename from changes/3625-read_text-encoding-utf-8.md rename to changes/3625-hswong3i.md From 8997cc5961139dd2695761a33c06a66adbf1430a Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 4 Apr 2022 13:29:17 +0100 Subject: [PATCH 28/62] Remove benchmarks completely (#3973) * removing benchmarks completely * [no ci] add change --- .github/workflows/ci.yml | 25 +-- Makefile | 16 -- benchmarks/profile.py | 39 ---- benchmarks/requirements.txt | 14 -- benchmarks/run.py | 294 ----------------------------- benchmarks/test_cattrs.py | 99 ---------- benchmarks/test_cerberus.py | 49 ----- benchmarks/test_drf.py | 53 ------ benchmarks/test_marshmallow.py | 45 ----- benchmarks/test_pydantic.py | 52 ----- benchmarks/test_schematics.py | 50 ----- benchmarks/test_trafaret.py | 46 ----- benchmarks/test_valideer.py | 47 ----- benchmarks/test_voluptuous.py | 51 ----- changes/3973-samuelcolvin.md | 1 + docs/benchmarks.md | 8 - docs/extra/redirects.js | 2 - docs/index.md | 3 +- docs/usage/validation_decorator.md | 2 +- mkdocs.yml | 1 - 20 files changed, 5 insertions(+), 892 deletions(-) delete mode 100644 benchmarks/profile.py delete mode 100644 benchmarks/requirements.txt delete mode 100644 benchmarks/run.py delete mode 100644 benchmarks/test_cattrs.py delete mode 100644 benchmarks/test_cerberus.py delete mode 100644 benchmarks/test_drf.py delete mode 100644 benchmarks/test_marshmallow.py delete mode 100644 benchmarks/test_pydantic.py delete mode 100644 benchmarks/test_schematics.py delete mode 100644 benchmarks/test_trafaret.py delete mode 100644 benchmarks/test_valideer.py delete mode 100644 benchmarks/test_voluptuous.py create mode 100644 changes/3973-samuelcolvin.md delete mode 100644 docs/benchmarks.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 595c863d2a0..08b22ce399e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -255,32 +255,9 @@ jobs: - name: test run: make test-fastapi - benchmark: - name: run benchmarks - runs-on: ubuntu-latest - env: - BENCHMARK_REPEATS: 1 - - steps: - - uses: actions/checkout@v3 - - - name: set up python - uses: actions/setup-python@v3 - with: - python-version: '3.8' - - - name: install and build - run: | - make build - make install-benchmarks - - - run: make benchmark-pydantic - - run: make benchmark-all - - run: make benchmark-json - build: name: build py3.${{ matrix.python-version }} on ${{ matrix.platform || matrix.os }} - needs: [lint, test-linux, test-windows-mac, test-old-mypy, test-fastapi, benchmark] + needs: [lint, test-linux, test-windows-mac, test-old-mypy, test-fastapi] if: "success() && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master')" strategy: fail-fast: false diff --git a/Makefile b/Makefile index 89bef90a605..46060befb65 100644 --- a/Makefile +++ b/Makefile @@ -21,10 +21,6 @@ install-testing: install-pydantic install-docs: install-pydantic pip install -U -r docs/requirements.txt -.PHONY: install-benchmarks -install-benchmarks: install-pydantic - pip install -U -r benchmarks/requirements.txt - .PHONY: install install: install-testing install-linting install-docs @echo 'installed development requirements' @@ -85,18 +81,6 @@ test-fastapi: .PHONY: all all: lint mypy testcov -.PHONY: benchmark-all -benchmark-all: - python benchmarks/run.py - -.PHONY: benchmark-pydantic -benchmark-pydantic: - python benchmarks/run.py pydantic-only - -.PHONY: benchmark-json -benchmark-json: - TEST_JSON=1 python benchmarks/run.py - .PHONY: clean clean: rm -rf `find . -name __pycache__` diff --git a/benchmarks/profile.py b/benchmarks/profile.py deleted file mode 100644 index 53e4ab43eec..00000000000 --- a/benchmarks/profile.py +++ /dev/null @@ -1,39 +0,0 @@ -import json - -from line_profiler import LineProfiler - -import pydantic.datetime_parse -import pydantic.validators -from pydantic import validate_model -from pydantic.fields import ModelField -from test_pydantic import TestPydantic - -with open('./benchmarks/cases.json') as f: - cases = json.load(f) - - -def run(): - count, pass_count = 0, 0 - test = TestPydantic(False) - for case in cases: - passed, result = test.validate(case) - count += 1 - pass_count += passed - print('success percentage:', pass_count / count * 100) - - -funcs_to_profile = [validate_model, ModelField.validate, ModelField._validate_singleton, ModelField._apply_validators] -module_objects = {**vars(pydantic.validators), **vars(pydantic.datetime_parse), **vars(ModelField)} -funcs_to_profile += [v for k, v in module_objects.items() if not k.startswith('_') and str(v).startswith('{lpad}} ({i+1:>{len(str(repeats))}}/{repeats}) time={time:0.3f}s, success={success:0.2f}%') - times.append(time) - print(f'{p:>{lpad}} best={min(times):0.3f}s, avg={mean(times):0.3f}s, stdev={stdev(times):0.3f}s') - model_count = 3 * len(cases) - avg = mean(times) / model_count * 1e6 - sd = stdev(times) / model_count * 1e6 - results.append(f'{p:>{lpad}} best={min(times) / model_count * 1e6:0.3f}μs/iter ' - f'avg={avg:0.3f}μs/iter stdev={sd:0.3f}μs/iter version={test_class.version}') - csv_results.append([p, test_class.version, avg]) - print() - - return results, csv_results - -def main(): - json_path = THIS_DIR / 'cases.json' - if not json_path.exists(): - print('generating test cases...') - cases = [generate_case() for _ in range(2000)] - with json_path.open('w') as f: - json.dump(cases, f, indent=2, sort_keys=True) - else: - with json_path.open() as f: - cases = json.load(f) - - tests = [TestPydantic] - if 'pydantic-only' not in sys.argv: - tests += active_other_tests - - repeats = int(os.getenv('BENCHMARK_REPEATS', '5')) - test_json = 'TEST_JSON' in os.environ - results, csv_results = run_tests(tests, cases, repeats, test_json) - - for r in results: - print(r) - - if 'SAVE' in os.environ: - save_md(csv_results) - - -def save_md(data): - headings = 'Package', 'Version', 'Relative Performance', 'Mean validation time' - rows = [headings, ['---' for _ in headings]] - - first_avg = None - for package, version, avg in sorted(data, key=itemgetter(2)): - if first_avg: - relative = f'{avg / first_avg:0.1f}x slower' - else: - relative = '' - first_avg = avg - rows.append([package, f'`{version}`', relative, f'{avg:0.1f}μs']) - - table = '\n'.join(' | '.join(row) for row in rows) - text = f"""\ -[//]: <> (Generated with benchmarks/run.py, DO NOT EDIT THIS FILE DIRECTLY, instead run `SAVE=1 python ./run.py`.) - -{table} -""" - (Path(__file__).parent / '..' / 'docs' / '.benchmarks_table.md').write_text(text) - - -def diff(): - json_path = THIS_DIR / 'cases.json' - with json_path.open() as f: - cases = json.load(f) - - allow_extra = True - pydantic = TestPydantic(allow_extra) - others = [t(allow_extra) for t in active_other_tests] - - for case in cases: - pydantic_passed, pydantic_result = pydantic.validate(case) - for other in others: - other_passed, other_result = other.validate(case) - if other_passed != pydantic_passed: - print(f'⨯ pydantic {pydantic_passed} != {other.package} {other_passed}') - debug(case, pydantic_result, other_result) - return - print('✓ data passes match for all packages') - - -if __name__ == '__main__': - if 'diff' in sys.argv: - diff() - else: - main() - - # if None in other_tests: - # print('not all libraries could be imported!') - # sys.exit(1) diff --git a/benchmarks/test_cattrs.py b/benchmarks/test_cattrs.py deleted file mode 100644 index 617c68dfa9f..00000000000 --- a/benchmarks/test_cattrs.py +++ /dev/null @@ -1,99 +0,0 @@ -from datetime import datetime -from typing import List, Optional - -import attr -import cattr -from dateutil.parser import parse - - -class TestCAttrs: - package = 'attrs + cattrs' - version = attr.__version__ - - def __init__(self, allow_extra): - # cf. https://github.com/Tinche/cattrs/issues/26 why at least structure_str is needed - def structure_str(s, _): - if not isinstance(s, str): - raise ValueError() - return s - - def structure_int(i, _): - if not isinstance(i, int): - raise ValueError() - return i - - class PositiveInt(int): - ... - - def structure_posint(i, x): - i = PositiveInt(i) - if not isinstance(i, PositiveInt): - raise ValueError() - if i <= 0: - raise ValueError() - return i - - cattr.register_structure_hook(datetime, lambda isostring, _: parse(isostring)) - cattr.register_structure_hook(str, structure_str) - cattr.register_structure_hook(int, structure_int) - cattr.register_structure_hook(PositiveInt, structure_posint) - - def str_len_val(max_len: int, min_len: int = 0, required: bool = False): - # validate the max len of a string and optionally its min len and whether None is - # an acceptable value - def _check_str_len(self, attribute, value): - if value is None: - if required: - raise ValueError("") - else: - return - if len(value) > max_len: - raise ValueError("") - if min_len and len(value) < min_len: - raise ValueError("") - - return _check_str_len - - def pos_int(self, attribute, value): - # Validate that value is a positive >0 integer; None is allowed - if value is None: - return - if value <= 0: - raise ValueError("") - - @attr.s(auto_attribs=True, frozen=True, kw_only=True) - class Skill: - subject: str - subject_id: int - category: str - qual_level: str - qual_level_id: int - qual_level_ranking: float = 0 - - @attr.s(auto_attribs=True, frozen=True, kw_only=True) - class Location: - latitude: float = None - longitude: float = None - - @attr.s(auto_attribs=True, frozen=True, kw_only=True) - class Model: - id: int - sort_index: float - client_name: str = attr.ib(validator=str_len_val(255)) - # client_email: EmailStr = None - client_phone: Optional[str] = attr.ib(default=None, validator=str_len_val(255)) - location: Optional[Location] = None - - contractor: Optional[PositiveInt] - upstream_http_referrer: Optional[str] = attr.ib(default=None, validator=str_len_val(1023)) - grecaptcha_response: str = attr.ib(validator=str_len_val(1000, 20, required=True)) - last_updated: Optional[datetime] = None - skills: List[Skill] = [] - - self.model = Model - - def validate(self, data): - try: - return True, cattr.structure(data, self.model) - except (ValueError, TypeError, KeyError) as e: - return False, str(e) diff --git a/benchmarks/test_cerberus.py b/benchmarks/test_cerberus.py deleted file mode 100644 index 966d069cce3..00000000000 --- a/benchmarks/test_cerberus.py +++ /dev/null @@ -1,49 +0,0 @@ -from cerberus import Validator, __version__ -from dateutil.parser import parse as datetime_parse - - -class TestCerberus: - package = 'cerberus' - version = str(__version__) - - def __init__(self, allow_extra): - schema = { - 'id': {'type': 'integer', 'required': True}, - 'client_name': {'type': 'string', 'maxlength': 255, 'required': True}, - 'sort_index': {'type': 'float', 'required': True}, - 'client_phone': {'type': 'string', 'maxlength': 255, 'nullable': True}, - 'location': { - 'type': 'dict', - 'schema': {'latitude': {'type': 'float'}, 'longitude': {'type': 'float'}}, - 'nullable': True, - }, - 'contractor': {'type': 'integer', 'min': 0, 'nullable': True, 'coerce': int}, - 'upstream_http_referrer': {'type': 'string', 'maxlength': 1023, 'nullable': True}, - 'grecaptcha_response': {'type': 'string', 'minlength': 20, 'maxlength': 1000, 'required': True}, - 'last_updated': {'type': 'datetime', 'nullable': True, 'coerce': datetime_parse}, - 'skills': { - 'type': 'list', - 'default': [], - 'schema': { - 'type': 'dict', - 'schema': { - 'subject': {'type': 'string', 'required': True}, - 'subject_id': {'type': 'integer', 'required': True}, - 'category': {'type': 'string', 'required': True}, - 'qual_level': {'type': 'string', 'required': True}, - 'qual_level_id': {'type': 'integer', 'required': True}, - 'qual_level_ranking': {'type': 'float', 'default': 0, 'required': True}, - }, - }, - }, - } - - self.v = Validator(schema) - self.v.allow_unknown = allow_extra - - def validate(self, data): - validated = self.v.validated(data) - if validated is None: - return False, self.v.errors - else: - return True, validated diff --git a/benchmarks/test_drf.py b/benchmarks/test_drf.py deleted file mode 100644 index 638a0dfaeea..00000000000 --- a/benchmarks/test_drf.py +++ /dev/null @@ -1,53 +0,0 @@ -import django -from django.conf import settings - -settings.configure( - INSTALLED_APPS=['django.contrib.auth', 'django.contrib.contenttypes'] -) -django.setup() - -from rest_framework import __version__, serializers - - -class TestDRF: - package = 'django-rest-framework' - version = __version__ - - def __init__(self, allow_extra): - class Model(serializers.Serializer): - id = serializers.IntegerField() - client_name = serializers.CharField(max_length=255, trim_whitespace=False) - sort_index = serializers.FloatField() - # client_email = serializers.EmailField(required=False, allow_null=True) - client_phone = serializers.CharField(max_length=255, trim_whitespace=False, required=False, allow_null=True) - - class Location(serializers.Serializer): - latitude = serializers.FloatField(required=False, allow_null=True) - longitude = serializers.FloatField(required=False, allow_null=True) - location = Location(required=False, allow_null=True) - - contractor = serializers.IntegerField(required=False, allow_null=True, min_value=0) - upstream_http_referrer = serializers.CharField( - max_length=1023, trim_whitespace=False, required=False, allow_null=True - ) - grecaptcha_response = serializers.CharField(min_length=20, max_length=1000, trim_whitespace=False) - last_updated = serializers.DateTimeField(required=False, allow_null=True) - - class Skill(serializers.Serializer): - subject = serializers.CharField() - subject_id = serializers.IntegerField() - category = serializers.CharField() - qual_level = serializers.CharField() - qual_level_id = serializers.IntegerField() - qual_level_ranking = serializers.FloatField(default=0) - skills = serializers.ListField(child=Skill()) - - self.allow_extra = allow_extra # unused - self.serializer = Model - - def validate(self, data): - s = self.serializer(data=data) - if s.is_valid(): - return True, dict(s.data) - else: - return False, dict(s.errors) diff --git a/benchmarks/test_marshmallow.py b/benchmarks/test_marshmallow.py deleted file mode 100644 index 8ebc0355c27..00000000000 --- a/benchmarks/test_marshmallow.py +++ /dev/null @@ -1,45 +0,0 @@ -from marshmallow import Schema, ValidationError, __version__, fields, validate - - -class TestMarshmallow: - package = 'marshmallow' - version = __version__ - - def __init__(self, allow_extra): - class LocationSchema(Schema): - latitude = fields.Float(allow_none=True) - longitude = fields.Float(allow_none=True) - - class SkillSchema(Schema): - subject = fields.Str(required=True) - subject_id = fields.Integer(required=True) - category = fields.Str(required=True) - qual_level = fields.Str(required=True) - qual_level_id = fields.Integer(required=True) - qual_level_ranking = fields.Float(default=0) - - class Model(Schema): - id = fields.Integer(required=True) - client_name = fields.Str(validate=validate.Length(max=255), required=True) - sort_index = fields.Float(required=True) - # client_email = fields.Email() - client_phone = fields.Str(validate=validate.Length(max=255), allow_none=True) - - location = fields.Nested(LocationSchema) - - contractor = fields.Integer(validate=validate.Range(min=0), allow_none=True) - upstream_http_referrer = fields.Str(validate=validate.Length(max=1023), allow_none=True) - grecaptcha_response = fields.Str(validate=validate.Length(min=20, max=1000), required=True) - last_updated = fields.DateTime(allow_none=True) - skills = fields.Nested(SkillSchema, many=True) - - self.allow_extra = allow_extra # unused - self.schema = Model() - - def validate(self, data): - try: - result = self.schema.load(data) - except ValidationError as e: - return False, e.normalized_messages() - else: - return True, result diff --git a/benchmarks/test_pydantic.py b/benchmarks/test_pydantic.py deleted file mode 100644 index ff99dceb69c..00000000000 --- a/benchmarks/test_pydantic.py +++ /dev/null @@ -1,52 +0,0 @@ -from datetime import datetime -from typing import List - -from pydantic import VERSION, BaseModel, Extra, PositiveInt, ValidationError, constr - - -class TestPydantic: - package = 'pydantic' - version = str(VERSION) - - def __init__(self, allow_extra): - class Model(BaseModel): - id: int - client_name: constr(max_length=255) - sort_index: float - # client_email: EmailStr = None - client_phone: constr(max_length=255) = None - - class Location(BaseModel): - latitude: float = None - longitude: float = None - - location: Location = None - - contractor: PositiveInt = None - upstream_http_referrer: constr(max_length=1023) = None - grecaptcha_response: constr(min_length=20, max_length=1000) - last_updated: datetime = None - - class Skill(BaseModel): - subject: str - subject_id: int - category: str - qual_level: str - qual_level_id: int - qual_level_ranking: float = 0 - - skills: List[Skill] = [] - - class Config: - extra = Extra.allow if allow_extra else Extra.forbid - - self.model = Model - - def validate(self, data): - try: - return True, self.model(**data) - except ValidationError as e: - return False, e.errors() - - def to_json(self, model): - return model.json() diff --git a/benchmarks/test_schematics.py b/benchmarks/test_schematics.py deleted file mode 100644 index 94af84637d0..00000000000 --- a/benchmarks/test_schematics.py +++ /dev/null @@ -1,50 +0,0 @@ -from schematics import __version__ -from schematics.exceptions import DataError, ValidationError -from schematics.models import Model as PModel -from schematics.types import IntType, StringType -from schematics.types.base import DateType, FloatType -from schematics.types.compound import ListType, ModelType - - -class TestSchematics: - package = 'schematics' - version = __version__ - - def __init__(self, allow_extra): - class Model(PModel): - id = IntType(required=True) - client_name = StringType(max_length=255, required=True) - sort_index = FloatType(required=True) - client_phone = StringType(max_length=255, default=None) - - class Location(PModel): - latitude = FloatType(default=None) - longitude = FloatType(default=None) - - location = ModelType(model_spec=Location, default=None) - - contractor = IntType(min_value=1, default=None) - upstream_http_referrer = StringType(max_length=1023, default=None) - grecaptcha_response = StringType(min_length=20, max_length=1000, required=True) - last_updated = DateType(formats='%Y-%m-%dT%H:%M:%S') - - class Skill(PModel): - subject = StringType(required=True) - subject_id = IntType(required=True) - category = StringType(required=True) - qual_level = StringType(required=True) - qual_level_id = IntType(required=True) - qual_level_ranking = FloatType(default=0, required=True) - - skills = ListType(ModelType(Skill), default=[]) - - self.model = Model - - def validate(self, data): - try: - obj = self.model(data) - return True, obj.validate() - except DataError as e: - return False, e - except ValidationError as e: - return False, e diff --git a/benchmarks/test_trafaret.py b/benchmarks/test_trafaret.py deleted file mode 100644 index 546c165d1f2..00000000000 --- a/benchmarks/test_trafaret.py +++ /dev/null @@ -1,46 +0,0 @@ -from dateutil.parser import parse -import trafaret as t - - -class TestTrafaret: - package = 'trafaret' - version = '.'.join(map(str, t.__VERSION__)) - - def __init__(self, allow_extra): - self.schema = t.Dict({ - 'id': t.Int(), - 'client_name': t.String(max_length=255), - 'sort_index': t.Float, - # t.Key('client_email', optional=True): t.Or(t.Null | t.Email()), - t.Key('client_phone', optional=True): t.Or(t.Null | t.String(max_length=255)), - - t.Key('location', optional=True): t.Or(t.Null | t.Dict({ - 'latitude': t.Or(t.Float | t.Null), - 'longitude': t.Or(t.Float | t.Null), - })), - - t.Key('contractor', optional=True): t.Or(t.Null | t.Int(gt=0)), - t.Key('upstream_http_referrer', optional=True): t.Or(t.Null | t.String(max_length=1023)), - t.Key('grecaptcha_response'): t.String(min_length=20, max_length=1000), - - t.Key('last_updated', optional=True): t.Or(t.Null | t.String >> parse), - - t.Key('skills', default=[]): t.List(t.Dict({ - 'subject': t.String, - 'subject_id': t.Int, - 'category': t.String, - 'qual_level': t.String, - 'qual_level_id': t.Int, - t.Key('qual_level_ranking', default=0): t.Float, - })), - }) - if allow_extra: - self.schema.allow_extra('*') - - def validate(self, data): - try: - return True, self.schema.check(data) - except t.DataError: - return False, None - except ValueError: - return False, None diff --git a/benchmarks/test_valideer.py b/benchmarks/test_valideer.py deleted file mode 100644 index 353122acec4..00000000000 --- a/benchmarks/test_valideer.py +++ /dev/null @@ -1,47 +0,0 @@ -import re -import subprocess - -import dateutil.parser -import valideer as V - -# valideer appears to provide no way of getting the installed version -p = subprocess.run(['pip', 'freeze'], stdout=subprocess.PIPE, encoding='utf8', check=True) -valideer_version = re.search(r'valideer==(.+)', p.stdout).group(1) - - -class TestValideer: - package = 'valideer' - version = valideer_version - - def __init__(self, allow_extra): - schema = { - '+id': int, - '+client_name': V.String(max_length=255), - '+sort_index': float, - 'client_phone': V.Nullable(V.String(max_length=255)), - 'location': {'latitude': float, 'longitude': float}, - 'contractor': V.Range(V.AdaptTo(int), min_value=1), - 'upstream_http_referrer': V.Nullable(V.String(max_length=1023)), - '+grecaptcha_response': V.String(min_length=20, max_length=1000), - 'last_updated': V.AdaptBy(dateutil.parser.parse), - 'skills': V.Nullable( - [ - { - '+subject': str, - '+subject_id': int, - '+category': str, - '+qual_level': str, - '+qual_level_id': int, - 'qual_level_ranking': V.Nullable(float, default=0), - } - ], - default=[], - ), - } - self.validator = V.parse(schema, additional_properties=allow_extra) - - def validate(self, data): - try: - return True, self.validator.validate(data) - except V.ValidationError as e: - return False, str(e) diff --git a/benchmarks/test_voluptuous.py b/benchmarks/test_voluptuous.py deleted file mode 100644 index 0ac46f5d653..00000000000 --- a/benchmarks/test_voluptuous.py +++ /dev/null @@ -1,51 +0,0 @@ -from dateutil.parser import parse as parse_datetime -import voluptuous as v -from voluptuous.humanize import humanize_error - - -class TestVoluptuous: - package = 'voluptuous' - version = v.__version__ - - def __init__(self, allow_extra): - self.schema = v.Schema( - { - v.Required('id'): int, - v.Required('client_name'): v.All(str, v.Length(max=255)), - v.Required('sort_index'): float, - # v.Optional('client_email'): v.Maybe(v.Email), - v.Optional('client_phone'): v.Maybe(v.All(str, v.Length(max=255))), - v.Optional('location'): v.Maybe( - v.Schema( - { - 'latitude': v.Maybe(float), - 'longitude': v.Maybe(float) - }, - required=True - ) - ), - v.Optional('contractor'): v.Maybe(v.All(v.Coerce(int), v.Range(min=1))), - v.Optional('upstream_http_referrer'): v.Maybe(v.All(str, v.Length(max=1023))), - v.Required('grecaptcha_response'): v.All(str, v.Length(min=20, max=1000)), - v.Optional('last_updated'): v.Maybe(parse_datetime), - v.Required('skills', default=[]): [ - v.Schema( - { - v.Required('subject'): str, - v.Required('subject_id'): int, - v.Required('category'): str, - v.Required('qual_level'): str, - v.Required('qual_level_id'): int, - v.Required('qual_level_ranking', default=0): float, - } - ) - ], - }, - extra=allow_extra, - ) - - def validate(self, data): - try: - return True, self.schema(data) - except v.MultipleInvalid as e: - return False, humanize_error(data, e) diff --git a/changes/3973-samuelcolvin.md b/changes/3973-samuelcolvin.md new file mode 100644 index 00000000000..a6838a23e1e --- /dev/null +++ b/changes/3973-samuelcolvin.md @@ -0,0 +1 @@ +Remove benchmarks from codebase and docs. diff --git a/docs/benchmarks.md b/docs/benchmarks.md deleted file mode 100644 index 94364bf3470..00000000000 --- a/docs/benchmarks.md +++ /dev/null @@ -1,8 +0,0 @@ -Below are the results of crude benchmarks comparing *pydantic* to other validation libraries. - -{!.benchmarks_table.md!} - -See [the benchmarks code](https://github.com/samuelcolvin/pydantic/tree/master/benchmarks) -for more details on the test case. Feel free to suggest more packages to benchmark or improve an existing one. - -Benchmarks were run with Python 3.8.6 and the package versions listed above installed via pypi on macOS Big Sur. diff --git a/docs/extra/redirects.js b/docs/extra/redirects.js index d8aec3e981e..b6b1a3c0f55 100644 --- a/docs/extra/redirects.js +++ b/docs/extra/redirects.js @@ -82,8 +82,6 @@ const lookup = { 'id7': '/usage/postponed_annotations/', 'id8': '/usage/postponed_annotations/', 'usage-of-union-in-annotations-and-type-order': '/usage/types/#unions', - 'benchmarks': '/benchmarks/', - 'benchmarks-tag': '/benchmarks/', 'contributing-to-pydantic': '/contributing/', 'pycharm-plugin': '/pycharm_plugin/', 'id9': '/pycharm_plugin/', diff --git a/docs/index.md b/docs/index.md index f06b555831a..ba4366dd114 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,7 +56,8 @@ So *pydantic* uses some cool new language features, but why should I actually go be read from environment variables, and more complex objects like DSNs and python objects are often required. **fast** -: In [benchmarks](benchmarks.md) *pydantic* is faster than all other tested libraries. +: *pydantic* has always taken performance seriously, most of the library is compiled with cython giving a ~50% speedup, + it's generally as fast or faster than most similar libraries. **validate complex structures** : use of [recursive *pydantic* models](usage/models.md#recursive-models), `typing`'s diff --git a/docs/usage/validation_decorator.md b/docs/usage/validation_decorator.md index 914b9319865..74fcc8aa311 100644 --- a/docs/usage/validation_decorator.md +++ b/docs/usage/validation_decorator.md @@ -149,7 +149,7 @@ to use this, it may even become the default for the decorator. ### Performance -We've made a big effort to make *pydantic* as performant as possible (see [the benchmarks](../benchmarks.md)) +We've made a big effort to make *pydantic* as performant as possible and argument inspect and model creation is only performed once when the function is defined, however there will still be a performance impact to using the `validate_arguments` decorator compared to calling the raw function. diff --git a/mkdocs.yml b/mkdocs.yml index ff51c5b7a68..efdbbc20680 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,7 +56,6 @@ nav: - 'Usage with mypy': usage/mypy.md - 'Usage with devtools': usage/devtools.md - Contributing to pydantic: contributing.md -- benchmarks.md - 'Mypy plugin': mypy_plugin.md - 'PyCharm plugin': pycharm_plugin.md - 'Visual Studio Code': visual_studio_code.md From 5490ad5173743ef2bf85216d11b9ff0822b3d25b Mon Sep 17 00:00:00 2001 From: Eric Jolibois Date: Wed, 11 May 2022 18:03:48 +0200 Subject: [PATCH 29/62] fix: `Config.copy_on_model_validation` does a deep copy and not a shallow one (#3642) * fix: `Config.copy_on_model_validation` does a deep copy and not a shallow one closes #3641 * fix: typo * use python 3.10 to run fastapi tests * fix fastapi test call Co-authored-by: Samuel Colvin --- .github/workflows/ci.yml | 3 --- changes/3641-PrettyWood.md | 1 + pydantic/main.py | 2 +- tests/test_main.py | 18 +++++++++++++++++- 4 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 changes/3641-PrettyWood.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08b22ce399e..6070168905d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -246,9 +246,6 @@ jobs: with: python-version: '3.10' - - name: upgrade pip - run: pip install -U pip setuptools wheel - - name: install run: make install-testing diff --git a/changes/3641-PrettyWood.md b/changes/3641-PrettyWood.md new file mode 100644 index 00000000000..d0338c66369 --- /dev/null +++ b/changes/3641-PrettyWood.md @@ -0,0 +1 @@ +`Config.copy_on_model_validation` does a deep copy and not a shallow one \ No newline at end of file diff --git a/pydantic/main.py b/pydantic/main.py index eea8abcbbe8..1dcda9efd55 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -666,7 +666,7 @@ def __get_validators__(cls) -> 'CallableGenerator': def validate(cls: Type['Model'], value: Any) -> 'Model': if isinstance(value, cls): if cls.__config__.copy_on_model_validation: - return value._copy_and_set_values(value.__dict__, value.__fields_set__, deep=False) + return value._copy_and_set_values(value.__dict__, value.__fields_set__, deep=True) else: return value diff --git a/tests/test_main.py b/tests/test_main.py index c00d70688cb..f4d3844c4aa 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1561,12 +1561,28 @@ class Config: assert t.user is not my_user assert t.user.hobbies == ['scuba diving'] - assert t.user.hobbies is my_user.hobbies # `Config.copy_on_model_validation` only does a shallow copy + assert t.user.hobbies is not my_user.hobbies # `Config.copy_on_model_validation` does a deep copy assert t.user._priv == 13 assert t.user.password.get_secret_value() == 'hashedpassword' assert t.dict() == {'id': '1234567890', 'user': {'id': 42, 'hobbies': ['scuba diving']}} +def test_validation_deep_copy(): + """By default, Config.copy_on_model_validation should do a deep copy""" + + class A(BaseModel): + name: str + + class B(BaseModel): + list_a: List[A] + + a = A(name='a') + b = B(list_a=[a]) + assert b.list_a == [A(name='a')] + a.name = 'b' + assert b.list_a == [A(name='a')] + + @pytest.mark.parametrize( 'kinds', [ From 7392d2ea858f9b0c9b738f5e1b738bc513a99a11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 May 2022 17:59:22 +0100 Subject: [PATCH 30/62] build(deps): bump typing-extensions from 4.1.1 to 4.2.0 (#4040) Bumps [typing-extensions](https://github.com/python/typing) from 4.1.1 to 4.2.0. - [Release notes](https://github.com/python/typing/releases) - [Changelog](https://github.com/python/typing/blob/master/typing_extensions/CHANGELOG.md) - [Commits](https://github.com/python/typing/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: typing-extensions dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3beeb0fdbf4..71a0fbf007d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ Cython==0.29.28;sys_platform!='win32' devtools==0.8.0 email-validator==1.1.3 dataclasses==0.6; python_version < '3.7' -typing-extensions==4.1.1 +typing-extensions==4.2.0 python-dotenv==0.20.0 From 37260596106f7a6d929492585185516927af7e74 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 May 2022 18:00:13 +0100 Subject: [PATCH 31/62] build(deps): bump mkdocs-material from 8.2.8 to 8.2.12 (#4038) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.2.8 to 8.2.12. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.2.8...8.2.12) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 8c28433d5f9..da19a20fcdf 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,7 +6,7 @@ markdown-include==0.6.0 mdx-truly-sane-lists==1.2 mkdocs==1.3.0 mkdocs-exclude==1.0.2 -mkdocs-material==8.2.8 +mkdocs-material==8.2.12 sqlalchemy orjson ujson From 59dffb6942494fc4b86a297452d7d508905c8939 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 May 2022 18:00:29 +0100 Subject: [PATCH 32/62] build(deps): bump pytest from 7.1.1 to 7.1.2 (#4037) Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.1.1 to 7.1.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.1.1...7.1.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/requirements-testing.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index f29e6454b8b..5721960d3c4 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -3,7 +3,7 @@ hypothesis==6.41.0 # 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.942 -pytest==7.1.1 +pytest==7.1.2 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-sugar==0.9.4 From b826ff65eccc930e0fbd77b32edd1328d974de2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 May 2022 18:00:41 +0100 Subject: [PATCH 33/62] build(deps): bump actions/upload-artifact from 2 to 3 (#4035) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6070168905d..1a785e26089 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: run: make docs - name: Store docs site - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: docs path: site @@ -120,7 +120,7 @@ jobs: CONTEXT: linux-py${{ matrix.python-version }}-compiled-no-deps-no - name: store coverage files - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: coverage path: coverage @@ -159,7 +159,7 @@ jobs: CONTEXT: ${{ matrix.os }}-py${{ matrix.python-version }} - name: store coverage files - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: coverage path: coverage @@ -200,7 +200,7 @@ jobs: CONTEXT: linux-py3.10-mypy${{ matrix.mypy-version }} - name: store coverage files - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: coverage path: coverage @@ -230,7 +230,7 @@ jobs: - run: coverage html --show-contexts - name: Store coverage html - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: coverage-html path: htmlcov @@ -308,7 +308,7 @@ jobs: twine check dist/* - name: Store dist artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pypi_files path: dist From d128f4507726b2af59d009a68ff2788b6c85ea0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 May 2022 18:00:51 +0100 Subject: [PATCH 34/62] build(deps): bump actions/download-artifact from 2 to 3 (#4034) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 2 to 3. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a785e26089..3726206a075 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -217,7 +217,7 @@ jobs: python-version: '3.8' - name: get coverage files - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: coverage path: coverage @@ -328,13 +328,13 @@ jobs: python-version: '3.8' - name: get dist artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: pypi_files path: dist - name: get docs - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: docs path: site From 150ee62d4a584cc397f98662b04253853f8973a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 May 2022 18:56:01 +0100 Subject: [PATCH 35/62] build(deps): bump email-validator from 1.1.3 to 1.2.1 (#4060) Bumps [email-validator](https://github.com/JoshData/python-email-validator) from 1.1.3 to 1.2.1. - [Release notes](https://github.com/JoshData/python-email-validator/releases) - [Commits](https://github.com/JoshData/python-email-validator/compare/v1.1.3...v1.2.1) --- updated-dependencies: - dependency-name: email-validator dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 71a0fbf007d..d82a66c99a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Cython==0.29.28;sys_platform!='win32' devtools==0.8.0 -email-validator==1.1.3 +email-validator==1.2.1 dataclasses==0.6; python_version < '3.7' typing-extensions==4.2.0 python-dotenv==0.20.0 From a1b7aa2a63914b243158a277294960305d3fc369 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 May 2022 18:56:10 +0100 Subject: [PATCH 36/62] build(deps): bump hypothesis from 6.41.0 to 6.46.3 (#4059) Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 6.41.0 to 6.46.3. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-6.41.0...hypothesis-python-6.46.3) --- updated-dependencies: - dependency-name: hypothesis dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- tests/requirements-linting.txt | 2 +- tests/requirements-testing.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index da19a20fcdf..5ce36b12257 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,7 +1,7 @@ ansi2html==1.6.0 flake8==4.0.1 flake8-quotes==3.3.1 -hypothesis==6.41.0 +hypothesis==6.46.3 markdown-include==0.6.0 mdx-truly-sane-lists==1.2 mkdocs==1.3.0 diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index f33bda7f724..fa72992b821 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -1,7 +1,7 @@ black==22.3.0 flake8==4.0.1 flake8-quotes==3.3.1 -hypothesis==6.41.0 +hypothesis==6.46.3 isort==5.10.1 mypy==0.942 pre-commit==2.17.0 diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index 5721960d3c4..ba68b3311c0 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -1,5 +1,5 @@ coverage==6.3.2 -hypothesis==6.41.0 +hypothesis==6.46.3 # 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.942 From dc07bc5a497a839f6c4ec3c8e937114f22e8c127 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 May 2022 18:56:21 +0100 Subject: [PATCH 37/62] build(deps): bump pre-commit from 2.17.0 to 2.19.0 (#4061) Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.17.0 to 2.19.0. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v2.17.0...v2.19.0) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- tests/requirements-linting.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index fa72992b821..4bfa013c456 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -4,7 +4,7 @@ flake8-quotes==3.3.1 hypothesis==6.46.3 isort==5.10.1 mypy==0.942 -pre-commit==2.17.0 +pre-commit==2.19.0 pycodestyle==2.8.0 pyflakes==2.4.0 twine==4.0.0 From 74403c2f15975a0dd491b17985e8d62a91267f0d Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 11 May 2022 19:00:37 +0100 Subject: [PATCH 38/62] test pyright with pydantic (#3972) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test pyright with pydantic * rename file to avoid pytest running it * try another name 😴 * add docs about BaseSettings and Field * add change --- .github/workflows/ci.yml | 9 ++++++ Makefile | 4 +++ changes/3972-samuelcolvin.md | 1 + docs/visual_studio_code.md | 48 ++++++++++++++++++++++++++++++-- tests/pyright/pyproject.toml | 4 +++ tests/pyright/pyright_example.py | 38 +++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 changes/3972-samuelcolvin.md create mode 100644 tests/pyright/pyproject.toml create mode 100644 tests/pyright/pyright_example.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3726206a075..eeb7556de15 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,15 @@ jobs: - name: check dist run: make check-dist + - name: install node for pyright + uses: actions/setup-node@v3 + with: + node-version: '14' + + - run: npm install -g pyright + + - run: make pyright + docs-build: runs-on: ubuntu-latest steps: diff --git a/Makefile b/Makefile index 46060befb65..cf846064eaf 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,10 @@ check-dist: mypy: mypy pydantic +.PHONY: pyright +pyright: + cd tests/pyright && pyright + .PHONY: test test: pytest --cov=pydantic diff --git a/changes/3972-samuelcolvin.md b/changes/3972-samuelcolvin.md new file mode 100644 index 00000000000..42f24982775 --- /dev/null +++ b/changes/3972-samuelcolvin.md @@ -0,0 +1 @@ +Typing checking with pyright in CI, improve docs on vscode/pylance/pyright. diff --git a/docs/visual_studio_code.md b/docs/visual_studio_code.md index 98c58fb01f4..6439a5d3dfe 100644 --- a/docs/visual_studio_code.md +++ b/docs/visual_studio_code.md @@ -130,10 +130,18 @@ Below are several techniques to achieve it. You can disable the errors for a specific line using a comment of: -``` +```py # type: ignore ``` +or (to be specific to pylance/pyright): + +```py +# pyright: ignore +``` + +([pyright](https://github.com/microsoft/pyright) is the language server used by Pylance.). + coming back to the example with `age='23'`, it would be: ```Python hl_lines="10" @@ -146,7 +154,7 @@ class Knight(BaseModel): color: str = 'blue' -lancelot = Knight(title='Sir Lancelot', age='23') # type: ignore +lancelot = Knight(title='Sir Lancelot', age='23') # pyright: ignore ``` that way Pylance and mypy will ignore errors in that line. @@ -243,10 +251,44 @@ The specific configuration **`frozen`** (in beta) has a special meaning. It prevents other code from changing a model instance once it's created, keeping it **"frozen"**. -When using the second version to declare `frozen=True` (with **keyword arguments** in the class definition), Pylance can use it to help you check in your code and **detect errors** when something is trying to set values in a model that is "frozen". +When using the second version to declare `frozen=True` (with **keyword arguments** in the class definition), +Pylance can use it to help you check in your code and **detect errors** when something is trying to set values +in a model that is "frozen". ![VS Code strict type errors with model](./img/vs_code_08.png) +## BaseSettings and ignoring Pylance/pyright errors + +Pylance/pyright does not work well with [`BaseSettings`](./usage/settings.md) - fields in settings classes can be +configured via environment variables and therefore "required" fields do not have to be explicitly set when +initialising a settings instance. However, pyright considers these fields as "required" and will therefore +show an error when they're not set. + +See [#3753](https://github.com/samuelcolvin/pydantic/issues/3753#issuecomment-1087417884) for an explanation of the +reasons behind this, and why we can't avoid the problem. + +There are two potential workarounds: + +* use an ignore comment (`# pylance: ignore`) when initialising `settings` +* or, use `settings.parse_obj({})` to avoid the warning + +## Adding a default with `Field` + +Pylance/pyright requires `default` to be a keyword argument to `Field` in order to infer that the field is optional. + +```py +from pydantic import BaseModel, Field + + +class Knight(BaseModel): + title: str = Field(default='Sir Lancelot') # this is okay + age: int = Field(23) # this works fine at runtime but will case an error for pyright + +lance = Knight() # error: Argument missing for parameter "age" +``` + +Like the issue with `BaseSettings`, this is a limitation of dataclass transforms and cannot be fixed in pydantic. + ## Technical Details !!! warning diff --git a/tests/pyright/pyproject.toml b/tests/pyright/pyproject.toml new file mode 100644 index 00000000000..991559aeafd --- /dev/null +++ b/tests/pyright/pyproject.toml @@ -0,0 +1,4 @@ +[tool.pyright] +extraPaths = ['../..'] +reportUnnecessaryTypeIgnoreComment = true +pythonVersion = '3.10' diff --git a/tests/pyright/pyright_example.py b/tests/pyright/pyright_example.py new file mode 100644 index 00000000000..0819afc3c4b --- /dev/null +++ b/tests/pyright/pyright_example.py @@ -0,0 +1,38 @@ +""" +This file is used to test pyright's ability to check pydantic code. + +In particular pydantic provides the `@__dataclass_transform__` for `BaseModel` +and all subclasses (including `BaseSettings`), see #2721. +""" + +from typing import List + +from pydantic import BaseModel, BaseSettings, Field + + +class MyModel(BaseModel): + x: str + y: List[int] + + +m1 = MyModel(x='hello', y=[1, 2, 3]) + +m2 = MyModel(x='hello') # pyright: ignore + + +class Knight(BaseModel): + title: str = Field(default='Sir Lancelot') # this is okay + age: int = Field(23) # this works fine at runtime but will case an error for pyright + + +k = Knight() # pyright: ignore + + +class Settings(BaseSettings): + x: str + y: int + + +s1 = Settings.parse_obj({}) + +s2 = Settings() # pyright: ignore[reportGeneralTypeIssues] From faee3301eb2c0d4157150a2f4cde2b4edb32ac8e Mon Sep 17 00:00:00 2001 From: Luis R Date: Wed, 11 May 2022 20:02:37 +0200 Subject: [PATCH 39/62] Fix regression in handling of nested dataclasses in `get_flat_models_from_field` (#3819) * add test for nested python dataclass schema generation * fix handling of dataclasses in `get_flat_models_from_field` * add change note --- changes/3819-himbeles.md | 1 + pydantic/schema.py | 5 ++++- tests/test_schema.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 changes/3819-himbeles.md diff --git a/changes/3819-himbeles.md b/changes/3819-himbeles.md new file mode 100644 index 00000000000..7845d7b0f93 --- /dev/null +++ b/changes/3819-himbeles.md @@ -0,0 +1 @@ +Fix nested Python dataclass schema regression in version 1.9 diff --git a/pydantic/schema.py b/pydantic/schema.py index e979678c229..b608d54b76b 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -419,10 +419,13 @@ def get_flat_models_from_field(field: ModelField, known_models: TypeModelSet) -> # Handle dataclass-based models if is_builtin_dataclass(field.type_): field.type_ = dataclass(field.type_) + was_dataclass = True + else: + was_dataclass = False field_type = field.type_ if lenient_issubclass(getattr(field_type, '__pydantic_model__', None), BaseModel): field_type = field_type.__pydantic_model__ - if field.sub_fields and not lenient_issubclass(field_type, BaseModel): + if field.sub_fields and (not lenient_issubclass(field_type, BaseModel) or was_dataclass): flat_models |= get_flat_models_from_fields(field.sub_fields, known_models=known_models) elif lenient_issubclass(field_type, BaseModel) and field_type not in known_models: flat_models |= get_flat_models_from_model(field_type, known_models=known_models) diff --git a/tests/test_schema.py b/tests/test_schema.py index 83888310923..4d3d4a49970 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -2884,3 +2884,34 @@ class Model(BaseModel): }, }, } + + +def test_nested_python_dataclasses(): + """ + Test schema generation for nested python dataclasses + """ + + from dataclasses import dataclass as python_dataclass + + @python_dataclass + class ChildModel: + name: str + + @python_dataclass + class NestedModel: + child: List[ChildModel] + + assert model_schema(dataclass(NestedModel)) == { + 'title': 'NestedModel', + 'type': 'object', + 'properties': {'child': {'title': 'Child', 'type': 'array', 'items': {'$ref': '#/definitions/ChildModel'}}}, + 'required': ['child'], + 'definitions': { + 'ChildModel': { + 'title': 'ChildModel', + 'type': 'object', + 'properties': {'name': {'title': 'Name', 'type': 'string'}}, + 'required': ['name'], + } + }, + } From 42acd8f8d2bd22f114e589672a054fe07a769e45 Mon Sep 17 00:00:00 2001 From: Yurii Karabas <1998uriyyo@gmail.com> Date: Wed, 11 May 2022 21:09:13 +0300 Subject: [PATCH 40/62] Fix issue with self-referencing dataclass (#3713) * Fix issue with self-referencing dataclass * Fix mypy issue --- changes/3675-uriyyo.md | 1 + pydantic/dataclasses.py | 9 ++++++++- pydantic/generics.py | 1 + pydantic/main.py | 16 ++++++++++++---- tests/test_dataclasses.py | 8 ++++++++ 5 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 changes/3675-uriyyo.md diff --git a/changes/3675-uriyyo.md b/changes/3675-uriyyo.md new file mode 100644 index 00000000000..7a34d4d2064 --- /dev/null +++ b/changes/3675-uriyyo.md @@ -0,0 +1 @@ +Fix issue with self-referencing dataclass diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py index 12d4c588a58..ac8fd6d89cb 100644 --- a/pydantic/dataclasses.py +++ b/pydantic/dataclasses.py @@ -184,7 +184,12 @@ def _process_class( validators = gather_all_validators(cls) cls.__pydantic_model__ = create_model( - cls.__name__, __config__=config, __module__=_cls.__module__, __validators__=validators, **field_definitions + cls.__name__, + __config__=config, + __module__=_cls.__module__, + __validators__=validators, + __cls_kwargs__={'__resolve_forward_refs__': False}, + **field_definitions, ) cls.__initialised__ = False @@ -196,6 +201,8 @@ def _process_class( if cls.__pydantic_model__.__config__.validate_assignment and not frozen: cls.__setattr__ = setattr_validate_assignment # type: ignore[assignment] + cls.__pydantic_model__.__try_update_forward_refs__(**{cls.__name__: cls}) + return cls diff --git a/pydantic/generics.py b/pydantic/generics.py index a712d26f2fc..baad72cbf73 100644 --- a/pydantic/generics.py +++ b/pydantic/generics.py @@ -98,6 +98,7 @@ def __class_getitem__(cls: Type[GenericModelT], params: Union[Type[Any], Tuple[T __base__=(cls,) + tuple(cls.__parameterized_bases__(typevars_map)), __config__=None, __validators__=validators, + __cls_kwargs__=None, **fields, ), ) diff --git a/pydantic/main.py b/pydantic/main.py index 1dcda9efd55..7afcafc4978 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -154,6 +154,7 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901 class_vars.update(base.__class_vars__) hash_func = base.__hash__ + resolve_forward_refs = kwargs.pop('__resolve_forward_refs__', True) allowed_config_kwargs: SetStr = { key for key in dir(config) @@ -289,7 +290,8 @@ def is_untouched(v: Any) -> bool: 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__() + if resolve_forward_refs: + cls.__try_update_forward_refs__() return cls @@ -765,12 +767,12 @@ def _get_value( return v @classmethod - def __try_update_forward_refs__(cls) -> None: + def __try_update_forward_refs__(cls, **localns: Any) -> 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(), cls.__config__.json_encoders, {}, (NameError,)) + update_model_forward_refs(cls, cls.__fields__.values(), cls.__config__.json_encoders, localns, (NameError,)) @classmethod def update_forward_refs(cls, **localns: Any) -> None: @@ -892,6 +894,7 @@ def create_model( __base__: None = None, __module__: str = __name__, __validators__: Dict[str, 'AnyClassMethod'] = None, + __cls_kwargs__: Dict[str, Any] = None, **field_definitions: Any, ) -> Type['BaseModel']: ... @@ -905,6 +908,7 @@ def create_model( __base__: Union[Type['Model'], Tuple[Type['Model'], ...]], __module__: str = __name__, __validators__: Dict[str, 'AnyClassMethod'] = None, + __cls_kwargs__: Dict[str, Any] = None, **field_definitions: Any, ) -> Type['Model']: ... @@ -917,6 +921,7 @@ def create_model( __base__: Union[None, Type['Model'], Tuple[Type['Model'], ...]] = None, __module__: str = __name__, __validators__: Dict[str, 'AnyClassMethod'] = None, + __cls_kwargs__: Dict[str, Any] = None, **field_definitions: Any, ) -> Type['Model']: """ @@ -926,6 +931,7 @@ def create_model( :param __base__: base class for the new model to inherit from :param __module__: module of the created model :param __validators__: a dict of method names and @validator class methods + :param __cls_kwargs__: a dict for class creation :param field_definitions: fields of the model (or extra fields if a base is supplied) in the format `=(, )` or `=, e.g. `foobar=(str, ...)` or `foobar=123`, or, for complex use-cases, in the format @@ -940,6 +946,8 @@ def create_model( else: __base__ = (cast(Type['Model'], BaseModel),) + __cls_kwargs__ = __cls_kwargs__ or {} + fields = {} annotations = {} @@ -969,7 +977,7 @@ def create_model( if __config__: namespace['Config'] = inherit_config(__config__, BaseConfig) - return type(__model_name, __base__, namespace) + return type(__model_name, __base__, namespace, **__cls_kwargs__) _missing = object() diff --git a/tests/test_dataclasses.py b/tests/test_dataclasses.py index e99a9c72343..ef5968ce07f 100644 --- a/tests/test_dataclasses.py +++ b/tests/test_dataclasses.py @@ -989,3 +989,11 @@ def __new__(cls, *args, **kwargs): instance = cls(a=test_string) assert instance._special_property == 1 assert instance.a == test_string + + +def test_self_reference_dataclass(): + @pydantic.dataclasses.dataclass + class MyDataclass: + self_reference: 'MyDataclass' + + assert MyDataclass.__pydantic_model__.__fields__['self_reference'].type_ is MyDataclass From a45276d6b1c0dd7c10d46767bb19954401b3b04a Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Fri, 13 May 2022 15:06:53 +0100 Subject: [PATCH 41/62] guard against ClassVar in fields (#4064) * guard against ClassVar in fields, fix #3679 * fix linting * skipif for test_class_var_forward_ref --- changes/3679-samuelcolvin.md | 1 + pydantic/fields.py | 3 +++ tests/test_forward_ref.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 changes/3679-samuelcolvin.md diff --git a/changes/3679-samuelcolvin.md b/changes/3679-samuelcolvin.md new file mode 100644 index 00000000000..ac589451f5c --- /dev/null +++ b/changes/3679-samuelcolvin.md @@ -0,0 +1 @@ +Allow self referencing `ClassVar`s in models but checking for class vars after forward refs are resolved. diff --git a/pydantic/fields.py b/pydantic/fields.py index e6736f0239b..8890ede93e1 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -37,6 +37,7 @@ display_as_type, get_args, get_origin, + is_classvar, is_literal_type, is_new_type, is_none_type, @@ -611,6 +612,8 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) return elif origin is Callable: return + elif is_classvar(origin): + return elif is_union(origin): types_ = [] for type_ in get_args(self.type_): diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py index 61e7be5cfb2..969c3f59bcd 100644 --- a/tests/test_forward_ref.py +++ b/tests/test_forward_ref.py @@ -1,3 +1,4 @@ +import sys from typing import Optional, Tuple import pytest @@ -669,3 +670,19 @@ class Config: m = module.User(name='anne', friends=[{'name': 'ben'}, {'name': 'charlie'}]) assert m.json(models_as_dict=False) == '{"name": "anne", "friends": ["User(ben)", "User(charlie)"]}' + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason='needs 3.9 or newer') +def test_class_var_forward_ref(create_module): + # see #3679 + create_module( + # language=Python + """ +from __future__ import annotations +from typing import ClassVar +from pydantic import BaseModel + +class WithClassVar(BaseModel): + Instances: ClassVar[dict[str, WithClassVar]] = {} +""" + ) From e5540296cc1690e3807d99c46f8a99b908735d38 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Fri, 13 May 2022 09:16:32 -0500 Subject: [PATCH 42/62] Fix issue with in-place modification of FieldInfo (#4067) * Fix info with in-place modification of field info * add changes * add test for 3714 * Update changes/4067-adriangb.md Co-authored-by: Samuel Colvin Co-authored-by: Samuel Colvin --- changes/4067-adriangb.md | 1 + pydantic/fields.py | 2 ++ tests/test_annotated.py | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 changes/4067-adriangb.md diff --git a/changes/4067-adriangb.md b/changes/4067-adriangb.md new file mode 100644 index 00000000000..4c6689ebe5e --- /dev/null +++ b/changes/4067-adriangb.md @@ -0,0 +1 @@ +Fix in-place modification of `FieldInfo` that caused problems with PEP 593 type aliases diff --git a/pydantic/fields.py b/pydantic/fields.py index 8890ede93e1..081f92213e9 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -1,3 +1,4 @@ +import copy from collections import Counter as CollectionCounter, defaultdict, deque from collections.abc import Hashable as CollectionsHashable, Iterable as CollectionsIterable from typing import ( @@ -447,6 +448,7 @@ def _get_field_info( raise ValueError(f'cannot specify multiple `Annotated` `Field`s for {field_name!r}') field_info = next(iter(field_infos), None) if field_info is not None: + field_info = copy.copy(field_info) field_info.update_from_config(field_info_from_config) if field_info.default is not Undefined: raise ValueError(f'`Field` default cannot be set in `Annotated` for {field_name!r}') diff --git a/tests/test_annotated.py b/tests/test_annotated.py index b5c27a8c52f..bf2cad4b882 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -1,3 +1,5 @@ +from typing import List + import pytest from typing_extensions import Annotated @@ -132,3 +134,21 @@ class Config: assert Foo.schema(by_alias=True)['properties'] == { 'a': {'title': 'A', 'description': 'descr', 'foobar': 'hello', 'type': 'integer'}, } + + +def test_annotated_alias() -> None: + # https://github.com/samuelcolvin/pydantic/issues/2971 + + StrAlias = Annotated[str, Field(max_length=3)] + IntAlias = Annotated[int, Field(default_factory=lambda: 2)] + + Nested = Annotated[List[StrAlias], Field(description='foo')] + + class MyModel(BaseModel): + a: StrAlias = 'abc' + b: StrAlias + c: IntAlias + d: IntAlias + e: Nested + + assert MyModel(b='def', e=['xyz']) == MyModel(a='abc', b='def', c=2, d=2, e=['xyz']) From 268588b08d3c857989889decfab86d41a49abf84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 13 May 2022 16:29:06 +0100 Subject: [PATCH 43/62] build(deps): bump mkdocs-material from 8.2.8 to 8.2.14 (#4063) Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.2.8 to 8.2.14. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.2.8...8.2.14) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 5ce36b12257..348505f4d92 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -6,7 +6,7 @@ markdown-include==0.6.0 mdx-truly-sane-lists==1.2 mkdocs==1.3.0 mkdocs-exclude==1.0.2 -mkdocs-material==8.2.12 +mkdocs-material==8.2.14 sqlalchemy orjson ujson From f4197103815fba6546b4a3a7e5cccdd8b5a8f3be Mon Sep 17 00:00:00 2001 From: Blake Naccarato Date: Fri, 13 May 2022 11:10:55 -0700 Subject: [PATCH 44/62] Fix typo `# pylance: ignore` > `# pyright: ignore` (#4072) In #3972, `# pyright: ignore` was added in multiple places in the docs, and `# pylance: ignore` only once. I believe it's a typo, as AFAIK such a typing ignore comment flag doesn't exist. --- docs/visual_studio_code.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/visual_studio_code.md b/docs/visual_studio_code.md index 6439a5d3dfe..7ec37b5930f 100644 --- a/docs/visual_studio_code.md +++ b/docs/visual_studio_code.md @@ -269,7 +269,7 @@ reasons behind this, and why we can't avoid the problem. There are two potential workarounds: -* use an ignore comment (`# pylance: ignore`) when initialising `settings` +* use an ignore comment (`# pyright: ignore`) when initialising `settings` * or, use `settings.parse_obj({})` to avoid the warning ## Adding a default with `Field` From 8718db581def3e2a392439dcc4b99825e672ac6f Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 14 May 2022 07:26:34 +0100 Subject: [PATCH 45/62] Fix JSON Schema generation for Discriminated Unions within lists. (#4071) * Fix JSON Schema generation for Discriminated Unions within lists. * linting * fix mypy --- changes/3608-samuelcolvin.md | 1 + pydantic/schema.py | 73 ++++++++++++++++-------------- tests/test_schema.py | 86 ++++++++++++++++++++++++++++++++++++ 3 files changed, 127 insertions(+), 33 deletions(-) create mode 100644 changes/3608-samuelcolvin.md diff --git a/changes/3608-samuelcolvin.md b/changes/3608-samuelcolvin.md new file mode 100644 index 00000000000..ec3c0bafca0 --- /dev/null +++ b/changes/3608-samuelcolvin.md @@ -0,0 +1 @@ +Fix JSON Schema generation for Discriminated Unions within lists. diff --git a/pydantic/schema.py b/pydantic/schema.py index b608d54b76b..d37e82206c2 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -265,36 +265,6 @@ def field_schema( known_models=known_models or set(), ) - # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminator-object - if field.discriminator_key is not None: - assert field.sub_fields_mapping is not None - - discriminator_models_refs: Dict[str, Union[str, Dict[str, Any]]] = {} - - for discriminator_value, sub_field in field.sub_fields_mapping.items(): - # sub_field is either a `BaseModel` or directly an `Annotated` `Union` of many - if is_union(get_origin(sub_field.type_)): - sub_models = get_sub_types(sub_field.type_) - discriminator_models_refs[discriminator_value] = { - model_name_map[sub_model]: get_schema_ref( - model_name_map[sub_model], ref_prefix, ref_template, False - ) - for sub_model in sub_models - } - else: - sub_field_type = sub_field.type_ - if hasattr(sub_field_type, '__pydantic_model__'): - sub_field_type = sub_field_type.__pydantic_model__ - - discriminator_model_name = model_name_map[sub_field_type] - discriminator_model_ref = get_schema_ref(discriminator_model_name, ref_prefix, ref_template, False) - discriminator_models_refs[discriminator_value] = discriminator_model_ref['$ref'] - - s['discriminator'] = { - 'propertyName': field.discriminator_alias, - 'mapping': discriminator_models_refs, - } - # $ref will only be returned when there are no schema_overrides if '$ref' in f_schema: return f_schema, f_definitions, f_nested_models @@ -718,7 +688,7 @@ def enum_process_schema(enum: Type[Enum], *, field: Optional[ModelField] = None) def field_singleton_sub_fields_schema( - sub_fields: Sequence[ModelField], + field: ModelField, *, by_alias: bool, model_name_map: Dict[TypeModelOrEnum, str], @@ -733,6 +703,7 @@ def field_singleton_sub_fields_schema( Take a list of Pydantic ``ModelField`` from the declaration of a type with parameters, and generate their schema. I.e., fields used as "type parameters", like ``str`` and ``int`` in ``Tuple[str, int]``. """ + sub_fields = cast(List[ModelField], field.sub_fields) definitions = {} nested_models: Set[str] = set() if len(sub_fields) == 1: @@ -746,6 +717,37 @@ def field_singleton_sub_fields_schema( known_models=known_models, ) else: + s: Dict[str, Any] = {} + # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.2.md#discriminator-object + if field.discriminator_key is not None: + assert field.sub_fields_mapping is not None + + discriminator_models_refs: Dict[str, Union[str, Dict[str, Any]]] = {} + + for discriminator_value, sub_field in field.sub_fields_mapping.items(): + # sub_field is either a `BaseModel` or directly an `Annotated` `Union` of many + if is_union(get_origin(sub_field.type_)): + sub_models = get_sub_types(sub_field.type_) + discriminator_models_refs[discriminator_value] = { + model_name_map[sub_model]: get_schema_ref( + model_name_map[sub_model], ref_prefix, ref_template, False + ) + for sub_model in sub_models + } + else: + sub_field_type = sub_field.type_ + if hasattr(sub_field_type, '__pydantic_model__'): + sub_field_type = sub_field_type.__pydantic_model__ + + discriminator_model_name = model_name_map[sub_field_type] + discriminator_model_ref = get_schema_ref(discriminator_model_name, ref_prefix, ref_template, False) + discriminator_models_refs[discriminator_value] = discriminator_model_ref['$ref'] + + s['discriminator'] = { + 'propertyName': field.discriminator_alias, + 'mapping': discriminator_models_refs, + } + sub_field_schemas = [] for sf in sub_fields: sub_schema, sub_definitions, sub_nested_models = field_type_schema( @@ -763,9 +765,14 @@ def field_singleton_sub_fields_schema( # object. Otherwise we will end up with several allOf inside anyOf. # See https://github.com/samuelcolvin/pydantic/issues/1209 sub_schema = sub_schema['allOf'][0] + + if sub_schema.keys() == {'discriminator', 'anyOf'}: + # we don't want discriminator information inside anyOf choices, this is dealt with elsewhere + sub_schema.pop('discriminator') sub_field_schemas.append(sub_schema) nested_models.update(sub_nested_models) - return {'anyOf': sub_field_schemas}, definitions, nested_models + s['anyOf'] = sub_field_schemas + return s, definitions, nested_models # Order is important, e.g. subclasses of str must go before str @@ -849,7 +856,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) (field.field_info and field.field_info.const) or not lenient_issubclass(field_type, BaseModel) ): return field_singleton_sub_fields_schema( - field.sub_fields, + field, by_alias=by_alias, model_name_map=model_name_map, schema_overrides=schema_overrides, diff --git a/tests/test_schema.py b/tests/test_schema.py index 4d3d4a49970..a483e3fad1f 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -2915,3 +2915,89 @@ class NestedModel: } }, } + + +def test_discriminated_union_in_list(): + class BlackCat(BaseModel): + pet_type: Literal['cat'] + color: Literal['black'] + black_name: str + + class WhiteCat(BaseModel): + pet_type: Literal['cat'] + color: Literal['white'] + white_name: str + + Cat = Annotated[Union[BlackCat, WhiteCat], Field(discriminator='color')] + + class Dog(BaseModel): + pet_type: Literal['dog'] + name: str + + Pet = Annotated[Union[Cat, Dog], Field(discriminator='pet_type')] + + class Model(BaseModel): + pets: Pet + n: int + + assert Model.schema() == { + 'title': 'Model', + 'type': 'object', + 'properties': { + 'pets': { + 'title': 'Pets', + 'discriminator': { + 'propertyName': 'pet_type', + 'mapping': { + 'cat': { + 'BlackCat': {'$ref': '#/definitions/BlackCat'}, + 'WhiteCat': {'$ref': '#/definitions/WhiteCat'}, + }, + 'dog': '#/definitions/Dog', + }, + }, + 'anyOf': [ + { + 'anyOf': [ + {'$ref': '#/definitions/BlackCat'}, + {'$ref': '#/definitions/WhiteCat'}, + ], + }, + {'$ref': '#/definitions/Dog'}, + ], + }, + 'n': {'title': 'N', 'type': 'integer'}, + }, + 'required': ['pets', 'n'], + 'definitions': { + 'BlackCat': { + 'title': 'BlackCat', + 'type': 'object', + 'properties': { + 'pet_type': {'title': 'Pet Type', 'enum': ['cat'], 'type': 'string'}, + 'color': {'title': 'Color', 'enum': ['black'], 'type': 'string'}, + 'black_name': {'title': 'Black Name', 'type': 'string'}, + }, + 'required': ['pet_type', 'color', 'black_name'], + }, + 'WhiteCat': { + 'title': 'WhiteCat', + 'type': 'object', + 'properties': { + 'pet_type': {'title': 'Pet Type', 'enum': ['cat'], 'type': 'string'}, + 'color': {'title': 'Color', 'enum': ['white'], 'type': 'string'}, + 'white_name': {'title': 'White Name', 'type': 'string'}, + }, + 'required': ['pet_type', 'color', 'white_name'], + }, + 'Dog': { + 'title': 'Dog', + 'type': 'object', + 'properties': { + 'pet_type': {'title': 'Pet Type', 'enum': ['dog'], 'type': 'string'}, + 'name': {'title': 'Name', 'type': 'string'}, + }, + 'required': ['pet_type', 'name'], + }, + }, + } From 3e9cd71325cc193551db4b156df3e32076d484f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 14 May 2022 09:32:54 +0100 Subject: [PATCH 46/62] build(deps): bump mypy from 0.942 to 0.950 (#4062) * build(deps): bump mypy from 0.942 to 0.950 Bumps [mypy](https://github.com/python/mypy) from 0.942 to 0.950. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.942...v0.950) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * ignore ChainMap type Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Samuel Colvin --- pydantic/class_validators.py | 2 +- tests/requirements-linting.txt | 2 +- tests/requirements-testing.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pydantic/class_validators.py b/pydantic/class_validators.py index ecaeef393cc..a9cd0dbab90 100644 --- a/pydantic/class_validators.py +++ b/pydantic/class_validators.py @@ -329,7 +329,7 @@ def _generic_validator_basic(validator: AnyCallable, sig: 'Signature', args: Set def gather_all_validators(type_: 'ModelOrDc') -> Dict[str, 'AnyClassMethod']: - all_attributes = ChainMap(*[cls.__dict__ for cls in type_.__mro__]) + all_attributes = ChainMap(*[cls.__dict__ for cls in type_.__mro__]) # type: ignore[arg-type,var-annotated] return { k: v for k, v in all_attributes.items() diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index 4bfa013c456..2574a34b083 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -3,7 +3,7 @@ flake8==4.0.1 flake8-quotes==3.3.1 hypothesis==6.46.3 isort==5.10.1 -mypy==0.942 +mypy==0.950 pre-commit==2.19.0 pycodestyle==2.8.0 pyflakes==2.4.0 diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index ba68b3311c0..47ca8d1eefb 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -2,7 +2,7 @@ coverage==6.3.2 hypothesis==6.46.3 # 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.942 +mypy==0.950 pytest==7.1.2 pytest-cov==3.0.0 pytest-mock==3.7.0 From b246bc68596c46fecadea67c794a9febad7bcbe6 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 14 May 2022 12:31:29 +0100 Subject: [PATCH 47/62] update mypy ci (#4073) * update mypy ci * test against older mypy --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eeb7556de15..9900407ec70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,7 +179,7 @@ jobs: strategy: fail-fast: false matrix: - mypy-version: ['0.910', '0.920', '0.921'] + mypy-version: ['0.910', '0.921', '0.931', '0.942', '0.950'] steps: - uses: actions/checkout@v3 From 9baec86270374d7ebcb54cbdf3750542f7a60bbd Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 14 May 2022 15:32:53 +0100 Subject: [PATCH 48/62] try speeding up ci using caching and rearranging jobs (#3974) * try speeding up ci using caching and rearranging jobs * fix job references * cache lint and docs install * tweak caching * tweak caching to avoid conflicts * correct use of runner.os * bump * stop skipping steps depending on cache * prevent hypothesis flakeyness * try skipping install steps again * remove skipping install :-( --- .github/workflows/ci.yml | 117 +++++++++++++++++--------- .github/workflows/upload-previews.yml | 2 +- tests/test_hypothesis_plugin.py | 2 +- 3 files changed, 81 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9900407ec70..838897946e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,20 @@ jobs: with: python-version: 3.9 + - uses: actions/cache@v3 + id: cache + with: + path: | + ${{ env.pythonLocation }} + .mypy_cache + key: > + lint + ${{ runner.os }} + ${{ env.pythonLocation }} + ${{ hashFiles('tests/requirements-linting.txt') }} + - name: install + if: steps.cache.outputs.cache-hit != 'true' run: | make install-linting pip freeze @@ -54,9 +67,22 @@ jobs: - name: set up python uses: actions/setup-python@v3 with: - python-version: 3.8 + python-version: 3.9 + + - uses: actions/cache@v3 + id: cache + with: + path: ${{ env.pythonLocation }} + key: > + docs-build + ${{ runner.os }} + ${{ env.pythonLocation }} + ${{ hashFiles('setup.py') }} + ${{ hashFiles('requirements.txt') }} + ${{ hashFiles('docs/requirements.txt') }} - name: install + if: steps.cache.outputs.cache-hit != 'true' run: make install-docs - name: build site @@ -68,8 +94,8 @@ jobs: name: docs path: site - test-linux: - name: test py${{ matrix.python-version }} on linux + test-linux-compiled: + name: test py${{ matrix.python-version }} on linux compiled runs-on: ubuntu-latest strategy: fail-fast: false @@ -87,10 +113,20 @@ jobs: with: python-version: ${{ matrix.python-version }} + - uses: actions/cache@v3 + id: cache + with: + path: ${{ env.pythonLocation }} + key: > + test-linux-compiled + ${{ runner.os }} + ${{ env.pythonLocation }} + ${{ hashFiles('setup.py') }} + ${{ hashFiles('requirements.txt') }} + ${{ hashFiles('tests/requirements-testing.txt') }} + - name: install - run: | - make install-testing - pip freeze + run: make install-testing - name: compile run: | @@ -101,32 +137,11 @@ jobs: - run: mkdir coverage - - name: test compiled and deps - run: make test - env: - COVERAGE_FILE: coverage/.coverage.linux-py${{ matrix.python-version }}-cY-dY - CONTEXT: linux-py${{ matrix.python-version }}-compiled-yes-deps-yes - - - name: uninstall deps - run: pip uninstall -y cython email-validator devtools python-dotenv - - - name: test compiled without deps - run: make test - env: - COVERAGE_FILE: coverage/.coverage.linux-py${{ matrix.python-version }}-cY-dN - CONTEXT: linux-py${{ matrix.python-version }}-compiled-yes-deps-no - - - name: remove compiled binaries - run: | - rm -r pydantic/*.so pydantic/*.c pydantic/__pycache__ - ls -alh - ls -alh pydantic/ - - - name: test uncompiled without deps + - name: test run: make test env: - COVERAGE_FILE: coverage/.coverage.linux-py${{ matrix.python-version }}-cN-dN - CONTEXT: linux-py${{ matrix.python-version }}-compiled-no-deps-no + COVERAGE_FILE: coverage/.coverage.linux-py${{ matrix.python-version }}-compiled + CONTEXT: linux-py${{ matrix.python-version }}-compiled - name: store coverage files uses: actions/upload-artifact@v3 @@ -134,13 +149,16 @@ jobs: name: coverage path: coverage - test-windows-mac: + test-not-compiled: name: test py${{ matrix.python-version }} on ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [macos, windows] + os: [ubuntu, macos, windows] python-version: ['3.7', '3.8', '3.9', '3.10'] + include: + - os: ubuntu + env: PYTHON: ${{ matrix.python-version }} OS: ${{ matrix.os }} @@ -156,16 +174,39 @@ jobs: with: python-version: ${{ matrix.python-version }} + - uses: actions/cache@v3 + id: cache + with: + path: ${{ env.pythonLocation }} + key: > + test-not-compiled + ${{ runner.os }} + ${{ env.pythonLocation }} + ${{ hashFiles('setup.py') }} + ${{ hashFiles('requirements.txt') }} + ${{ hashFiles('tests/requirements-testing.txt') }} + - name: install run: make install-testing + - run: pip freeze + - run: mkdir coverage - - name: test + - name: test with deps + run: make test + env: + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-with-deps + CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-with-deps + + - name: uninstall deps + run: pip uninstall -y cython email-validator devtools python-dotenv + + - name: test without deps run: make test env: - COVERAGE_FILE: coverage/.coverage.${{ matrix.os }}-py${{ matrix.python-version }} - CONTEXT: ${{ matrix.os }}-py${{ matrix.python-version }} + COVERAGE_FILE: coverage/.coverage.${{ runner.os }}-py${{ matrix.python-version }}-without-deps + CONTEXT: ${{ runner.os }}-py${{ matrix.python-version }}-without-deps - name: store coverage files uses: actions/upload-artifact@v3 @@ -215,7 +256,7 @@ jobs: path: coverage coverage-combine: - needs: [test-linux, test-windows-mac, test-old-mypy] + needs: [test-linux-compiled, test-not-compiled, test-old-mypy] runs-on: ubuntu-latest steps: @@ -233,7 +274,7 @@ jobs: - run: pip install coverage - - run: ls -la + - run: ls -la coverage - run: coverage combine coverage - run: coverage report - run: coverage html --show-contexts @@ -263,7 +304,7 @@ jobs: build: name: build py3.${{ matrix.python-version }} on ${{ matrix.platform || matrix.os }} - needs: [lint, test-linux, test-windows-mac, test-old-mypy, test-fastapi] + needs: [lint, test-linux-compiled, test-not-compiled, test-old-mypy, test-fastapi] if: "success() && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/master')" strategy: fail-fast: false diff --git a/.github/workflows/upload-previews.yml b/.github/workflows/upload-previews.yml index 0bec37843b5..2ebcf4bb026 100644 --- a/.github/workflows/upload-previews.yml +++ b/.github/workflows/upload-previews.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/setup-python@v3 with: - python-version: '3.8' + python-version: '3.9' - run: pip install smokeshow diff --git a/tests/test_hypothesis_plugin.py b/tests/test_hypothesis_plugin.py index 95dc59c979e..ad3538826d8 100644 --- a/tests/test_hypothesis_plugin.py +++ b/tests/test_hypothesis_plugin.py @@ -108,7 +108,7 @@ class EmailsModel(pydantic.BaseModel): @pytest.mark.parametrize('model', gen_models()) -@settings(suppress_health_check={HealthCheck.too_slow}) +@settings(suppress_health_check={HealthCheck.too_slow}, deadline=None) @given(data=st.data()) def test_can_construct_models_with_all_fields(data, model): # The value of this test is to confirm that Hypothesis knows how to provide From f69012a5aae2d22162dfbd17c3d9e77f5165204b Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 14 May 2022 18:26:12 +0200 Subject: [PATCH 49/62] fix: `error checking inheritance` when using PEP585 and PEP604 type hints (#3681) * Add tests * Fix the issue * Add changes file * Improved convert_generics * Add default fallback to convert_generics Improved Annotated and Literal handling * Fix Cython doesn't support generic types (PEP560) Watch cython issue cython/cython#2753 Previous implementation can be used after cython 3.0 release * Add custom type test * Cosmetic fixes Co-authored-by: Samuel Colvin * Fix typos * Add SelfReferencing test validation Add parametrization to * Fix: parametrization caused test discovery problem * Better explanation for a test case * Better assertions for model creation tests * Rerun CI Co-authored-by: Samuel Colvin --- changes/3681-aleksul.md | 1 + pydantic/fields.py | 3 +- pydantic/typing.py | 63 +++++++++++++++++++++++++++++++++++++++ tests/test_forward_ref.py | 42 ++++++++++++++++++++++++++ tests/test_typing.py | 62 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 changes/3681-aleksul.md diff --git a/changes/3681-aleksul.md b/changes/3681-aleksul.md new file mode 100644 index 00000000000..13f745ac7d3 --- /dev/null +++ b/changes/3681-aleksul.md @@ -0,0 +1 @@ +Fixed "error checking inheritance of" when using PEP585 and PEP604 type hints diff --git a/pydantic/fields.py b/pydantic/fields.py index 081f92213e9..3d8a0e1816a 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -35,6 +35,7 @@ Callable, ForwardRef, NoArgAnyCallable, + convert_generics, display_as_type, get_args, get_origin, @@ -396,7 +397,7 @@ def __init__( self.name: str = name self.has_alias: bool = bool(alias) self.alias: str = alias or name - self.type_: Any = type_ + self.type_: Any = convert_generics(type_) self.outer_type_: Any = type_ self.class_validators = class_validators or {} self.default: Any = default diff --git a/pydantic/typing.py b/pydantic/typing.py index c6e9da18ff7..11c59323e03 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -38,6 +38,12 @@ # python < 3.9 does not have GenericAlias (list[int], tuple[str, ...] and so on) TypingGenericAlias = () +try: + from types import UnionType as TypesUnionType # type: ignore +except ImportError: + # python < 3.10 does not have UnionType (str | int, byte | bool and so on) + TypesUnionType = () + if sys.version_info < (3, 9): @@ -145,6 +151,63 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]: return _typing_get_args(tp) or getattr(tp, '__args__', ()) or _generic_get_args(tp) +if sys.version_info < (3, 9): + + def convert_generics(tp: Type[Any]) -> Type[Any]: + """Python 3.9 and older only supports generics from `typing` module. + They convert strings to ForwardRef automatically. + + Examples:: + typing.List['Hero'] == typing.List[ForwardRef('Hero')] + """ + return tp + +else: + from typing import _UnionGenericAlias # type: ignore + + from typing_extensions import _AnnotatedAlias + + def convert_generics(tp: Type[Any]) -> Type[Any]: + """ + Recursively searches for `str` type hints and replaces them with ForwardRef. + + Examples:: + convert_generics(list['Hero']) == list[ForwardRef('Hero')] + convert_generics(dict['Hero', 'Team']) == dict[ForwardRef('Hero'), ForwardRef('Team')] + convert_generics(typing.Dict['Hero', 'Team']) == typing.Dict[ForwardRef('Hero'), ForwardRef('Team')] + convert_generics(list[str | 'Hero'] | int) == list[str | ForwardRef('Hero')] | int + """ + origin = get_origin(tp) + if not origin or not hasattr(tp, '__args__'): + return tp + + args = get_args(tp) + + # typing.Annotated needs special treatment + if origin is Annotated: + return _AnnotatedAlias(convert_generics(args[0]), args[1:]) + + # recursively replace `str` instances inside of `GenericAlias` with `ForwardRef(arg)` + converted = tuple( + ForwardRef(arg) if isinstance(arg, str) and isinstance(tp, TypingGenericAlias) else convert_generics(arg) + for arg in args + ) + + if converted == args: + return tp + elif isinstance(tp, TypingGenericAlias): + return TypingGenericAlias(origin, converted) + elif isinstance(tp, TypesUnionType): + # recreate types.UnionType (PEP604, Python >= 3.10) + return _UnionGenericAlias(origin, converted) + else: + try: + setattr(tp, '__args__', converted) + except AttributeError: + pass + return tp + + if sys.version_info < (3, 10): def is_union(tp: Optional[Type[Any]]) -> bool: diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py index 969c3f59bcd..54062154211 100644 --- a/tests/test_forward_ref.py +++ b/tests/test_forward_ref.py @@ -672,6 +672,48 @@ class Config: assert m.json(models_as_dict=False) == '{"name": "anne", "friends": ["User(ben)", "User(charlie)"]}' +skip_pep585 = pytest.mark.skipif( + sys.version_info < (3, 9), reason='PEP585 generics only supported for python 3.9 and above' +) + + +@skip_pep585 +def test_pep585_self_referencing_generics(): + class SelfReferencing(BaseModel): + names: list['SelfReferencing'] # noqa: F821 + + SelfReferencing.update_forward_refs() # will raise an exception if the forward ref isn't resolvable + # test the class + assert SelfReferencing.__fields__['names'].type_ is SelfReferencing + # NOTE: outer_type_ is not converted + assert SelfReferencing.__fields__['names'].outer_type_ == list['SelfReferencing'] + # test that object creation works + obj = SelfReferencing(names=[SelfReferencing(names=[])]) + assert obj.names == [SelfReferencing(names=[])] + + +@skip_pep585 +def test_pep585_recursive_generics(create_module): + @create_module + def module(): + from pydantic import BaseModel + + class Team(BaseModel): + name: str + heroes: list['Hero'] # noqa: F821 + + class Hero(BaseModel): + name: str + teams: list[Team] + + Team.update_forward_refs() + + assert module.Team.__fields__['heroes'].type_ is module.Hero + assert module.Hero.__fields__['teams'].type_ is module.Team + + module.Hero(name='Ivan', teams=[module.Team(name='TheBest', heroes=[])]) + + @pytest.mark.skipif(sys.version_info < (3, 9), reason='needs 3.9 or newer') def test_class_var_forward_ref(create_module): # see #3679 diff --git a/tests/test_typing.py b/tests/test_typing.py index af0b8d96955..9e5936574ef 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -1,9 +1,12 @@ +import sys from collections import namedtuple -from typing import Callable as TypingCallable, NamedTuple +from typing import Any, Callable as TypingCallable, Dict, ForwardRef, List, NamedTuple, NewType, Union # noqa: F401 import pytest +from typing_extensions import Annotated # noqa: F401 -from pydantic.typing import Literal, is_namedtuple, is_none_type, is_typeddict +from pydantic import Field # noqa: F401 +from pydantic.typing import Literal, convert_generics, is_namedtuple, is_none_type, is_typeddict try: from typing import TypedDict as typing_TypedDict @@ -66,3 +69,58 @@ def test_is_none_type(): # `collections.abc.Callable` (even with python >= 3.9) as they behave # differently assert is_none_type(TypingCallable) is False + + +class Hero: + pass + + +class Team: + pass + + +@pytest.mark.skipif(sys.version_info < (3, 9), reason='PEP585 generics only supported for python 3.9 and above.') +@pytest.mark.parametrize( + ['type_', 'expectations'], + [ + ('int', 'int'), + ('Union[list["Hero"], int]', 'Union[list[ForwardRef("Hero")], int]'), + ('list["Hero"]', 'list[ForwardRef("Hero")]'), + ('dict["Hero", "Team"]', 'dict[ForwardRef("Hero"), ForwardRef("Team")]'), + ('dict["Hero", list["Team"]]', 'dict[ForwardRef("Hero"), list[ForwardRef("Team")]]'), + ('dict["Hero", List["Team"]]', 'dict[ForwardRef("Hero"), List[ForwardRef("Team")]]'), + ('Dict["Hero", list["Team"]]', 'Dict[ForwardRef("Hero"), list[ForwardRef("Team")]]'), + ( + 'Annotated[list["Hero"], Field(min_length=2)]', + 'Annotated[list[ForwardRef("Hero")], Field(min_length=2)]', + ), + ], +) +def test_convert_generics(type_, expectations): + assert str(convert_generics(eval(type_))) == str(eval(expectations)) + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason='NewType class was added in python 3.10.') +def test_convert_generics_unsettable_args(): + class User(NewType): + + __origin__ = type(list[str]) + __args__ = (list['Hero'],) + + def __init__(self, name: str, tp: type) -> None: + super().__init__(name, tp) + + def __setattr__(self, __name: str, __value: Any) -> None: + if __name == '__args__': + raise AttributeError # will be thrown during the generics conversion + return super().__setattr__(__name, __value) + + # tests that convert_generics will not throw an exception even if __args__ isn't settable + assert convert_generics(User('MyUser', str)).__args__ == (list['Hero'],) + + +@pytest.mark.skipif(sys.version_info < (3, 10), reason='PEP604 unions only supported for python 3.10 and above.') +def test_convert_generics_pep604(): + assert ( + convert_generics(dict['Hero', list['Team']] | int) == dict[ForwardRef('Hero'), list[ForwardRef('Team')]] | int + ) From 122dd2f24e37216833dd28a617da5fd836a78cae Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 14 May 2022 17:27:52 +0100 Subject: [PATCH 50/62] fix ClassVars, better fix for #3679 (#4077) --- changes/3679-samuelcolvin.md | 2 +- pydantic/fields.py | 3 --- pydantic/typing.py | 10 +++++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/changes/3679-samuelcolvin.md b/changes/3679-samuelcolvin.md index ac589451f5c..e02ebc7febf 100644 --- a/changes/3679-samuelcolvin.md +++ b/changes/3679-samuelcolvin.md @@ -1 +1 @@ -Allow self referencing `ClassVar`s in models but checking for class vars after forward refs are resolved. +Allow self referencing `ClassVar`s in models. diff --git a/pydantic/fields.py b/pydantic/fields.py index 3d8a0e1816a..10360b8394a 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -39,7 +39,6 @@ display_as_type, get_args, get_origin, - is_classvar, is_literal_type, is_new_type, is_none_type, @@ -615,8 +614,6 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) return elif origin is Callable: return - elif is_classvar(origin): - return elif is_union(origin): types_ = [] for type_ in get_args(self.type_): diff --git a/pydantic/typing.py b/pydantic/typing.py index 11c59323e03..44fba36109c 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -450,7 +450,15 @@ def _check_classvar(v: Optional[Type[Any]]) -> bool: def is_classvar(ann_type: Type[Any]) -> bool: - return _check_classvar(ann_type) or _check_classvar(get_origin(ann_type)) + if _check_classvar(ann_type) or _check_classvar(get_origin(ann_type)): + return True + + # this is an ugly workaround for class vars that contain forward references and are therefore themselves + # forward references, see #3679 + if ann_type.__class__ == ForwardRef and ann_type.__forward_arg__.startswith('ClassVar['): + return True + + return False def update_field_forward_refs(field: 'ModelField', globalns: Any, localns: Any) -> None: From cc54acb612cb5144a34caebb9ac143324a0cb4a1 Mon Sep 17 00:00:00 2001 From: Maxim Martynov Date: Sat, 14 May 2022 19:35:39 +0300 Subject: [PATCH 51/62] Fix missing port in HttpUrl.build() result (#3652) * Port number is no longer being ignored by HttpUrl.build() * Update tests/test_networks.py Co-authored-by: Samuel Colvin * Update networks.py * Update tests/test_networks.py Co-authored-by: Samuel Colvin * Update test_networks.py * Update test_networks.py * update change description Co-authored-by: Samuel Colvin Co-authored-by: Samuel Colvin --- changes/3652-dolfinus.md | 1 + pydantic/networks.py | 18 +++++++++++++++++- tests/test_networks.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 changes/3652-dolfinus.md diff --git a/changes/3652-dolfinus.md b/changes/3652-dolfinus.md new file mode 100644 index 00000000000..907a440c69a --- /dev/null +++ b/changes/3652-dolfinus.md @@ -0,0 +1 @@ +Include non-standard port numbers in rendered URLs diff --git a/pydantic/networks.py b/pydantic/networks.py index cbc9e21f4c5..0626d2c3550 100644 --- a/pydantic/networks.py +++ b/pydantic/networks.py @@ -54,6 +54,10 @@ class Parts(TypedDict, total=False): else: email_validator = None + class Parts(dict): + pass + + NetworkType = Union[str, bytes, int, Tuple[Union[str, bytes, int], Union[str, int]]] __all__ = [ @@ -176,6 +180,18 @@ def build( fragment: Optional[str] = None, **_kwargs: str, ) -> str: + parts = Parts( + scheme=scheme, + user=user, + password=password, + host=host, + port=port, + path=path, + query=query, + fragment=fragment, + **_kwargs, # type: ignore[misc] + ) + url = scheme + '://' if user: url += user @@ -184,7 +200,7 @@ def build( if user or password: url += '@' url += host - if port and 'port' not in cls.hidden_parts: + if port and ('port' not in cls.hidden_parts or cls.get_default_parts(parts).get('port') != port): url += ':' + port if path: url += path diff --git a/tests/test_networks.py b/tests/test_networks.py index 5ced4ac4c69..4d768b20fa9 100644 --- a/tests/test_networks.py +++ b/tests/test_networks.py @@ -2,6 +2,7 @@ from pydantic import ( AmqpDsn, + AnyHttpUrl, AnyUrl, BaseModel, EmailStr, @@ -564,6 +565,41 @@ def test_build_url(kwargs, expected): assert AnyUrl(None, **kwargs) == expected +@pytest.mark.parametrize( + 'kwargs,expected', + [ + (dict(scheme='http', host='example.net'), 'http://example.net'), + (dict(scheme='https', host='example.net'), 'https://example.net'), + (dict(scheme='http', user='foo', host='example.net'), 'http://foo@example.net'), + (dict(scheme='https', user='foo', host='example.net'), 'https://foo@example.net'), + (dict(scheme='http', user='foo', host='example.net', port='123'), 'http://foo@example.net:123'), + (dict(scheme='https', user='foo', host='example.net', port='123'), 'https://foo@example.net:123'), + (dict(scheme='http', user='foo', password='x', host='example.net'), 'http://foo:x@example.net'), + (dict(scheme='http2', user='foo', password='x', host='example.net'), 'http2://foo:x@example.net'), + (dict(scheme='http', host='example.net', query='a=b', fragment='c=d'), 'http://example.net?a=b#c=d'), + (dict(scheme='http2', host='example.net', query='a=b', fragment='c=d'), 'http2://example.net?a=b#c=d'), + (dict(scheme='http', host='example.net', port='1234'), 'http://example.net:1234'), + (dict(scheme='https', host='example.net', port='1234'), 'https://example.net:1234'), + ], +) +@pytest.mark.parametrize('klass', [AnyHttpUrl, HttpUrl]) +def test_build_any_http_url(klass, kwargs, expected): + assert klass(None, **kwargs) == expected + + +@pytest.mark.parametrize( + 'klass, kwargs,expected', + [ + (AnyHttpUrl, dict(scheme='http', user='foo', host='example.net', port='80'), 'http://foo@example.net:80'), + (AnyHttpUrl, dict(scheme='https', user='foo', host='example.net', port='443'), 'https://foo@example.net:443'), + (HttpUrl, dict(scheme='http', user='foo', host='example.net', port='80'), 'http://foo@example.net'), + (HttpUrl, dict(scheme='https', user='foo', host='example.net', port='443'), 'https://foo@example.net'), + ], +) +def test_build_http_url_port(klass, kwargs, expected): + assert klass(None, **kwargs) == expected + + def test_son(): class Model(BaseModel): v: HttpUrl From a7e896c5a368220d96ca3708d057c0d8bc9d435f Mon Sep 17 00:00:00 2001 From: David Brochart Date: Mon, 16 May 2022 12:26:31 +0200 Subject: [PATCH 52/62] Update Jupyter's use of pydantic (#4082) * Update Jupyter's use of pydantic * Fix markdown link --- changes/4082-davidbrochart.md | 1 + docs/index.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 changes/4082-davidbrochart.md diff --git a/changes/4082-davidbrochart.md b/changes/4082-davidbrochart.md new file mode 100644 index 00000000000..cad2efbb829 --- /dev/null +++ b/changes/4082-davidbrochart.md @@ -0,0 +1 @@ +Add Jupyverse and FPS as Jupyter projects using pydantic diff --git a/docs/index.md b/docs/index.md index ba4366dd114..24f04b35286 100644 --- a/docs/index.md +++ b/docs/index.md @@ -84,7 +84,9 @@ Hundreds of organisations and packages are using *pydantic*, including: [Project Jupyter](https://jupyter.org/) : developers of the Jupyter notebook are using *pydantic* - [for subprojects](https://github.com/samuelcolvin/pydantic/issues/773). + [for subprojects](https://github.com/samuelcolvin/pydantic/issues/773), through the FastAPI-based Jupyter server + [Jupyverse](https://github.com/jupyter-server/jupyverse), and for [FPS](https://github.com/jupyter-server/fps)'s + configuration management. **Microsoft** : are using *pydantic* (via FastAPI) for From abea8232eef0eeeb728824cdec9b445dfbd3192e Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 17 May 2022 14:13:36 +0100 Subject: [PATCH 53/62] speedup __instancecheck__ check on BaseModel when they fail (#4081) * speedup __instancecheck__ check on BaseModel when they fail * add change description * linting --- changes/4081-samuelcolvin.md | 1 + pydantic/main.py | 8 ++++++++ tests/test_edge_cases.py | 14 ++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 changes/4081-samuelcolvin.md diff --git a/changes/4081-samuelcolvin.md b/changes/4081-samuelcolvin.md new file mode 100644 index 00000000000..53fd4f52a89 --- /dev/null +++ b/changes/4081-samuelcolvin.md @@ -0,0 +1 @@ +Speedup `__isinstancecheck__` on pydantic models when the type is not a model, may also avoid memory "leaks". diff --git a/pydantic/main.py b/pydantic/main.py index 7afcafc4978..0c20d9e69d5 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -295,6 +295,14 @@ def is_untouched(v: Any) -> bool: return cls + def __instancecheck__(self, instance: Any) -> bool: + """ + Avoid calling ABC _abc_subclasscheck unless we're pretty sure. + + See #3829 and python/cpython#92810 + """ + return hasattr(instance, '__fields__') and super().__instancecheck__(instance) + object_setattr = object.__setattr__ diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index 5da62257040..b7083d05846 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -1932,3 +1932,17 @@ def __new__(cls, data: int): m = MyModel(my_int=IntSubclass(123)) assert m.my_int.__class__ == IntSubclass + + +def test_model_issubclass(): + assert not issubclass(int, BaseModel) + + class MyModel(BaseModel): + x: int + + assert issubclass(MyModel, BaseModel) + + class Custom: + __fields__ = True + + assert not issubclass(Custom, BaseModel) From 467d6b468f26bce76f1358c9697b80e3d8384dae Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 18 May 2022 12:18:27 +0100 Subject: [PATCH 54/62] adding sponsors to index page of docs (#4074) * adding sponsors to index page of docs * Adjust ExoFlare sponsor link (#4080) * add UTM identifiers to TC link Co-authored-by: Tom Hamilton Stubber Co-authored-by: Huon Wilson Co-authored-by: Tom Hamilton Stubber --- docs/extra/tweaks.css | 16 +++++--- docs/index.md | 53 +++++++++++++++++++++++++++ docs/sponsor_logos/exoflare.png | Bin 0 -> 19190 bytes docs/sponsor_logos/fastapi.png | Bin 0 -> 14381 bytes docs/sponsor_logos/robusta.png | Bin 0 -> 23227 bytes docs/sponsor_logos/salesforce.png | Bin 0 -> 21472 bytes docs/sponsor_logos/sendcloud.png | Bin 0 -> 17042 bytes docs/sponsor_logos/tutorcruncher.png | Bin 0 -> 12614 bytes 8 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 docs/sponsor_logos/exoflare.png create mode 100644 docs/sponsor_logos/fastapi.png create mode 100644 docs/sponsor_logos/robusta.png create mode 100644 docs/sponsor_logos/salesforce.png create mode 100644 docs/sponsor_logos/sendcloud.png create mode 100644 docs/sponsor_logos/tutorcruncher.png diff --git a/docs/extra/tweaks.css b/docs/extra/tweaks.css index 53e4dae037c..2713d66363f 100644 --- a/docs/extra/tweaks.css +++ b/docs/extra/tweaks.css @@ -14,14 +14,18 @@ background: hsla(0, 0%, 92.5%, 0.5); } -#_default_ a._default_, #_default_ .default-text { - width: 100% !important; +.sponsors { + display: flex; + justify-content: space-between; + align-items: center; + margin: 1rem 0; } -.md-nav__link[data-md-state=blur]:not(.md-nav__link--active) { - color: rgba(0, 0, 0, 0.87); +.sponsors div { + text-align: center; } -.md-nav__link--active { - font-weight: 700; +.sponsors img { + width: 75%; + border-radius: 5px; } diff --git a/docs/index.md b/docs/index.md index 24f04b35286..c3ef572d38d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -13,6 +13,59 @@ Data validation and settings management using python type annotations. Define how data should be in pure, canonical python; validate it with *pydantic*. +## Sponsors + +Development of *pydantic* is made possible by the following sponsors: + + + +And many more who kindly sponsor Samuel Colvin on [GitHub Sponsors](https://github.com/sponsors/samuelcolvin#sponsors). + + + ## Example ```py diff --git a/docs/sponsor_logos/exoflare.png b/docs/sponsor_logos/exoflare.png new file mode 100644 index 0000000000000000000000000000000000000000..1f18c0e7eba7024cb98c75e4d1b541a1ce0ee625 GIT binary patch literal 19190 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 J$d$hp@Ly~?lT-iz literal 0 HcmV?d00001 diff --git a/docs/sponsor_logos/robusta.png b/docs/sponsor_logos/robusta.png new file mode 100644 index 0000000000000000000000000000000000000000..02c7ef2978370cda66828561a54b80d600f54760 GIT binary patch literal 23227 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 M#nnsri#PxNA2uwIN&o-= literal 0 HcmV?d00001 diff --git a/docs/sponsor_logos/salesforce.png b/docs/sponsor_logos/salesforce.png new file mode 100644 index 0000000000000000000000000000000000000000..9264adb933930047c65ae0bc3ff19394be0af948 GIT binary patch literal 21472 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 literal 0 HcmV?d00001 diff --git a/docs/sponsor_logos/tutorcruncher.png b/docs/sponsor_logos/tutorcruncher.png new file mode 100644 index 0000000000000000000000000000000000000000..573cd48a1b56efbf9befe16820ab07d92d50d607 GIT binary patch literal 12614 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 am!zecG?&>pgi%1}kkTV{xiT5E7yko{YFax0 literal 0 HcmV?d00001 From 5a6129225224128bf358be637ec68ada7dc01251 Mon Sep 17 00:00:00 2001 From: Giuliano Oliveira Date: Wed, 18 May 2022 07:19:25 -0400 Subject: [PATCH 55/62] Adds `dataclass_transform` to dataclass (#4007) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Adds dataclass_transform to dataclasses * ✨ Adds dataclass_transform to dataclasses Co-authored-by: Samuel Colvin --- changes/4006-giuliano-oliveira.md | 1 + pydantic/dataclasses.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 changes/4006-giuliano-oliveira.md diff --git a/changes/4006-giuliano-oliveira.md b/changes/4006-giuliano-oliveira.md new file mode 100644 index 00000000000..97a7eeccfc7 --- /dev/null +++ b/changes/4006-giuliano-oliveira.md @@ -0,0 +1 @@ +Add support for autocomplete in VS Code via `__dataclass_transform__` when using `pydantic.dataclasses.dataclass` diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py index ac8fd6d89cb..692bfb9f3a7 100644 --- a/pydantic/dataclasses.py +++ b/pydantic/dataclasses.py @@ -1,10 +1,10 @@ -from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Type, TypeVar, Union, overload +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Optional, Type, TypeVar, Union, overload from .class_validators import gather_all_validators from .error_wrappers import ValidationError from .errors import DataclassTypeError from .fields import Field, FieldInfo, Required, Undefined -from .main import create_model, validate_model +from .main import __dataclass_transform__, create_model, validate_model from .typing import resolve_annotations from .utils import ClassAttribute @@ -16,11 +16,11 @@ DataclassT = TypeVar('DataclassT', bound='Dataclass') class Dataclass: - __pydantic_model__: Type[BaseModel] - __initialised__: bool - __post_init_original__: Optional[Callable[..., None]] - __processed__: Optional[ClassAttribute] - __has_field_info_default__: bool # whether or not a `pydantic.Field` is used as default value + __pydantic_model__: ClassVar[Type[BaseModel]] + __initialised__: ClassVar[bool] + __post_init_original__: ClassVar[Optional[Callable[..., None]]] + __processed__: ClassVar[Optional[ClassAttribute]] + __has_field_info_default__: ClassVar[bool] # whether or not a `pydantic.Field` is used as default value def __init__(self, *args: Any, **kwargs: Any) -> None: pass @@ -206,6 +206,7 @@ def _process_class( return cls +@__dataclass_transform__(kw_only_default=True, field_descriptors=(Field, FieldInfo)) @overload def dataclass( *, @@ -220,6 +221,7 @@ def dataclass( ... +@__dataclass_transform__(kw_only_default=True, field_descriptors=(Field, FieldInfo)) @overload def dataclass( _cls: Type[Any], @@ -235,6 +237,7 @@ def dataclass( ... +@__dataclass_transform__(kw_only_default=True, field_descriptors=(Field, FieldInfo)) def dataclass( _cls: Optional[Type[Any]] = None, *, From 8846ec4685e749b93907081450f592060eeb99b1 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 18 May 2022 15:22:58 +0100 Subject: [PATCH 56/62] limit the length of `generics._limit_assigned_parameters` (#4083) * limit the length of generics._limit_assigned_parameters * switch to using _limit_cache_size for both * add change description * correct `_limit_cache_size` cache * implemented LimitedDict * try using UserDict * try upgrading cython * stop LimitedDict from inheriting from dict * separate LimitedDict for typing checking :-( * fix for __class_getitem__ --- changes/4083-samuelcolvin.md | 2 ++ pydantic/generics.py | 6 +++--- pydantic/utils.py | 38 +++++++++++++++++++++++++++++++++ tests/test_utils.py | 41 ++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 changes/4083-samuelcolvin.md diff --git a/changes/4083-samuelcolvin.md b/changes/4083-samuelcolvin.md new file mode 100644 index 00000000000..ed252f16b5a --- /dev/null +++ b/changes/4083-samuelcolvin.md @@ -0,0 +1,2 @@ +Limit the size of `generics._generic_types_cache` and `generics._assigned_parameters` +to avoid unlimited increase in memory usage. diff --git a/pydantic/generics.py b/pydantic/generics.py index baad72cbf73..2467bb77111 100644 --- a/pydantic/generics.py +++ b/pydantic/generics.py @@ -24,20 +24,20 @@ from .main import BaseModel, create_model from .types import JsonWrapper from .typing import display_as_type, get_all_type_hints, get_args, get_origin, typing_base -from .utils import all_identical, lenient_issubclass +from .utils import LimitedDict, all_identical, lenient_issubclass -_generic_types_cache: Dict[Tuple[Type[Any], Union[Any, Tuple[Any, ...]]], Type[BaseModel]] = {} GenericModelT = TypeVar('GenericModelT', bound='GenericModel') TypeVarType = Any # since mypy doesn't allow the use of TypeVar as a type Parametrization = Mapping[TypeVarType, Type[Any]] +_generic_types_cache: LimitedDict[Tuple[Type[Any], Union[Any, Tuple[Any, ...]]], Type[BaseModel]] = LimitedDict() # _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] = {} +_assigned_parameters: LimitedDict[Type[Any], Parametrization] = LimitedDict() class GenericModel(BaseModel): diff --git a/pydantic/utils.py b/pydantic/utils.py index 2a3960f6d7a..f9c4ec16022 100644 --- a/pydantic/utils.py +++ b/pydantic/utils.py @@ -16,6 +16,7 @@ Iterator, List, Mapping, + MutableMapping, Optional, Set, Tuple, @@ -73,6 +74,7 @@ 'ROOT_KEY', 'get_unique_discriminator_alias', 'get_discriminator_alias_and_values', + 'LimitedDict', ) ROOT_KEY = '__root__' @@ -749,3 +751,39 @@ def _get_union_alias_and_all_values( # unzip: [('alias_a',('v1', 'v2)), ('alias_b', ('v3',))] => [('alias_a', 'alias_b'), (('v1', 'v2'), ('v3',))] all_aliases, all_values = zip(*zipped_aliases_values) return get_unique_discriminator_alias(all_aliases, discriminator_key), all_values + + +KT = TypeVar('KT') +VT = TypeVar('VT') +if TYPE_CHECKING: + # Annoying inheriting from `MutableMapping` and `dict` breaks cython, hence this work around + class LimitedDict(dict, MutableMapping[KT, VT]): # type: ignore[type-arg] + def __init__(self, size_limit: int = 1000): + ... + +else: + + class LimitedDict(dict): + """ + Limit the size/length of a dict used for caching to avoid unlimited increase in memory usage. + + Since the dict is ordered, and we always remove elements from the beginning, this is effectively a FIFO cache. + + Annoying inheriting from `MutableMapping` breaks cython. + """ + + def __init__(self, size_limit: int = 1000): + self.size_limit = size_limit + super().__init__() + + def __setitem__(self, __key: Any, __value: Any) -> None: + super().__setitem__(__key, __value) + if len(self) > self.size_limit: + excess = len(self) - self.size_limit + self.size_limit // 10 + to_remove = list(self.keys())[:excess] + for key in to_remove: + del self[key] + + def __class_getitem__(cls, *args: Any) -> Any: + # to avoid errors with 3.7 + pass diff --git a/tests/test_utils.py b/tests/test_utils.py index 132081a7052..d7eaa74c697 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -28,6 +28,7 @@ from pydantic.utils import ( BUILTIN_COLLECTIONS, ClassAttribute, + LimitedDict, ValueItems, all_identical, deep_update, @@ -525,3 +526,43 @@ def test_all_identical(): def test_undefined_pickle(): undefined2 = pickle.loads(pickle.dumps(Undefined)) assert undefined2 is Undefined + + +def test_limited_dict(): + d = LimitedDict(10) + d[1] = '1' + d[2] = '2' + assert list(d.items()) == [(1, '1'), (2, '2')] + for no in '34567890': + d[int(no)] = no + assert list(d.items()) == [ + (1, '1'), + (2, '2'), + (3, '3'), + (4, '4'), + (5, '5'), + (6, '6'), + (7, '7'), + (8, '8'), + (9, '9'), + (0, '0'), + ] + d[11] = '11' + + # reduce size to 9 after setting 11 + assert len(d) == 9 + assert list(d.items()) == [ + (3, '3'), + (4, '4'), + (5, '5'), + (6, '6'), + (7, '7'), + (8, '8'), + (9, '9'), + (0, '0'), + (11, '11'), + ] + d[12] = '12' + assert len(d) == 10 + d[13] = '13' + assert len(d) == 9 From dae9325b38fb75d2f579999978958edaf1ef71ec Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 14 May 2022 16:29:59 +0100 Subject: [PATCH 57/62] revert removing python 3.6, #3605 --- .github/workflows/ci.yml | 8 +- README.md | 2 +- changes/3605-samuelcolvin.md | 1 - docs/contributing.md | 4 +- docs/install.md | 7 +- docs/usage/dataclasses.md | 2 + docs/usage/models.md | 4 + mkdocs.yml | 2 +- pydantic/config.py | 5 +- pydantic/fields.py | 1 + pydantic/json.py | 9 +- pydantic/typing.py | 109 ++++++++++++++----- setup.py | 4 +- tests/mypy/modules/plugin_fail.py | 2 +- tests/mypy/modules/plugin_success.py | 14 +-- tests/mypy/modules/success.py | 28 ++--- tests/mypy/outputs/plugin-success-strict.txt | 6 +- tests/mypy/outputs/plugin_success.txt | 6 +- tests/mypy/test_mypy.py | 2 +- tests/test_decorator.py | 4 +- tests/test_discrimated_union.py | 2 + tests/test_edge_cases.py | 7 +- tests/test_errors.py | 2 + tests/test_forward_ref.py | 13 +++ tests/test_generics.py | 66 +++++++++++ tests/test_main.py | 2 + tests/test_private_attributes.py | 4 + tests/test_schema.py | 15 +++ tests/test_types.py | 13 ++- tests/test_utils.py | 1 + 30 files changed, 260 insertions(+), 85 deletions(-) delete mode 100644 changes/3605-samuelcolvin.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 838897946e1..515b0821b7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,7 +100,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] env: PYTHON: ${{ matrix.python-version }} OS: ubuntu @@ -155,9 +155,7 @@ jobs: fail-fast: false matrix: os: [ubuntu, macos, windows] - python-version: ['3.7', '3.8', '3.9', '3.10'] - include: - - os: ubuntu + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] env: PYTHON: ${{ matrix.python-version }} @@ -310,7 +308,7 @@ jobs: fail-fast: false matrix: os: [ubuntu , macos , windows] - python-version: ['7', '8', '9', '10'] + python-version: ['6', '7', '8', '9', '10'] include: - os: ubuntu platform: linux diff --git a/README.md b/README.md index c4ddb13ad25..c5c8a62c073 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Data validation and settings management using Python type hints. Fast and extensible, *pydantic* plays nicely with your linters/IDE/brain. -Define how data should be in pure, canonical Python 3.7+; validate it with *pydantic*. +Define how data should be in pure, canonical Python 3.6+; validate it with *pydantic*. ## Help diff --git a/changes/3605-samuelcolvin.md b/changes/3605-samuelcolvin.md deleted file mode 100644 index 305c118e9df..00000000000 --- a/changes/3605-samuelcolvin.md +++ /dev/null @@ -1 +0,0 @@ -Drop support for python3.6, associated cleanup diff --git a/docs/contributing.md b/docs/contributing.md index 2fd4fa6ea77..01c3b441aa6 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 a version between **python 3.7 and 3.10**, **virtualenv**, **git**, and **make** installed. +You'll need to have a version between **python 3.6 and 3.10**, **virtualenv**, **git**, and **make** installed. ```bash # 1. clone your fork and cd into the repo directory @@ -44,7 +44,7 @@ cd pydantic virtualenv -p `which python3.8` env source env/bin/activate # Building docs requires 3.8. If you don't need to build docs you can use -# whichever version; 3.7 will work too. +# whichever version; 3.6 will work too. # 3. Install pydantic, dependencies, test dependencies and doc dependencies make install diff --git a/docs/install.md b/docs/install.md index 30b3c0fa4ee..15341596b8b 100644 --- a/docs/install.md +++ b/docs/install.md @@ -4,9 +4,10 @@ Installation is as simple as: pip install pydantic ``` -*pydantic* has no required dependencies except python 3.7, 3.8, 3.9 or 3.10 and -[`typing-extensions`](https://pypi.org/project/typing-extensions/). -If you've got python 3.7+ and `pip` installed, you're good to go. +*pydantic* has no required dependencies except python 3.6, 3.7, 3.8, 3.9 or 3.10, +[`typing-extensions`](https://pypi.org/project/typing-extensions/), and the +[`dataclasses`](https://pypi.org/project/dataclasses/) backport package for python 3.6. +If you've got python 3.6+ and `pip` installed, you're good to go. Pydantic is also available on [conda](https://www.anaconda.com) under the [conda-forge](https://conda-forge.org) channel: diff --git a/docs/usage/dataclasses.md b/docs/usage/dataclasses.md index 3b302d65bb6..ef07de3a689 100644 --- a/docs/usage/dataclasses.md +++ b/docs/usage/dataclasses.md @@ -1,6 +1,8 @@ If you don't want to use _pydantic_'s `BaseModel` you can instead get the same data validation on standard [dataclasses](https://docs.python.org/3/library/dataclasses.html) (introduced in python 3.7). +Dataclasses work in python 3.6 using the [dataclasses backport package](https://github.com/ericvsmith/dataclasses). + ```py {!.tmp_examples/dataclasses_main.py!} ``` diff --git a/docs/usage/models.md b/docs/usage/models.md index 8f7bc8237b9..072038d4152 100644 --- a/docs/usage/models.md +++ b/docs/usage/models.md @@ -296,6 +296,10 @@ For example, in the example above, if `_fields_set` was not provided, Pydantic supports the creation of generic models to make it easier to reuse a common model structure. +!!! warning + Generic models are only supported with python `>=3.7`, this is because of numerous subtle changes in how + generics are implemented between python 3.6 and python 3.7. + In order to declare a generic model, you perform the following steps: * Declare one or more `typing.TypeVar` instances to use to parameterize your model. diff --git a/mkdocs.yml b/mkdocs.yml index efdbbc20680..2e5933b7986 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: pydantic -site_description: Data validation and settings management using python type hints +site_description: Data validation and settings management using python 3.6 type hinting strict: true site_url: https://pydantic-docs.helpmanual.io/ diff --git a/pydantic/config.py b/pydantic/config.py index ef4b3c008fc..b37cd98ff17 100644 --- a/pydantic/config.py +++ b/pydantic/config.py @@ -1,6 +1,6 @@ import json from enum import Enum -from typing import TYPE_CHECKING, Any, Callable, Dict, ForwardRef, Optional, Tuple, Type, Union +from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type, Union from .typing import AnyCallable from .utils import GetterDict @@ -59,7 +59,8 @@ class BaseConfig: schema_extra: Union[Dict[str, Any], 'SchemaExtraCallable'] = {} json_loads: Callable[[str], Any] = json.loads json_dumps: Callable[..., str] = json.dumps - json_encoders: Dict[Union[Type[Any], str, ForwardRef], AnyCallable] = {} + # key type should include ForwardRef, but that breaks with python3.6 + json_encoders: Dict[Union[Type[Any], str], AnyCallable] = {} underscore_attrs_are_private: bool = False # whether inherited models as fields should be reconstructed as base model diff --git a/pydantic/fields.py b/pydantic/fields.py index 10360b8394a..df79edfda53 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -1138,6 +1138,7 @@ def is_complex(self) -> bool: def _type_display(self) -> PyObjectStr: t = display_as_type(self.type_) + # have to do this since display_as_type(self.outer_type_) is different (and wrong) on python 3.6 if self.shape in MAPPING_LIKE_SHAPES: t = f'Mapping[{display_as_type(self.key_field.type_)}, {t}]' # type: ignore elif self.shape == SHAPE_TUPLE: diff --git a/pydantic/json.py b/pydantic/json.py index 0769228e416..cab8b800859 100644 --- a/pydantic/json.py +++ b/pydantic/json.py @@ -1,14 +1,21 @@ import datetime +import re +import sys from collections import deque from decimal import Decimal from enum import Enum from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network from pathlib import Path -from re import Pattern from types import GeneratorType from typing import Any, Callable, Dict, Type, Union from uuid import UUID +if sys.version_info >= (3, 7): + Pattern = re.Pattern +else: + # python 3.6 + Pattern = re.compile('a').__class__ + from .color import Color from .networks import NameEmail from .types import SecretBytes, SecretStr diff --git a/pydantic/typing.py b/pydantic/typing.py index 44fba36109c..f9297ba197b 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -1,14 +1,11 @@ import sys -from collections.abc import Callable from os import PathLike from typing import ( # type: ignore TYPE_CHECKING, AbstractSet, Any, - Callable as TypingCallable, ClassVar, Dict, - ForwardRef, Generator, Iterable, List, @@ -45,7 +42,28 @@ TypesUnionType = () -if sys.version_info < (3, 9): +if sys.version_info < (3, 7): + if TYPE_CHECKING: + + class ForwardRef: + def __init__(self, arg: Any): + pass + + def _eval_type(self, globalns: Any, localns: Any) -> Any: + pass + + else: + from typing import _ForwardRef as ForwardRef +else: + from typing import ForwardRef + + +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) @@ -60,7 +78,7 @@ def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: if sys.version_info < (3, 9): # Ensure we always get all the whole `Annotated` hint, not just the annotated type. - # For 3.7 to 3.8, `get_type_hints` doesn't recognize `typing_extensions.Annotated`, + # For 3.6 to 3.8, `get_type_hints` doesn't recognize `typing_extensions.Annotated`, # so it already returns the full annotation get_all_type_hints = get_type_hints @@ -70,8 +88,17 @@ def get_all_type_hints(obj: Any, globalns: Any = None, localns: Any = None) -> A return get_type_hints(obj, globalns, localns, include_extras=True) -AnyCallable = TypingCallable[..., Any] -NoArgAnyCallable = TypingCallable[[], Any] +if sys.version_info < (3, 7): + from typing import Callable as Callable + + AnyCallable = Callable[..., Any] + NoArgAnyCallable = Callable[[], Any] +else: + from collections.abc import Callable as Callable + from typing import Callable as TypingCallable + + AnyCallable = TypingCallable[..., Any] + NoArgAnyCallable = TypingCallable[[], Any] # Annotated[...] is implemented by returning an instance of one of these classes, depending on @@ -83,8 +110,7 @@ def get_all_type_hints(obj: Any, globalns: Any = None, localns: Any = None) -> A def get_origin(t: Type[Any]) -> Optional[Type[Any]]: if type(t).__name__ in AnnotatedTypeNames: - # weirdly this is a runtime requirement, as well as for mypy - return cast(Type[Any], Annotated) + return cast(Type[Any], Annotated) # mypy complains about _SpecialForm in py3.6 return getattr(t, '__origin__', None) else: @@ -102,7 +128,22 @@ def get_origin(tp: Type[Any]) -> Optional[Type[Any]]: return _typing_get_origin(tp) or getattr(tp, '__origin__', None) -if sys.version_info < (3, 8): +if sys.version_info < (3, 7): # noqa: C901 (ignore complexity) + + def get_args(t: Type[Any]) -> Tuple[Any, ...]: + """Simplest get_args compatibility layer possible. + + The Python 3.6 typing module does not have `_GenericAlias` so + this won't work for everything. In particular this will not + support the `generics` module (we don't support generic models in + python 3.6). + + """ + if type(t).__name__ in AnnotatedTypeNames: + return t.__args__ + t.__metadata__ + return getattr(t, '__args__', ()) + +elif sys.version_info < (3, 8): # noqa: C901 from typing import _GenericAlias def get_args(t: Type[Any]) -> Tuple[Any, ...]: @@ -301,8 +342,8 @@ def is_union(tp: Optional[Type[Any]]) -> bool: if sys.version_info < (3, 8): - # Even though this implementation is slower, we need it for python 3.7: - # In python 3.7 "Literal" is not a builtin type and uses a different + # Even though this implementation is slower, we need it for python 3.6/3.7: + # In python 3.6/3.7 "Literal" is not a builtin type and uses a different # mechanism. # for this reason `Literal[None] is Literal[None]` evaluates to `False`, # breaking the faster implementation used for the other python versions. @@ -370,8 +411,10 @@ def resolve_annotations(raw_annotations: Dict[str, Type[Any]], module_name: Opti if isinstance(value, str): if (3, 10) > sys.version_info >= (3, 9, 8) or sys.version_info >= (3, 10, 1): value = ForwardRef(value, is_argument=False, is_class=True) - else: + elif sys.version_info >= (3, 7): value = ForwardRef(value, is_argument=False) + else: + value = ForwardRef(value) try: value = _eval_type(value, base_globals, None) except NameError: @@ -385,12 +428,21 @@ def is_callable_type(type_: Type[Any]) -> bool: return type_ is Callable or get_origin(type_) is Callable -def is_literal_type(type_: Type[Any]) -> bool: - return Literal is not None and get_origin(type_) is Literal +if sys.version_info >= (3, 7): + def is_literal_type(type_: Type[Any]) -> bool: + return Literal is not None and get_origin(type_) is Literal -def literal_values(type_: Type[Any]) -> Tuple[Any, ...]: - return get_args(type_) + def literal_values(type_: Type[Any]) -> Tuple[Any, ...]: + return get_args(type_) + +else: + + def is_literal_type(type_: Type[Any]) -> bool: + return Literal is not None and hasattr(type_, '__values__') and type_ == Literal[type_.__values__] + + def literal_values(type_: Type[Any]) -> Tuple[Any, ...]: + return type_.__values__ def all_literal_values(type_: Type[Any]) -> Tuple[Any, ...]: @@ -446,7 +498,7 @@ def _check_classvar(v: Optional[Type[Any]]) -> bool: if v is None: return False - return v.__class__ == ClassVar.__class__ and getattr(v, '_name', None) == 'ClassVar' + return v.__class__ == ClassVar.__class__ and (sys.version_info < (3, 7) or getattr(v, '_name', None) == 'ClassVar') def is_classvar(ann_type: Type[Any]) -> bool: @@ -480,7 +532,7 @@ def update_field_forward_refs(field: 'ModelField', globalns: Any, localns: Any) def update_model_forward_refs( model: Type[Any], fields: Iterable['ModelField'], - json_encoders: Dict[Union[Type[Any], str, ForwardRef], AnyCallable], + json_encoders: Dict[Union[Type[Any], str], AnyCallable], localns: 'DictStrAny', exc_to_suppress: Tuple[Type[BaseException], ...] = (), ) -> None: @@ -521,14 +573,17 @@ 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 without brackets. Otherwise returns None. """ - if get_origin(type_) is None: - return None - - args = get_args(type_) - if not args or not isinstance(args[0], type): - return True - else: - return args[0] + try: + origin = get_origin(type_) + if origin is None: # Python 3.6 + origin = type_ + if issubclass(origin, Type): # type: ignore + if not get_args(type_) or not isinstance(get_args(type_)[0], type): + return True + return get_args(type_)[0] + except (AttributeError, TypeError): + pass + return None def get_sub_types(tp: Any) -> List[Any]: diff --git a/setup.py b/setup.py index c59056e0e56..031a386f8ae 100644 --- a/setup.py +++ b/setup.py @@ -104,6 +104,7 @@ def extra(self): 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', @@ -126,9 +127,10 @@ def extra(self): license='MIT', packages=['pydantic'], package_data={'pydantic': ['py.typed']}, - python_requires='>=3.7', + python_requires='>=3.6.1', zip_safe=False, # https://mypy.readthedocs.io/en/latest/installed_packages.html install_requires=[ + 'dataclasses>=0.6;python_version<"3.7"', 'typing-extensions>=3.7.4.3' ], extras_require={ diff --git a/tests/mypy/modules/plugin_fail.py b/tests/mypy/modules/plugin_fail.py index 4c57862f0ac..7dd992a6daa 100644 --- a/tests/mypy/modules/plugin_fail.py +++ b/tests/mypy/modules/plugin_fail.py @@ -114,7 +114,7 @@ class Blah(BaseModel): fields_set: Optional[Set[str]] = None -# (comment to keep line numbers unchanged) +# Need to test generic checking here since generics don't work in 3.6, and plugin-success.py is executed T = TypeVar('T') diff --git a/tests/mypy/modules/plugin_success.py b/tests/mypy/modules/plugin_success.py index ee383c526fb..217eebcd32e 100644 --- a/tests/mypy/modules/plugin_success.py +++ b/tests/mypy/modules/plugin_success.py @@ -1,8 +1,7 @@ -from typing import ClassVar, Generic, Optional, TypeVar, Union +from typing import ClassVar, Optional, Union from pydantic import BaseModel, BaseSettings, Field, create_model, validator from pydantic.dataclasses import dataclass -from pydantic.generics import GenericModel class Model(BaseModel): @@ -182,14 +181,3 @@ class ModelWithAllowReuseValidator(BaseModel): model_with_allow_reuse_validator = ModelWithAllowReuseValidator(name='xyz') - - -T = TypeVar('T') - - -class Response(GenericModel, Generic[T]): - data: T - error: Optional[str] - - -response = Response[Model](data=model, error=None) diff --git a/tests/mypy/modules/success.py b/tests/mypy/modules/success.py index 11e3db10ce1..51611e373ab 100644 --- a/tests/mypy/modules/success.py +++ b/tests/mypy/modules/success.py @@ -5,6 +5,7 @@ """ import json import os +import sys from datetime import date, datetime, timedelta from pathlib import Path, PurePath from typing import Any, Dict, Generic, List, Optional, TypeVar @@ -133,24 +134,23 @@ def day_of_week(dt: datetime) -> int: assert m_copy.list_of_ints == m_from_obj.list_of_ints -T = TypeVar('T') +if sys.version_info >= (3, 7): + T = TypeVar('T') + class WrapperModel(GenericModel, Generic[T]): + payload: T -class WrapperModel(GenericModel, Generic[T]): - payload: T + int_instance = WrapperModel[int](payload=1) + int_instance.payload += 1 + assert int_instance.payload == 2 + str_instance = WrapperModel[str](payload='a') + str_instance.payload += 'a' + assert str_instance.payload == 'aa' -int_instance = WrapperModel[int](payload=1) -int_instance.payload += 1 -assert int_instance.payload == 2 - -str_instance = WrapperModel[str](payload='a') -str_instance.payload += 'a' -assert str_instance.payload == 'aa' - -model_instance = WrapperModel[Model](payload=m) -model_instance.payload.list_of_ints.append(4) -assert model_instance.payload.list_of_ints == [1, 2, 3, 4] + model_instance = WrapperModel[Model](payload=m) + model_instance.payload.list_of_ints.append(4) + assert model_instance.payload.list_of_ints == [1, 2, 3, 4] class WithField(BaseModel): diff --git a/tests/mypy/outputs/plugin-success-strict.txt b/tests/mypy/outputs/plugin-success-strict.txt index 662ad6828de..80365a9df88 100644 --- a/tests/mypy/outputs/plugin-success-strict.txt +++ b/tests/mypy/outputs/plugin-success-strict.txt @@ -1,3 +1,3 @@ -30: error: Unexpected keyword argument "z" for "Model" [call-arg] -65: error: Untyped fields disallowed [pydantic-field] -80: error: Argument "x" to "OverrideModel" has incompatible type "float"; expected "int" [arg-type] +29: error: Unexpected keyword argument "z" for "Model" [call-arg] +64: error: Untyped fields disallowed [pydantic-field] +79: error: Argument "x" to "OverrideModel" has incompatible type "float"; expected "int" [arg-type] \ No newline at end of file diff --git a/tests/mypy/outputs/plugin_success.txt b/tests/mypy/outputs/plugin_success.txt index 5b782e4396a..26ee50c39e1 100644 --- a/tests/mypy/outputs/plugin_success.txt +++ b/tests/mypy/outputs/plugin_success.txt @@ -1,3 +1,3 @@ -122: error: Unexpected keyword argument "name" for "AddProject" [call-arg] -122: error: Unexpected keyword argument "slug" for "AddProject" [call-arg] -122: error: Unexpected keyword argument "description" for "AddProject" [call-arg] +121: error: Unexpected keyword argument "name" for "AddProject" [call-arg] +121: error: Unexpected keyword argument "slug" for "AddProject" [call-arg] +121: error: Unexpected keyword argument "description" for "AddProject" [call-arg] \ No newline at end of file diff --git a/tests/mypy/test_mypy.py b/tests/mypy/test_mypy.py index 4e6d173ae44..5f772eb72e5 100644 --- a/tests/mypy/test_mypy.py +++ b/tests/mypy/test_mypy.py @@ -73,7 +73,7 @@ def test_mypy_results(config_filename: str, python_filename: str, output_filenam output_path.write_text(actual_out) raise RuntimeError(f'wrote actual output to {output_path} since file did not exist') - expected_out = Path(output_path).read_text().rstrip('\n') if output_path else '' + expected_out = Path(output_path).read_text() if output_path else '' # fix for compatibility between mypy versions: (this can be dropped once we drop support for mypy<0.930) if actual_out and float(mypy_version) < 0.930: diff --git a/tests/test_decorator.py b/tests/test_decorator.py index c2503dd72f2..97b78f88e88 100644 --- a/tests/test_decorator.py +++ b/tests/test_decorator.py @@ -87,7 +87,9 @@ def foo_bar(a: int, b: int): assert foo_bar.model.__fields__.keys() == {'a', 'b', 'args', 'kwargs', 'v__duplicate_kwargs'} assert foo_bar.model.__name__ == 'FooBar' assert foo_bar.model.schema()['title'] == 'FooBar' - assert repr(inspect.signature(foo_bar)) == '' + # signature is slightly different on 3.6 + if sys.version_info >= (3, 7): + assert repr(inspect.signature(foo_bar)) == '' def test_kwargs(): diff --git a/tests/test_discrimated_union.py b/tests/test_discrimated_union.py index a4dd501bc97..33fed5fb5ce 100644 --- a/tests/test_discrimated_union.py +++ b/tests/test_discrimated_union.py @@ -1,4 +1,5 @@ import re +import sys from enum import Enum from typing import Generic, TypeVar, Union @@ -375,6 +376,7 @@ class Model(BaseModel): assert isinstance(Model(**{'pet': {'pet_type': 'dog', 'name': 'Milou'}, 'n': 5}).pet, Dog) +@pytest.mark.skipif(sys.version_info < (3, 7), reason='generics only supported for python 3.7 and above') def test_generic(): T = TypeVar('T') diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py index b7083d05846..be4c17a9fc2 100644 --- a/tests/test_edge_cases.py +++ b/tests/test_edge_cases.py @@ -1298,6 +1298,7 @@ def validator(v): yield validator +@pytest.mark.skipif(sys.version_info < (3, 7), reason='output slightly different for 3.6') @pytest.mark.parametrize( 'type_,expected', [ @@ -1418,8 +1419,10 @@ def check_something(cls, value): class Bar(Foo): pass - assert repr(Foo.__fields__['foo']) == "ModelField(name='foo', type=List[List[int]], required=True)" - assert repr(Bar.__fields__['foo']) == "ModelField(name='foo', type=List[List[int]], required=True)" + # output is slightly different for 3.6 + if sys.version_info >= (3, 7): + assert repr(Foo.__fields__['foo']) == "ModelField(name='foo', type=List[List[int]], required=True)" + assert repr(Bar.__fields__['foo']) == "ModelField(name='foo', type=List[List[int]], required=True)" assert Foo(foo=[[0, 1]]).foo == [[0, 1]] assert Bar(foo=[[0, 1]]).foo == [[0, 1]] diff --git a/tests/test_errors.py b/tests/test_errors.py index 7224372df68..0f901b4edd1 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,4 +1,5 @@ import pickle +import sys from typing import Dict, List, Optional, Union from uuid import UUID, uuid4 @@ -71,6 +72,7 @@ def check_action(cls, v): ] +@pytest.mark.skipif(sys.version_info < (3, 7), reason='output slightly different for 3.6') def test_error_on_optional(): class Foobar(BaseModel): foo: Optional[str] = None diff --git a/tests/test_forward_ref.py b/tests/test_forward_ref.py index 54062154211..1580cb2e5bc 100644 --- a/tests/test_forward_ref.py +++ b/tests/test_forward_ref.py @@ -5,7 +5,10 @@ from pydantic import BaseModel, ConfigError, ValidationError +skip_pre_37 = pytest.mark.skipif(sys.version_info < (3, 7), reason='testing >= 3.7 behaviour only') + +@skip_pre_37 def test_postponed_annotations(create_module): module = create_module( # language=Python @@ -21,6 +24,7 @@ class Model(BaseModel): assert m.dict() == {'a': 123} +@skip_pre_37 def test_postponed_annotations_optional(create_module): module = create_module( # language=Python @@ -37,6 +41,7 @@ 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 @@ -210,6 +215,7 @@ class Dataclass: assert m.url == 'http://example.com' +@skip_pre_37 def test_forward_ref_dataclass_with_future_annotations(create_module): module = create_module( # language=Python @@ -324,6 +330,7 @@ class Account(BaseModel): } +@skip_pre_37 def test_self_reference_json_schema_with_future_annotations(create_module): module = create_module( # language=Python @@ -408,6 +415,7 @@ class Account(BaseModel): } +@skip_pre_37 def test_circular_reference_json_schema_with_future_annotations(create_module): module = create_module( # language=Python @@ -477,6 +485,7 @@ class Foo(BaseModel): c: List[Foo] = Field(..., gt=0) +@skip_pre_37 def test_forward_ref_optional(create_module): module = create_module( # language=Python @@ -522,6 +531,7 @@ def module(): assert instance.sub.dict() == {'foo': 'bar'} +@skip_pre_37 def test_resolve_forward_ref_dataclass(create_module): module = create_module( # language=Python @@ -601,6 +611,7 @@ class Dog(BaseModel): } +@skip_pre_37 def test_class_var_as_string(create_module): module = create_module( # language=Python @@ -617,6 +628,7 @@ class Model(BaseModel): assert module.Model.__class_vars__ == {'a'} +@skip_pre_37 def test_json_encoder_str(create_module): module = create_module( # language=Python @@ -650,6 +662,7 @@ class Config: assert m.json(models_as_dict=False) == '{"foo_user": {"x": "user1"}, "user": "User(user2)"}' +@skip_pre_37 def test_json_encoder_forward_ref(create_module): module = create_module( # language=Python diff --git a/tests/test_generics.py b/tests/test_generics.py index d65c0196a87..fb071b0cf82 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -23,7 +23,10 @@ from pydantic import BaseModel, Field, Json, ValidationError, root_validator, validator from pydantic.generics import GenericModel, _generic_types_cache, iter_contained_typevars, replace_types +skip_36 = pytest.mark.skipif(sys.version_info < (3, 7), reason='generics only supported for python 3.7 and above') + +@skip_36 def test_generic_name(): data_type = TypeVar('data_type') @@ -36,6 +39,7 @@ class Result(GenericModel, Generic[data_type]): assert Result[int].__name__ == 'Result[int]' +@skip_36 def test_double_parameterize_error(): data_type = TypeVar('data_type') @@ -48,6 +52,7 @@ class Result(GenericModel, Generic[data_type]): assert str(exc_info.value) == 'Cannot parameterize a concrete instantiation of a generic model' +@skip_36 def test_value_validation(): T = TypeVar('T') @@ -82,6 +87,7 @@ def validate_sum(cls, values): assert exc_info.value.errors() == [{'loc': ('__root__',), 'msg': 'sum too large', 'type': 'value_error'}] +@skip_36 def test_methods_are_inherited(): class CustomGenericModel(GenericModel): def method(self): @@ -97,6 +103,7 @@ class Model(CustomGenericModel, Generic[T]): assert instance.method() == 1 +@skip_36 def test_config_is_inherited(): class CustomGenericModel(GenericModel): class Config: @@ -115,6 +122,7 @@ class Model(CustomGenericModel, Generic[T]): assert str(exc_info.value) == '"Model[int]" is immutable and does not support item assignment' +@skip_36 def test_default_argument(): T = TypeVar('T') @@ -126,6 +134,7 @@ class Result(GenericModel, Generic[T]): assert result.other is True +@skip_36 def test_default_argument_for_typevar(): T = TypeVar('T') @@ -142,6 +151,7 @@ class Result(GenericModel, Generic[T]): assert result.data == 1 +@skip_36 def test_classvar(): T = TypeVar('T') @@ -155,6 +165,7 @@ class Result(GenericModel, Generic[T]): assert 'other' not in Result.__fields__ +@skip_36 def test_non_annotated_field(): T = TypeVar('T') @@ -169,6 +180,7 @@ class Result(GenericModel, Generic[T]): assert result.other is True +@skip_36 def test_must_inherit_from_generic(): with pytest.raises(TypeError) as exc_info: @@ -180,6 +192,7 @@ class Result(GenericModel): assert str(exc_info.value) == 'Type Result must inherit from typing.Generic before being parameterized' +@skip_36 def test_parameters_placed_on_generic(): T = TypeVar('T') with pytest.raises(TypeError, match='Type parameters should be placed on typing.Generic, not GenericModel'): @@ -188,6 +201,7 @@ class Result(GenericModel[T]): pass +@skip_36 def test_parameters_must_be_typevar(): with pytest.raises(TypeError, match='Type GenericModel must inherit from typing.Generic before being '): @@ -195,6 +209,7 @@ class Result(GenericModel[int]): pass +@skip_36 def test_subclass_can_be_genericized(): T = TypeVar('T') @@ -204,6 +219,7 @@ class Result(GenericModel, Generic[T]): Result[T] +@skip_36 def test_parameter_count(): T = TypeVar('T') S = TypeVar('S') @@ -221,6 +237,7 @@ class Model(GenericModel, Generic[T, S]): assert str(exc_info.value) == 'Too few parameters for Model; actual 1, expected 2' +@skip_36 def test_cover_cache(): cache_size = len(_generic_types_cache) T = TypeVar('T') @@ -234,6 +251,7 @@ class Model(GenericModel, Generic[T]): assert len(_generic_types_cache) == cache_size + 2 +@skip_36 def test_generic_config(): data_type = TypeVar('data_type') @@ -249,6 +267,7 @@ class Config: result.data = 2 +@skip_36 def test_enum_generic(): T = TypeVar('T') @@ -263,6 +282,7 @@ class Model(GenericModel, Generic[T]): Model[MyEnum](enum=2) +@skip_36 def test_generic(): data_type = TypeVar('data_type') error_type = TypeVar('error_type') @@ -317,6 +337,7 @@ class Data(BaseModel): ] +@skip_36 def test_alongside_concrete_generics(): from pydantic.generics import GenericModel @@ -331,6 +352,7 @@ class MyModel(GenericModel, Generic[T]): assert model.metadata == {} +@skip_36 def test_complex_nesting(): from pydantic.generics import GenericModel @@ -344,6 +366,7 @@ class MyModel(GenericModel, Generic[T]): assert model.item == item +@skip_36 def test_required_value(): T = TypeVar('T') @@ -355,6 +378,7 @@ class MyModel(GenericModel, Generic[T]): assert exc_info.value.errors() == [{'loc': ('a',), 'msg': 'field required', 'type': 'value_error.missing'}] +@skip_36 def test_optional_value(): T = TypeVar('T') @@ -365,6 +389,7 @@ class MyModel(GenericModel, Generic[T]): assert model.dict() == {'a': 1} +@skip_36 def test_custom_schema(): T = TypeVar('T') @@ -375,6 +400,7 @@ class MyModel(GenericModel, Generic[T]): assert schema['properties']['a'].get('description') == 'Custom' +@skip_36 def test_child_schema(): T = TypeVar('T') @@ -393,6 +419,7 @@ class Child(Model[T], Generic[T]): } +@skip_36 def test_custom_generic_naming(): T = TypeVar('T') @@ -409,6 +436,7 @@ def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str: assert repr(MyModel[str](value=None)) == 'OptionalStrWrapper(value=None)' +@skip_36 def test_nested(): AT = TypeVar('AT') @@ -440,6 +468,7 @@ class OuterT_SameType(GenericModel, Generic[AT]): ] +@skip_36 def test_partial_specification(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -459,6 +488,7 @@ class Model(GenericModel, Generic[AT, BT]): ] +@skip_36 def test_partial_specification_with_inner_typevar(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -478,6 +508,7 @@ class Model(GenericModel, Generic[AT, BT]): assert nested_resolved.b == [456] +@skip_36 def test_partial_specification_name(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -492,6 +523,7 @@ class Model(GenericModel, Generic[AT, BT]): assert concrete_model.__name__ == 'Model[int, BT][str]' +@skip_36 def test_partial_specification_instantiation(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -512,6 +544,7 @@ class Model(GenericModel, Generic[AT, BT]): ] +@skip_36 def test_partial_specification_instantiation_bounded(): AT = TypeVar('AT') BT = TypeVar('BT', bound=int) @@ -536,6 +569,7 @@ class Model(GenericModel, Generic[AT, BT]): ] +@skip_36 def test_typevar_parametrization(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -555,6 +589,7 @@ class Model(GenericModel, Generic[AT, BT]): ] +@skip_36 def test_multiple_specification(): AT = TypeVar('AT') BT = TypeVar('BT') @@ -575,6 +610,7 @@ class Model(GenericModel, Generic[AT, BT]): ] +@skip_36 def test_generic_subclass_of_concrete_generic(): T = TypeVar('T') U = TypeVar('U') @@ -596,6 +632,7 @@ class GenericSub(GenericBaseModel[int], Generic[U]): ConcreteSub(data=2, extra=3) +@skip_36 def test_generic_model_pickle(create_module): # Using create_module because pickle doesn't support # objects with in their __qualname__ (e. g. defined in function) @@ -624,6 +661,7 @@ class MyGeneric(GenericModel, Generic[t]): assert loaded == original +@skip_36 def test_generic_model_from_function_pickle_fail(create_module): @create_module def module(): @@ -652,6 +690,7 @@ def get_generic(t): pickle.dumps(original) +@skip_36 def test_generic_model_redefined_without_cache_fail(create_module, monkeypatch): # match identity checker otherwise we never get to the redefinition check @@ -733,6 +772,7 @@ def test_get_caller_frame_info_when_sys_getframe_undefined(): sys._getframe = getframe +@skip_36 def test_iter_contained_typevars(): T = TypeVar('T') T2 = TypeVar('T2') @@ -746,6 +786,7 @@ class Model(GenericModel, Generic[T]): assert list(iter_contained_typevars(Optional[List[Union[str, Model[T], Callable[[T2, T], str]]]])) == [T, T2, T] +@skip_36 def test_nested_identity_parameterization(): T = TypeVar('T') T2 = TypeVar('T2') @@ -758,6 +799,7 @@ class Model(GenericModel, Generic[T]): assert Model[T2] is not Model +@skip_36 def test_replace_types(): T = TypeVar('T') @@ -781,6 +823,7 @@ class Model(GenericModel, Generic[T]): assert replace_types(list[Union[str, list, T]], {T: int}) == list[Union[str, list, int]] +@skip_36 def test_replace_types_with_user_defined_generic_type_field(): """Test that using user defined generic types as generic model fields are handled correctly.""" @@ -804,6 +847,7 @@ class Model(GenericModel, Generic[T, KT, VT]): assert replace_types(Model[T, VT, KT], {T: bool, KT: str, VT: int}) == Model[T, VT, KT][bool, int, str] +@skip_36 def test_replace_types_identity_on_unchanged(): T = TypeVar('T') U = TypeVar('U') @@ -812,6 +856,7 @@ def test_replace_types_identity_on_unchanged(): assert replace_types(type_, {T: int}) is type_ +@skip_36 def test_deep_generic(): T = TypeVar('T') S = TypeVar('S') @@ -843,6 +888,7 @@ class NormalModel(BaseModel): assert inner_model.__concrete__ is True +@skip_36 def test_deep_generic_with_inner_typevar(): T = TypeVar('T') @@ -860,6 +906,7 @@ class InnerModel(OuterModel[T], Generic[T]): assert InnerModel[int](a=['1']).a == [1] +@skip_36 def test_deep_generic_with_referenced_generic(): T = TypeVar('T') R = TypeVar('R') @@ -881,6 +928,7 @@ class InnerModel(OuterModel[T], Generic[T]): assert InnerModel[int](a={'a': 1}).a.a == 1 +@skip_36 def test_deep_generic_with_referenced_inner_generic(): T = TypeVar('T') @@ -904,6 +952,7 @@ class InnerModel(OuterModel[T], Generic[T]): assert (InnerModel[int].__fields__['a'].sub_fields[0].sub_fields[0].outer_type_.__fields__['a'].outer_type_) == int +@skip_36 def test_deep_generic_with_multiple_typevars(): T = TypeVar('T') U = TypeVar('U') @@ -921,6 +970,7 @@ class InnerModel(OuterModel[T], Generic[U, T]): assert ConcreteInnerModel(data=['1'], extra='2').dict() == {'data': [1.0], 'extra': 2} +@skip_36 def test_deep_generic_with_multiple_inheritance(): K = TypeVar('K') V = TypeVar('V') @@ -948,6 +998,7 @@ class InnerModel(OuterModelA[K, V], OuterModelB[T], Generic[K, V, T]): } +@skip_36 def test_generic_with_referenced_generic_type_1(): T = TypeVar('T') @@ -962,6 +1013,7 @@ class ReferenceModel(GenericModel, Generic[T]): ReferenceModel[int] +@skip_36 def test_generic_with_referenced_nested_typevar(): T = TypeVar('T') @@ -977,6 +1029,7 @@ class ReferenceModel(GenericModel, Generic[T]): ReferenceModel[int] +@skip_36 def test_generic_with_callable(): T = TypeVar('T') @@ -988,6 +1041,7 @@ class Model(GenericModel, Generic[T]): Model.__concrete__ is False +@skip_36 def test_generic_with_partial_callable(): T = TypeVar('T') U = TypeVar('U') @@ -1003,6 +1057,7 @@ class Model(GenericModel, Generic[T, U]): Model[str, int].__concrete__ is False +@skip_36 def test_generic_recursive_models(create_module): @create_module def module(): @@ -1026,6 +1081,7 @@ class Model2(GenericModel, Generic[T]): assert result == Model1(ref=Model2(ref=Model1(ref=Model2(ref='123')))) +@skip_36 def test_generic_enum(): T = TypeVar('T') @@ -1043,6 +1099,7 @@ class MyModel(BaseModel): assert m.my_gen.some_field is SomeStringEnum.A +@skip_36 def test_generic_literal(): FieldType = TypeVar('FieldType') ValueType = TypeVar('ValueType') @@ -1055,6 +1112,7 @@ class GModel(GenericModel, Generic[FieldType, ValueType]): assert m.dict() == {'field': {'foo': 'x'}} +@skip_36 def test_generic_enums(): T = TypeVar('T') @@ -1074,6 +1132,7 @@ class Model(BaseModel): assert set(Model.schema()['definitions']) == {'EnumA', 'EnumB', 'GModel_EnumA_', 'GModel_EnumB_'} +@skip_36 def test_generic_with_user_defined_generic_field(): T = TypeVar('T') @@ -1091,6 +1150,7 @@ class Model(GenericModel, Generic[T]): model = Model[int](field=['a']) +@skip_36 def test_generic_annotated(): T = TypeVar('T') @@ -1100,6 +1160,7 @@ class SomeGenericModel(GenericModel, Generic[T]): SomeGenericModel[str](the_alias='qwe') +@skip_36 def test_generic_subclass(): T = TypeVar('T') @@ -1115,6 +1176,7 @@ class B(A[T], Generic[T]): assert not issubclass(B[int], A[str]) +@skip_36 def test_generic_subclass_with_partial_application(): T = TypeVar('T') S = TypeVar('S') @@ -1131,6 +1193,7 @@ class B(A[S], Generic[T, S]): assert not issubclass(PartiallyAppliedB[str], A[int]) +@skip_36 def test_multilevel_generic_binding(): T = TypeVar('T') S = TypeVar('S') @@ -1146,6 +1209,7 @@ class B(A[str, T], Generic[T]): assert not issubclass(B[str], A[str, int]) +@skip_36 def test_generic_subclass_with_extra_type(): T = TypeVar('T') S = TypeVar('S') @@ -1162,6 +1226,7 @@ class B(A[S], Generic[T, S]): assert not issubclass(B[int, str], A[int]) +@skip_36 def test_multi_inheritance_generic_binding(): T = TypeVar('T') @@ -1181,6 +1246,7 @@ class C(B[str], Generic[T]): assert not issubclass(C[float], A[str]) +@skip_36 def test_parse_generic_json(): T = TypeVar('T') diff --git a/tests/test_main.py b/tests/test_main.py index f4d3844c4aa..8cf290cbebb 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1883,6 +1883,7 @@ class Outer(BaseModel): assert repr(parsed) == 'Outer(inner_1=Inner(val=0), inner_2=Inner(val=0))' +@pytest.mark.skipif(sys.version_info < (3, 7), reason='field constraints are set but not enforced with python 3.6') def test_none_min_max_items(): # None default class Foo(BaseModel): @@ -2054,6 +2055,7 @@ class Model(BaseModel): assert repr(m) == "Model(x={'one': 1, 'two': 2})" +@pytest.mark.skipif(sys.version_info < (3, 7), reason='generic classes need 3.7') def test_typing_non_coercion_of_dict_subclasses(): KT = TypeVar('KT') VT = TypeVar('VT') diff --git a/tests/test_private_attributes.py b/tests/test_private_attributes.py index 5655ef72fba..0d3a0978efe 100644 --- a/tests/test_private_attributes.py +++ b/tests/test_private_attributes.py @@ -1,3 +1,4 @@ +import sys from typing import ClassVar, Generic, TypeVar import pytest @@ -6,6 +7,8 @@ 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(): default = {'a': {}} @@ -183,6 +186,7 @@ class Config: assert m._private_attr == 123 +@skip_36 def test_generic_private_attribute(): T = TypeVar('T') diff --git a/tests/test_schema.py b/tests/test_schema.py index a483e3fad1f..9369ef74671 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -2316,6 +2316,9 @@ class Model(BaseModel): } +@pytest.mark.skipif( + sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' +) def test_schema_for_generic_field(): T = TypeVar('T') @@ -2392,6 +2395,9 @@ class LocationBase(BaseModel): } +@pytest.mark.skipif( + sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' +) def test_advanced_generic_schema(): T = TypeVar('T') K = TypeVar('K') @@ -2502,6 +2508,9 @@ class Model(BaseModel): } +@pytest.mark.skipif( + sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' +) def test_nested_generic(): """ Test a nested BaseModel that is also a Generic @@ -2536,6 +2545,9 @@ class Model(BaseModel): } +@pytest.mark.skipif( + sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' +) def test_nested_generic_model(): """ Test a nested GenericModel @@ -2564,6 +2576,9 @@ class Model(BaseModel): } +@pytest.mark.skipif( + sys.version_info < (3, 7), reason='schema generation for generic fields is not available in python < 3.7' +) def test_complex_nested_generic(): """ Handle a union of a generic. diff --git a/tests/test_types.py b/tests/test_types.py index 8e5481719c2..1e9b231b22d 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -2459,7 +2459,8 @@ class Foobar(BaseModel): pattern: Pattern f = Foobar(pattern=r'^whatev.r\d$') - assert f.pattern.__class__.__name__ == 'Pattern' + # SRE_Pattern for 3.6, Pattern for 3.7 + assert f.pattern.__class__.__name__ in {'SRE_Pattern', 'Pattern'} # check it's really a proper pattern assert f.pattern.match('whatever1') assert not f.pattern.match(' whatever1') @@ -2990,10 +2991,13 @@ class DefaultModel(BaseModel): assert DefaultModel(v=1).dict() == {'v': 1} assert DefaultModel(v='1').dict() == {'v': 1} + # In 3.6, Union[int, bool, str] == Union[int, str] + allowed_json_types = ('integer', 'string') if sys.version_info[:2] == (3, 6) else ('integer', 'boolean', 'string') + assert DefaultModel.schema() == { 'title': 'DefaultModel', 'type': 'object', - 'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in ('integer', 'boolean', 'string')]}}, + 'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in allowed_json_types]}}, 'required': ['v'], } @@ -3009,10 +3013,13 @@ class Config: assert SmartModel(v=True).dict() == {'v': True} assert SmartModel(v='1').dict() == {'v': '1'} + # In 3.6, Union[int, bool, str] == Union[int, str] + allowed_json_types = ('integer', 'string') if sys.version_info[:2] == (3, 6) else ('integer', 'boolean', 'string') + assert SmartModel.schema() == { 'title': 'SmartModel', 'type': 'object', - 'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in ('integer', 'boolean', 'string')]}}, + 'properties': {'v': {'title': 'V', 'anyOf': [{'type': t} for t in allowed_json_types]}}, 'required': ['v'], } diff --git a/tests/test_utils.py b/tests/test_utils.py index d7eaa74c697..e27a2d9b990 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -464,6 +464,7 @@ def test_smart_deepcopy_collection(collection, mocker): T = TypeVar('T') +@pytest.mark.skipif(sys.version_info < (3, 7), reason='get_origin is only consistent for python >= 3.7') @pytest.mark.parametrize( 'input_value,output_value', [ From 07ed83d77ae8e0083829c6445d2a76969089087f Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 14 May 2022 16:40:10 +0100 Subject: [PATCH 58/62] revert packages to support 3.6 --- requirements.txt | 2 +- tests/requirements-linting.txt | 6 +++--- tests/requirements-testing.txt | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index d82a66c99a2..28ab08875b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,5 @@ Cython==0.29.28;sys_platform!='win32' devtools==0.8.0 email-validator==1.2.1 dataclasses==0.6; python_version < '3.7' -typing-extensions==4.2.0 +typing-extensions==4.1.0 python-dotenv==0.20.0 diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index 2574a34b083..ade77b49293 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -1,10 +1,10 @@ black==22.3.0 flake8==4.0.1 flake8-quotes==3.3.1 -hypothesis==6.46.3 +hypothesis==6.31.6 isort==5.10.1 mypy==0.950 -pre-commit==2.19.0 +pre-commit==2.17.0 pycodestyle==2.8.0 pyflakes==2.4.0 -twine==4.0.0 +twine==3.8.0 diff --git a/tests/requirements-testing.txt b/tests/requirements-testing.txt index 47ca8d1eefb..c4bd5e5e198 100644 --- a/tests/requirements-testing.txt +++ b/tests/requirements-testing.txt @@ -1,9 +1,9 @@ -coverage==6.3.2 -hypothesis==6.46.3 +coverage==6.2 +hypothesis==6.31.6 # 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.950 -pytest==7.1.2 +pytest==7.0.1 pytest-cov==3.0.0 -pytest-mock==3.7.0 +pytest-mock==3.6.1 pytest-sugar==0.9.4 From 4d17a4cbfd89b61ea4de0397834275355f2ec63a Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 14 May 2022 17:03:22 +0100 Subject: [PATCH 59/62] user newer twine --- .github/workflows/ci.yml | 2 +- tests/requirements-linting.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 515b0821b7f..5cb39166301 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,9 +4,9 @@ on: push: branches: - master + - 1.9.X-fixes tags: - '**' - pull_request: {} jobs: lint: diff --git a/tests/requirements-linting.txt b/tests/requirements-linting.txt index ade77b49293..54faa414c80 100644 --- a/tests/requirements-linting.txt +++ b/tests/requirements-linting.txt @@ -7,4 +7,4 @@ mypy==0.950 pre-commit==2.17.0 pycodestyle==2.8.0 pyflakes==2.4.0 -twine==3.8.0 +twine==4.0.0 From 91a2ffc7c9557858d096f9eea9ab95e4a6d42a2c Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Sat, 14 May 2022 17:34:30 +0100 Subject: [PATCH 60/62] fix typing tests with 3.6 --- tests/test_typing.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 9e5936574ef..7d2b703d0d7 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -1,6 +1,6 @@ import sys from collections import namedtuple -from typing import Any, Callable as TypingCallable, Dict, ForwardRef, List, NamedTuple, NewType, Union # noqa: F401 +from typing import Any, Callable as TypingCallable, Dict, List, NamedTuple, NewType, Union # noqa: F401 import pytest from typing_extensions import Annotated # noqa: F401 @@ -24,6 +24,12 @@ except ImportError: mypy_extensions_TypedDict = None +try: + from typing import ForwardRef +except ImportError: + # ForwardRef is only available in Python 3.6+ + pass + ALL_TYPEDDICT_KINDS = (typing_TypedDict, typing_extensions_TypedDict, mypy_extensions_TypedDict) From 6ac2fcde487aefeb76f2f88a4f2c34afd8c8ecc7 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 19 May 2022 10:52:57 +0100 Subject: [PATCH 61/62] make #4083 compatible with py3.6 --- pydantic/generics.py | 4 ++-- pydantic/utils.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pydantic/generics.py b/pydantic/generics.py index 2467bb77111..1ec9a7da081 100644 --- a/pydantic/generics.py +++ b/pydantic/generics.py @@ -31,13 +31,13 @@ Parametrization = Mapping[TypeVarType, Type[Any]] -_generic_types_cache: LimitedDict[Tuple[Type[Any], Union[Any, Tuple[Any, ...]]], Type[BaseModel]] = LimitedDict() +_generic_types_cache: 'LimitedDict[Tuple[Type[Any], Union[Any, Tuple[Any, ...]]], Type[BaseModel]]' = LimitedDict() # _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: LimitedDict[Type[Any], Parametrization] = LimitedDict() +_assigned_parameters: 'LimitedDict[Type[Any], Parametrization]' = LimitedDict() class GenericModel(BaseModel): diff --git a/pydantic/utils.py b/pydantic/utils.py index f9c4ec16022..a3c87f4bf39 100644 --- a/pydantic/utils.py +++ b/pydantic/utils.py @@ -784,6 +784,6 @@ def __setitem__(self, __key: Any, __value: Any) -> None: for key in to_remove: del self[key] - def __class_getitem__(cls, *args: Any) -> Any: - # to avoid errors with 3.7 + def __class_getitem__(cls, *args: Any) -> Any: # pragma: no cover + # just in case LimitedDict is used in type annotations pass From a5edcb35d57a543143b0dc127dbcf8e143c38e6d Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 19 May 2022 11:33:13 +0100 Subject: [PATCH 62/62] generate history from changes, uprev --- HISTORY.md | 29 +++++++++++++++++++++++++++++ changes/3608-samuelcolvin.md | 1 - changes/3625-hswong3i.md | 1 - changes/3636-tommilligan.md | 1 - changes/3641-PrettyWood.md | 1 - changes/3652-dolfinus.md | 1 - changes/3675-uriyyo.md | 1 - changes/3679-samuelcolvin.md | 1 - changes/3681-aleksul.md | 1 - changes/3706-samuelcolvin.md | 1 - changes/3806-garyd203.md | 1 - changes/3819-himbeles.md | 1 - changes/3972-samuelcolvin.md | 1 - changes/3973-samuelcolvin.md | 1 - changes/4006-giuliano-oliveira.md | 1 - changes/4067-adriangb.md | 1 - changes/4081-samuelcolvin.md | 1 - changes/4082-davidbrochart.md | 1 - changes/4083-samuelcolvin.md | 2 -- pydantic/version.py | 2 +- 20 files changed, 30 insertions(+), 20 deletions(-) delete mode 100644 changes/3608-samuelcolvin.md delete mode 100644 changes/3625-hswong3i.md delete mode 100644 changes/3636-tommilligan.md delete mode 100644 changes/3641-PrettyWood.md delete mode 100644 changes/3652-dolfinus.md delete mode 100644 changes/3675-uriyyo.md delete mode 100644 changes/3679-samuelcolvin.md delete mode 100644 changes/3681-aleksul.md delete mode 100644 changes/3706-samuelcolvin.md delete mode 100644 changes/3806-garyd203.md delete mode 100644 changes/3819-himbeles.md delete mode 100644 changes/3972-samuelcolvin.md delete mode 100644 changes/3973-samuelcolvin.md delete mode 100644 changes/4006-giuliano-oliveira.md delete mode 100644 changes/4067-adriangb.md delete mode 100644 changes/4081-samuelcolvin.md delete mode 100644 changes/4082-davidbrochart.md delete mode 100644 changes/4083-samuelcolvin.md diff --git a/HISTORY.md b/HISTORY.md index 63ba284c00c..b5ba45b80e0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,32 @@ +## v1.9.1 (2022-05-19) + +Thank you to pydantic's sponsors: +@tiangolo, @stellargraph, @JonasKs, @grillazz, @Mazyod, @kevinalh, @chdsbd, @povilasb, @povilasb, @jina-ai, +@mainframeindustries, @robusta-dev, @SendCloud, @rszamszur, @jodal, @hardbyte, @corleyma, @daddycocoaman, +@Rehket, @jokull, @reillysiemens, @westonsteimel, @primer-io, @koxudaxi, @browniebroke, @stradivari96, +@adriangb, @kamalgill, @jqueguiner, @dev-zero, @datarootsio, @RedCarpetUp +for their kind support. + +* Limit the size of `generics._generic_types_cache` and `generics._assigned_parameters` + to avoid unlimited increase in memory usage, #4083 by @samuelcolvin +* Add Jupyverse and FPS as Jupyter projects using pydantic, #4082 by @davidbrochart +* Speedup `__isinstancecheck__` on pydantic models when the type is not a model, may also avoid memory "leaks", #4081 by @samuelcolvin +* Fix in-place modification of `FieldInfo` that caused problems with PEP 593 type aliases, #4067 by @adriangb +* Add support for autocomplete in VS Code via `__dataclass_transform__` when using `pydantic.dataclasses.dataclass`, #4006 by @giuliano-oliveira +* Remove benchmarks from codebase and docs, #3973 by @samuelcolvin +* Typing checking with pyright in CI, improve docs on vscode/pylance/pyright, #3972 by @samuelcolvin +* Fix nested Python dataclass schema regression, #3819 by @himbeles +* Update documentation about lazy evaluation of sources for Settings, #3806 by @garyd203 +* Prevent subclasses of bytes being converted to bytes, #3706 by @samuelcolvin +* Fixed "error checking inheritance of" when using PEP585 and PEP604 type hints, #3681 by @aleksul +* Allow self referencing `ClassVar`s in models, #3679 by @samuelcolvin +* Fix issue with self-referencing dataclass, #3675 by @uriyyo +* Include non-standard port numbers in rendered URLs, #3652 by @dolfinus +* `Config.copy_on_model_validation` does a deep copy and not a shallow one, #3641 by @PrettyWood +* fix: clarify that discriminated unions do not support singletons, #3636 by @tommilligan +* Add `read_text(encoding='utf-8')` for `setup.py`, #3625 by @hswong3i +* Fix JSON Schema generation for Discriminated Unions within lists, #3608 by @samuelcolvin + ## v1.9.0 (2021-12-31) Thank you to pydantic's sponsors: diff --git a/changes/3608-samuelcolvin.md b/changes/3608-samuelcolvin.md deleted file mode 100644 index ec3c0bafca0..00000000000 --- a/changes/3608-samuelcolvin.md +++ /dev/null @@ -1 +0,0 @@ -Fix JSON Schema generation for Discriminated Unions within lists. diff --git a/changes/3625-hswong3i.md b/changes/3625-hswong3i.md deleted file mode 100644 index a9365e5206d..00000000000 --- a/changes/3625-hswong3i.md +++ /dev/null @@ -1 +0,0 @@ -Add `read_text(encoding='utf-8')` for `setup.py` diff --git a/changes/3636-tommilligan.md b/changes/3636-tommilligan.md deleted file mode 100644 index ec10fce7ea4..00000000000 --- a/changes/3636-tommilligan.md +++ /dev/null @@ -1 +0,0 @@ -fix: clarify that discriminated unions do not support singletons diff --git a/changes/3641-PrettyWood.md b/changes/3641-PrettyWood.md deleted file mode 100644 index d0338c66369..00000000000 --- a/changes/3641-PrettyWood.md +++ /dev/null @@ -1 +0,0 @@ -`Config.copy_on_model_validation` does a deep copy and not a shallow one \ No newline at end of file diff --git a/changes/3652-dolfinus.md b/changes/3652-dolfinus.md deleted file mode 100644 index 907a440c69a..00000000000 --- a/changes/3652-dolfinus.md +++ /dev/null @@ -1 +0,0 @@ -Include non-standard port numbers in rendered URLs diff --git a/changes/3675-uriyyo.md b/changes/3675-uriyyo.md deleted file mode 100644 index 7a34d4d2064..00000000000 --- a/changes/3675-uriyyo.md +++ /dev/null @@ -1 +0,0 @@ -Fix issue with self-referencing dataclass diff --git a/changes/3679-samuelcolvin.md b/changes/3679-samuelcolvin.md deleted file mode 100644 index e02ebc7febf..00000000000 --- a/changes/3679-samuelcolvin.md +++ /dev/null @@ -1 +0,0 @@ -Allow self referencing `ClassVar`s in models. diff --git a/changes/3681-aleksul.md b/changes/3681-aleksul.md deleted file mode 100644 index 13f745ac7d3..00000000000 --- a/changes/3681-aleksul.md +++ /dev/null @@ -1 +0,0 @@ -Fixed "error checking inheritance of" when using PEP585 and PEP604 type hints diff --git a/changes/3706-samuelcolvin.md b/changes/3706-samuelcolvin.md deleted file mode 100644 index 3a22afee678..00000000000 --- a/changes/3706-samuelcolvin.md +++ /dev/null @@ -1 +0,0 @@ -Prevent subclasses of bytes being converted to bytes diff --git a/changes/3806-garyd203.md b/changes/3806-garyd203.md deleted file mode 100644 index 25b2217bb47..00000000000 --- a/changes/3806-garyd203.md +++ /dev/null @@ -1 +0,0 @@ -Update documentation about lazy evaluation of sources for Settings (it's not actually done). diff --git a/changes/3819-himbeles.md b/changes/3819-himbeles.md deleted file mode 100644 index 7845d7b0f93..00000000000 --- a/changes/3819-himbeles.md +++ /dev/null @@ -1 +0,0 @@ -Fix nested Python dataclass schema regression in version 1.9 diff --git a/changes/3972-samuelcolvin.md b/changes/3972-samuelcolvin.md deleted file mode 100644 index 42f24982775..00000000000 --- a/changes/3972-samuelcolvin.md +++ /dev/null @@ -1 +0,0 @@ -Typing checking with pyright in CI, improve docs on vscode/pylance/pyright. diff --git a/changes/3973-samuelcolvin.md b/changes/3973-samuelcolvin.md deleted file mode 100644 index a6838a23e1e..00000000000 --- a/changes/3973-samuelcolvin.md +++ /dev/null @@ -1 +0,0 @@ -Remove benchmarks from codebase and docs. diff --git a/changes/4006-giuliano-oliveira.md b/changes/4006-giuliano-oliveira.md deleted file mode 100644 index 97a7eeccfc7..00000000000 --- a/changes/4006-giuliano-oliveira.md +++ /dev/null @@ -1 +0,0 @@ -Add support for autocomplete in VS Code via `__dataclass_transform__` when using `pydantic.dataclasses.dataclass` diff --git a/changes/4067-adriangb.md b/changes/4067-adriangb.md deleted file mode 100644 index 4c6689ebe5e..00000000000 --- a/changes/4067-adriangb.md +++ /dev/null @@ -1 +0,0 @@ -Fix in-place modification of `FieldInfo` that caused problems with PEP 593 type aliases diff --git a/changes/4081-samuelcolvin.md b/changes/4081-samuelcolvin.md deleted file mode 100644 index 53fd4f52a89..00000000000 --- a/changes/4081-samuelcolvin.md +++ /dev/null @@ -1 +0,0 @@ -Speedup `__isinstancecheck__` on pydantic models when the type is not a model, may also avoid memory "leaks". diff --git a/changes/4082-davidbrochart.md b/changes/4082-davidbrochart.md deleted file mode 100644 index cad2efbb829..00000000000 --- a/changes/4082-davidbrochart.md +++ /dev/null @@ -1 +0,0 @@ -Add Jupyverse and FPS as Jupyter projects using pydantic diff --git a/changes/4083-samuelcolvin.md b/changes/4083-samuelcolvin.md deleted file mode 100644 index ed252f16b5a..00000000000 --- a/changes/4083-samuelcolvin.md +++ /dev/null @@ -1,2 +0,0 @@ -Limit the size of `generics._generic_types_cache` and `generics._assigned_parameters` -to avoid unlimited increase in memory usage. diff --git a/pydantic/version.py b/pydantic/version.py index 5b1ebc33e98..3c885370c22 100644 --- a/pydantic/version.py +++ b/pydantic/version.py @@ -1,6 +1,6 @@ __all__ = 'VERSION', 'version_info' -VERSION = '1.9.0' +VERSION = '1.9.1' def version_info() -> str: