Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

drop python3.6 support #3605

Merged
merged 5 commits into from Jan 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Expand Up @@ -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
Expand Down Expand Up @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions changes/3605-samuelcolvin.md
@@ -0,0 +1 @@
Drop support for python3.6, associated cleanup
4 changes: 2 additions & 2 deletions docs/contributing.md
Expand Up @@ -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
Expand All @@ -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
Expand Down
7 changes: 3 additions & 4 deletions docs/install.md
Expand Up @@ -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:
Expand Down
2 changes: 0 additions & 2 deletions 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!}
```
Expand Down
4 changes: 0 additions & 4 deletions docs/usage/models.md
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion 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/

Expand Down
5 changes: 2 additions & 3 deletions 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
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion pydantic/fields.py
Expand Up @@ -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:
Expand Down
9 changes: 1 addition & 8 deletions 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
Expand Down
109 changes: 27 additions & 82 deletions 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,
Expand Down Expand Up @@ -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)
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super weird 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ye, things like this scare me a little 😨.

return cast(Type[Any], Annotated)
return getattr(t, '__origin__', None)

else:
Expand All @@ -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, ...]:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand All @@ -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, ...]:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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]:
Expand Down
6 changes: 2 additions & 4 deletions setup.py
Expand Up @@ -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()
Expand Down Expand Up @@ -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',
Expand All @@ -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={
Expand Down
2 changes: 1 addition & 1 deletion tests/mypy/modules/plugin_fail.py
Expand Up @@ -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')


Expand Down