diff --git a/changes/1832-PrettyWood.md b/changes/1832-PrettyWood.md new file mode 100644 index 000000000..5038a0da0 --- /dev/null +++ b/changes/1832-PrettyWood.md @@ -0,0 +1 @@ +add basic support of python 3.9 diff --git a/pydantic/fields.py b/pydantic/fields.py index 01792b628..c52b34ea4 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -32,6 +32,8 @@ NoArgAnyCallable, NoneType, display_as_type, + get_args, + get_origin, is_literal_type, is_new_type, new_type_supertype, @@ -411,7 +413,7 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) elif is_literal_type(self.type_): return - origin = getattr(self.type_, '__origin__', None) + origin = get_origin(self.type_) if origin is None: # field is not "typing" object eg. Union, Dict, List etc. # allow None for virtual superclasses of NoneType, e.g. Hashable @@ -422,7 +424,7 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) return if origin is Union: types_ = [] - for type_ in self.type_.__args__: + for type_ in get_args(self.type_): if type_ is NoneType: if self.required is Undefined: self.required = False @@ -444,9 +446,9 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) if issubclass(origin, Tuple): # type: ignore self.shape = SHAPE_TUPLE self.sub_fields = [] - for i, t in enumerate(self.type_.__args__): + for i, t in enumerate(get_args(self.type_)): if t is Ellipsis: - self.type_ = self.type_.__args__[0] + self.type_ = get_args(self.type_)[0] self.shape = SHAPE_TUPLE_ELLIPSIS return self.sub_fields.append(self._create_sub_type(t, f'{self.name}_{i}')) @@ -460,7 +462,7 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) {f'list_{i}': Validator(validator, pre=True) for i, validator in enumerate(get_validators())} ) - self.type_ = self.type_.__args__[0] + self.type_ = get_args(self.type_)[0] self.shape = SHAPE_LIST elif issubclass(origin, Set): # Create self validators @@ -470,22 +472,22 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) {f'set_{i}': Validator(validator, pre=True) for i, validator in enumerate(get_validators())} ) - self.type_ = self.type_.__args__[0] + self.type_ = get_args(self.type_)[0] self.shape = SHAPE_SET elif issubclass(origin, FrozenSet): - self.type_ = self.type_.__args__[0] + self.type_ = get_args(self.type_)[0] self.shape = SHAPE_FROZENSET elif issubclass(origin, Sequence): - self.type_ = self.type_.__args__[0] + self.type_ = get_args(self.type_)[0] self.shape = SHAPE_SEQUENCE elif issubclass(origin, Mapping): - self.key_field = self._create_sub_type(self.type_.__args__[0], 'key_' + self.name, for_keys=True) - self.type_ = self.type_.__args__[1] + self.key_field = self._create_sub_type(get_args(self.type_)[0], 'key_' + self.name, for_keys=True) + self.type_ = get_args(self.type_)[1] self.shape = SHAPE_MAPPING # Equality check as almost everything inherits form Iterable, including str # check for Iterable and CollectionsIterable, as it could receive one even when declared with the other elif origin in {Iterable, CollectionsIterable}: - self.type_ = self.type_.__args__[0] + self.type_ = get_args(self.type_)[0] self.shape = SHAPE_ITERABLE self.sub_fields = [self._create_sub_type(self.type_, f'{self.name}_type')] elif issubclass(origin, Type): # type: ignore @@ -494,7 +496,7 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity) # Is a Pydantic-compatible generic that handles itself # or we have arbitrary_types_allowed = True self.shape = SHAPE_GENERIC - self.sub_fields = [self._create_sub_type(t, f'{self.name}_{i}') for i, t in enumerate(self.type_.__args__)] + self.sub_fields = [self._create_sub_type(t, f'{self.name}_{i}') for i, t in enumerate(get_args(self.type_))] self.type_ = origin return else: diff --git a/pydantic/generics.py b/pydantic/generics.py index 64562227d..0a5e75401 100644 --- a/pydantic/generics.py +++ b/pydantic/generics.py @@ -3,6 +3,7 @@ from .class_validators import gather_all_validators from .fields import FieldInfo, ModelField from .main import BaseModel, create_model +from .typing import get_origin from .utils import lenient_issubclass _generic_types_cache: Dict[Tuple[Type[Any], Union[Any, Tuple[Any, ...]]], Type[BaseModel]] = {} @@ -37,7 +38,7 @@ def __class_getitem__(cls: Type[GenericModelT], params: Union[Type[Any], Tuple[T check_parameters_count(cls, params) typevars_map: Dict[TypeVarType, Type[Any]] = dict(zip(cls.__parameters__, params)) type_hints = get_type_hints(cls).items() - instance_type_hints = {k: v for k, v in type_hints if getattr(v, '__origin__', None) is not ClassVar} + instance_type_hints = {k: v for k, v in type_hints if get_origin(v) is not ClassVar} concrete_type_hints: Dict[str, Type[Any]] = { k: resolve_type_hint(v, typevars_map) for k, v in instance_type_hints.items() } @@ -79,7 +80,7 @@ def __concrete_name__(cls: Type[Any], params: Tuple[Type[Any], ...]) -> str: def resolve_type_hint(type_: Any, typevars_map: Dict[Any, Any]) -> Type[Any]: - if hasattr(type_, '__origin__') and getattr(type_, '__parameters__', None): + if get_origin(type_) and getattr(type_, '__parameters__', None): concrete_type_args = tuple([typevars_map[x] for x in type_.__parameters__]) return type_[concrete_type_args] return typevars_map.get(type_, type_) diff --git a/pydantic/main.py b/pydantic/main.py index c872f1e3b..87299b645 100644 --- a/pydantic/main.py +++ b/pydantic/main.py @@ -33,7 +33,7 @@ from .parse import Protocol, load_file, load_str_bytes from .schema import model_schema from .types import PyObject, StrBytes -from .typing import AnyCallable, ForwardRef, is_classvar, resolve_annotations, update_field_forward_refs +from .typing import AnyCallable, ForwardRef, get_origin, is_classvar, resolve_annotations, update_field_forward_refs from .utils import ( ClassAttribute, GetterDict, @@ -256,7 +256,7 @@ def __new__(mcs, name, bases, namespace, **kwargs): # noqa C901 if ( isinstance(value, untouched_types) and ann_type != PyObject - and not lenient_issubclass(getattr(ann_type, '__origin__', None), Type) + and not lenient_issubclass(get_origin(ann_type), Type) ): continue fields[ann_name] = inferred = ModelField.infer( diff --git a/pydantic/schema.py b/pydantic/schema.py index 27c66b2bd..4f6258ab1 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -55,7 +55,7 @@ conset, constr, ) -from .typing import ForwardRef, Literal, is_callable_type, is_literal_type, literal_values +from .typing import ForwardRef, Literal, get_args, get_origin, is_callable_type, is_literal_type, literal_values from .utils import get_model, lenient_issubclass, sequence_like if TYPE_CHECKING: @@ -803,9 +803,9 @@ def go(type_: Any) -> Type[Any]: or lenient_issubclass(type_, (ConstrainedList, ConstrainedSet)) ): return type_ - origin = getattr(type_, '__origin__', None) + origin = get_origin(type_) if origin is not None: - args: Tuple[Any, ...] = type_.__args__ + args: Tuple[Any, ...] = get_args(type_) if any(isinstance(a, ForwardRef) for a in args): # forward refs cause infinite recursion below return type_ diff --git a/pydantic/typing.py b/pydantic/typing.py index 070691eeb..729ebd71b 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -44,12 +44,19 @@ def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: return type_._eval_type(globalns, localns) -else: +elif sys.version_info < (3, 9): def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: return type_._evaluate(globalns, localns) +else: + + # TODO: remove the pragma: no cover once we can run CI on python 3.9 + def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: # pragma: no cover + return type_._evaluate(globalns, localns, set()) + + if sys.version_info < (3, 7): from typing import Callable as Callable @@ -70,8 +77,50 @@ def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: from typing_extensions import Literal except ImportError: Literal = None + + def get_args(t: Type[Any]) -> Tuple[Any, ...]: + return getattr(t, '__args__', ()) + + def get_origin(t: Type[Any]) -> Optional[Type[Any]]: + return getattr(t, '__origin__', None) + + else: - from typing import Literal + from typing import Literal, get_args as typing_get_args, get_origin as typing_get_origin + + def get_origin(tp: Type[Any]) -> Type[Any]: + return typing_get_origin(tp) or getattr(tp, '__origin__', None) + + def generic_get_args(tp: Type[Any]) -> Tuple[Any, ...]: + """ + In python 3.9, `typing.Dict`, `typing.List`, ... + do have an empty `__args__` by default (instead of the generic ~T for example). + In order to still support `Dict` for example and consider it as `Dict[Any, Any]`, + we retrieve the `_nparams` value that tells us how many parameters it needs. + """ + # TODO: remove the pragma: no cover once we can run CI on python 3.9 + if hasattr(tp, '_nparams'): # pragma: no cover + return (Any,) * tp._nparams + return () + + def get_args(tp: Type[Any]) -> Tuple[Any, ...]: + """Get type arguments with all substitutions performed. + + For unions, basic simplifications used by Union constructor are performed. + Examples:: + get_args(Dict[str, int]) == (str, int) + get_args(int) == () + get_args(Union[int, Union[T, int], str][int]) == (int, str) + get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int]) + get_args(Callable[[], T][int]) == ([], int) + """ + try: + args = typing_get_args(tp) + # TODO: remove the pragma: no cover once we can run CI on python 3.9 + except IndexError: # pragma: no cover + args = () + return args or getattr(tp, '__args__', ()) or generic_get_args(tp) + if TYPE_CHECKING: from .fields import ModelField @@ -115,6 +164,8 @@ def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any: 'CallableGenerator', 'ReprArgs', 'CallableGenerator', + 'get_args', + 'get_origin', ) @@ -167,16 +218,16 @@ def resolve_annotations(raw_annotations: Dict[str, Type[Any]], module_name: Opti def is_callable_type(type_: Type[Any]) -> bool: - return type_ is Callable or getattr(type_, '__origin__', None) is Callable + 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 getattr(type_, '__origin__', None) is Literal + return Literal is not None and get_origin(type_) is Literal def literal_values(type_: Type[Any]) -> Tuple[Any, ...]: - return type_.__args__ + return get_args(type_) else: @@ -217,12 +268,15 @@ def new_type_supertype(type_: Type[Any]) -> Type[Any]: return type_ -def _check_classvar(v: Type[Any]) -> bool: +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') def is_classvar(ann_type: Type[Any]) -> bool: - return _check_classvar(ann_type) or _check_classvar(getattr(ann_type, '__origin__', None)) + return _check_classvar(ann_type) or _check_classvar(get_origin(ann_type)) def update_field_forward_refs(field: 'ModelField', globalns: Any, localns: Any) -> None: @@ -243,13 +297,13 @@ def get_class(type_: Type[Any]) -> Union[None, bool, Type[Any]]: without brackets. Otherwise returns None. """ try: - origin = getattr(type_, '__origin__') + origin = get_origin(type_) if origin is None: # Python 3.6 origin = type_ if issubclass(origin, Type): # type: ignore - if type_.__args__ is None or not isinstance(type_.__args__[0], type): + if not get_args(type_) or not isinstance(get_args(type_)[0], type): return True - return type_.__args__[0] - except AttributeError: + return get_args(type_)[0] + except (AttributeError, TypeError): pass return None