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

Add python 3.10 support #2885

Merged
merged 17 commits into from Jul 19, 2021
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Expand Up @@ -131,7 +131,7 @@ jobs:
fail-fast: false
matrix:
os: [macos, windows]
python-version: ['3.6', '3.7', '3.8', '3.9']
python-version: ['3.6', '3.7', '3.8', '3.9', '3.10.0-beta.2']
env:
PYTHON: ${{ matrix.python-version }}
OS: ${{ matrix.os }}
Expand Down
1 change: 1 addition & 0 deletions changes/2885-PrettyWood.md
@@ -0,0 +1 @@
add python 3.10 support
2 changes: 1 addition & 1 deletion 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 **python 3.6**, **3.7**, **3.8**, or **3.9**, **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
Expand Down
2 changes: 1 addition & 1 deletion docs/install.md
Expand Up @@ -4,7 +4,7 @@ Installation is as simple as:
pip install pydantic
```

*pydantic* has no required dependencies except python 3.6, 3.7, 3.8, or 3.9,
*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.
Expand Down
6 changes: 4 additions & 2 deletions pydantic/__init__.py
Expand Up @@ -2,6 +2,7 @@
from . import dataclasses
from .annotated_types import create_model_from_namedtuple, create_model_from_typeddict
from .class_validators import root_validator, validator
from .config import BaseConfig, Extra
from .decorator import validate_arguments
from .env_settings import BaseSettings
from .error_wrappers import ValidationError
Expand All @@ -27,6 +28,9 @@
# class_validators
'root_validator',
'validator',
# config
'BaseConfig',
'Extra',
# decorator
'validate_arguments',
# env_settings
Expand All @@ -37,9 +41,7 @@
'Field',
'Required',
# main
'BaseConfig',
'BaseModel',
'Extra',
'compiled',
'create_model',
'validate_model',
Expand Down
7 changes: 4 additions & 3 deletions pydantic/annotated_types.py
Expand Up @@ -42,9 +42,10 @@ def create_model_from_namedtuple(namedtuple_cls: Type['NamedTuple'], **kwargs: A
but also with `collections.namedtuple`, in this case we consider all fields
to have type `Any`.
"""
namedtuple_annotations: Dict[str, Type[Any]] = getattr(
namedtuple_cls, '__annotations__', {k: Any for k in namedtuple_cls._fields}
)
# With python 3.10+, `__annotations__` always exists but can be empty hence the `getattr... or...` logic
namedtuple_annotations: Dict[str, Type[Any]] = getattr(namedtuple_cls, '__annotations__', None) or {
k: Any for k in namedtuple_cls._fields
}
field_definitions: Dict[str, Any] = {
field_name: (field_type, Required) for field_name, field_type in namedtuple_annotations.items()
}
Expand Down
2 changes: 1 addition & 1 deletion pydantic/class_validators.py
Expand Up @@ -33,8 +33,8 @@ def __init__(
if TYPE_CHECKING:
from inspect import Signature

from .config import BaseConfig
from .fields import ModelField
from .main import BaseConfig
from .types import ModelOrDc

ValidatorCallable = Callable[[Optional[ModelOrDc], Any, Dict[str, Any], ModelField, Type[BaseConfig]], Any]
Expand Down
124 changes: 124 additions & 0 deletions pydantic/config.py
@@ -0,0 +1,124 @@
import json
from enum import Enum
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Type, Union

from .typing import AnyCallable
from .utils import GetterDict

if TYPE_CHECKING:
from typing import overload

import typing_extensions

from .fields import ModelField
from .main import BaseModel

ConfigType = Type['BaseConfig']

class SchemaExtraCallable(typing_extensions.Protocol):
@overload
def __call__(self, schema: Dict[str, Any]) -> None:
pass

@overload
def __call__(self, schema: Dict[str, Any], model_class: Type[BaseModel]) -> None:
pass


else:
SchemaExtraCallable = Callable[..., None]

__all__ = 'BaseConfig', 'Extra', 'inherit_config', 'prepare_config'


class Extra(str, Enum):
allow = 'allow'
ignore = 'ignore'
forbid = 'forbid'


class BaseConfig:
samuelcolvin marked this conversation as resolved.
Show resolved Hide resolved
title = None
anystr_lower = False
anystr_strip_whitespace = False
min_anystr_length = None
max_anystr_length = None
validate_all = False
extra = Extra.ignore
allow_mutation = True
frozen = False
allow_population_by_field_name = False
use_enum_values = False
fields: Dict[str, Union[str, Dict[str, str]]] = {}
validate_assignment = False
error_msg_templates: Dict[str, str] = {}
arbitrary_types_allowed = False
orm_mode: bool = False
getter_dict: Type[GetterDict] = GetterDict
alias_generator: Optional[Callable[[str], str]] = None
keep_untouched: Tuple[type, ...] = ()
schema_extra: Union[Dict[str, Any], 'SchemaExtraCallable'] = {}
json_loads: Callable[[str], Any] = json.loads
json_dumps: Callable[..., str] = json.dumps
json_encoders: Dict[Type[Any], AnyCallable] = {}
underscore_attrs_are_private: bool = False

# Whether or not inherited models as fields should be reconstructed as base model
copy_on_model_validation: bool = True

@classmethod
def get_field_info(cls, name: str) -> Dict[str, Any]:
"""
Get properties of FieldInfo from the `fields` property of the config class.
"""

fields_value = cls.fields.get(name)

if isinstance(fields_value, str):
field_info: Dict[str, Any] = {'alias': fields_value}
elif isinstance(fields_value, dict):
field_info = fields_value
else:
field_info = {}

if 'alias' in field_info:
field_info.setdefault('alias_priority', 2)

if field_info.get('alias_priority', 0) <= 1 and cls.alias_generator:
alias = cls.alias_generator(name)
if not isinstance(alias, str):
raise TypeError(f'Config.alias_generator must return str, not {alias.__class__}')
field_info.update(alias=alias, alias_priority=1)
return field_info

@classmethod
def prepare_field(cls, field: 'ModelField') -> None:
"""
Optional hook to check or modify fields during model creation.
"""
pass


def inherit_config(self_config: 'ConfigType', parent_config: 'ConfigType', **namespace: Any) -> 'ConfigType':
if not self_config:
base_classes: Tuple['ConfigType', ...] = (parent_config,)
elif self_config == parent_config:
base_classes = (self_config,)
else:
base_classes = self_config, parent_config

namespace['json_encoders'] = {
**getattr(parent_config, 'json_encoders', {}),
**getattr(self_config, 'json_encoders', {}),
**namespace.get('json_encoders', {}),
}

return type('Config', base_classes, namespace)


def prepare_config(config: Type[BaseConfig], cls_name: str) -> None:
if not isinstance(config.extra, Extra):
try:
config.extra = Extra(config.extra)
except ValueError:
raise ValueError(f'"{cls_name}": {config.extra} is not a valid value for "extra"')
3 changes: 2 additions & 1 deletion pydantic/dataclasses.py
Expand Up @@ -9,7 +9,8 @@
from .utils import ClassAttribute

if TYPE_CHECKING:
from .main import BaseConfig, BaseModel # noqa: F401
from .config import BaseConfig
from .main import BaseModel
from .typing import CallableGenerator, NoArgAnyCallable

DataclassT = TypeVar('DataclassT', bound='Dataclass')
Expand Down
3 changes: 2 additions & 1 deletion pydantic/decorator.py
Expand Up @@ -2,8 +2,9 @@
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, TypeVar, Union, overload

from . import validator
from .config import Extra
from .errors import ConfigError
from .main import BaseModel, Extra, create_model
from .main import BaseModel, create_model
from .typing import get_all_type_hints
from .utils import to_camel

Expand Down
3 changes: 2 additions & 1 deletion pydantic/env_settings.py
Expand Up @@ -3,8 +3,9 @@
from pathlib import Path
from typing import AbstractSet, Any, Callable, Dict, List, Mapping, Optional, Tuple, Union

from .config import BaseConfig, Extra
from .fields import ModelField
from .main import BaseConfig, BaseModel, Extra
from .main import BaseModel
from .typing import display_as_type
from .utils import deep_update, path_type, sequence_like

Expand Down
4 changes: 2 additions & 2 deletions pydantic/error_wrappers.py
Expand Up @@ -5,8 +5,8 @@
from .utils import Representation

if TYPE_CHECKING:
from .main import BaseConfig # noqa: F401
from .types import ModelOrDc # noqa: F401
from .config import BaseConfig
from .types import ModelOrDc
from .typing import ReprArgs

Loc = Tuple[Union[int, str], ...]
Expand Down
18 changes: 10 additions & 8 deletions pydantic/fields.py
@@ -1,5 +1,5 @@
from collections import defaultdict, deque
from collections.abc import Iterable as CollectionsIterable
from collections.abc import Hashable as CollectionsHashable, Iterable as CollectionsIterable
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -41,6 +41,7 @@
is_literal_type,
is_new_type,
is_typeddict,
is_union,
new_type_supertype,
)
from .utils import PyObjectStr, Representation, ValueItems, lenient_issubclass, sequence_like, smart_deepcopy
Expand Down Expand Up @@ -68,11 +69,11 @@ def __deepcopy__(self: T, _: Any) -> T:
Undefined = UndefinedType()

if TYPE_CHECKING:
from .class_validators import ValidatorsList # noqa: F401
from .class_validators import ValidatorsList
from .config import BaseConfig
from .error_wrappers import ErrorList
from .main import BaseConfig, BaseModel # noqa: F401
from .types import ModelOrDc # noqa: F401
from .typing import AbstractSetIntStr, MappingIntStrAny, ReprArgs # noqa: F401
from .types import ModelOrDc
from .typing import AbstractSetIntStr, MappingIntStrAny, ReprArgs

ValidateReturn = Tuple[Optional[Any], Optional[ErrorList]]
LocStr = Union[Tuple[Union[int, str], ...], str]
Expand Down Expand Up @@ -543,7 +544,8 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity)
return

origin = get_origin(self.type_)
if origin is None:
# add extra check for `collections.abc.Hashable` for python 3.10+ where origin is not `None`
if origin is None or origin is CollectionsHashable:
# field is not "typing" object eg. Union, Dict, List etc.
# allow None for virtual superclasses of NoneType, e.g. Hashable
if isinstance(self.type_, type) and isinstance(None, self.type_):
Expand All @@ -555,7 +557,7 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity)
return
if origin is Callable:
return
if origin is Union:
if is_union(origin):
types_ = []
for type_ in get_args(self.type_):
if type_ is NoneType:
Expand Down Expand Up @@ -948,7 +950,7 @@ def is_complex(self) -> bool:
"""
Whether the field is "complex" eg. env variables should be parsed as JSON.
"""
from .main import BaseModel # noqa: F811
from .main import BaseModel

return (
self.shape != SHAPE_SINGLETON
Expand Down