diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c33c64d3a0..d7760d88fd 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 98e022ac60..c4ddb13ad2 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 0000000000..305c118e9d --- /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 01c3b441aa..2fd4fa6ea7 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 15341596b8..30b3c0fa4e 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 ef07de3a68..3b302d65bb 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 072038d415..8f7bc8237b 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 537bca4e3e..ff51c5b7a6 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 b37cd98ff1..ef4b3c008f 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 7f0094cc46..6f16990323 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 ce956fea26..b732cc0a2a 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 730dc46442..c6e9da18ff 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 fb22f35fbb..ae91cf9a0d 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 7dd992a6da..4c57862f0a 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 217eebcd32..ee383c526f 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 51611e373a..11e3db10ce 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 80365a9df8..662ad6828d 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 26ee50c39e..5b782e4396 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 5f772eb72e..4e6d173ae4 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 97b78f88e8..c2503dd72f 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 c7cd5f4e5a..120f9d2f16 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 227881b8f3..dd07eb3d37 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 0f901b4edd..7224372df6 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 df378810cd..61e7be5cfb 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 fb071b0cf8..d65c0196a8 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 71da72544b..642b5610b3 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 0d3a0978ef..5655ef72fb 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 eb1f081e82..ea0290f09e 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 bbf4c23b1e..a7b7db89f0 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 9c0ab4fb94..132081a705 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', [