From ba3f55440f9a3d0a22088562af2dafe6d269db9f Mon Sep 17 00:00:00 2001 From: Davis Kirkendall Date: Sat, 13 Mar 2021 18:45:44 +0100 Subject: [PATCH 1/9] Allow collections.abc.Callable to be used as type in python 3.9 --- pydantic/validators.py | 9 ++++++--- tests/test_callable.py | 11 +++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pydantic/validators.py b/pydantic/validators.py index 57a1a23fcc..f2ad2eb46c 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -656,9 +656,12 @@ def find_validators( # noqa: C901 (ignore complexity) type_type = type_.__class__ if type_type == ForwardRef or type_type == TypeVar: return - if type_ in NONE_TYPES: - yield none_validator - return + try: + if type_ in NONE_TYPES: + yield none_validator + return + except TypeError: + pass # in case unhashable types are in the type if type_ is Pattern: yield pattern_validator return diff --git a/tests/test_callable.py b/tests/test_callable.py index 037b692e19..cf31241a22 100644 --- a/tests/test_callable.py +++ b/tests/test_callable.py @@ -1,11 +1,18 @@ +import sys from typing import Callable import pytest from pydantic import BaseModel, ValidationError +collection_callable_types = [Callable, Callable[[int], int]] +if sys.version_info >= (3, 9): + from collections.abc import Callable as CollectionsCallable -@pytest.mark.parametrize('annotation', [Callable, Callable[[int], int]]) + collection_callable_types += [CollectionsCallable, CollectionsCallable[[int], int]] + + +@pytest.mark.parametrize('annotation', collection_callable_types) def test_callable(annotation): class Model(BaseModel): callback: annotation @@ -14,7 +21,7 @@ class Model(BaseModel): assert callable(m.callback) -@pytest.mark.parametrize('annotation', [Callable, Callable[[int], int]]) +@pytest.mark.parametrize('annotation', collection_callable_types) def test_non_callable(annotation): class Model(BaseModel): callback: annotation From 7573c094f8171a087a0054ac0991cf5a41d88644 Mon Sep 17 00:00:00 2001 From: Davis Kirkendall Date: Tue, 16 Mar 2021 08:52:03 +0100 Subject: [PATCH 2/9] Add is_none_type as function to check none types by identity --- pydantic/fields.py | 4 ++-- pydantic/schema.py | 4 ++-- pydantic/typing.py | 12 +++++++++++- pydantic/validators.py | 12 +++++------- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/pydantic/fields.py b/pydantic/fields.py index dc79dbfa7c..272a166f10 100644 --- a/pydantic/fields.py +++ b/pydantic/fields.py @@ -30,7 +30,6 @@ from .errors import ConfigError, NoneIsNotAllowedError from .types import Json, JsonWrapper from .typing import ( - NONE_TYPES, Callable, ForwardRef, NoArgAnyCallable, @@ -40,6 +39,7 @@ get_origin, is_literal_type, is_new_type, + is_none_type, is_typeddict, new_type_supertype, ) @@ -737,7 +737,7 @@ def validate( return v, errors if v is None: - if self.type_ in NONE_TYPES: + if is_none_type(self.type_): # keep validating pass elif self.allow_none: diff --git a/pydantic/schema.py b/pydantic/schema.py index 3a9bc4c59b..f033ed2440 100644 --- a/pydantic/schema.py +++ b/pydantic/schema.py @@ -63,7 +63,6 @@ constr, ) from .typing import ( - NONE_TYPES, ForwardRef, all_literal_values, get_args, @@ -71,6 +70,7 @@ is_callable_type, is_literal_type, is_namedtuple, + is_none_type, ) from .utils import ROOT_KEY, get_model, lenient_issubclass, sequence_like @@ -787,7 +787,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity) ) if field_type is Any or field_type.__class__ == TypeVar: return {}, definitions, nested_models # no restrictions - if field_type in NONE_TYPES: + if is_none_type(field_type): return {'type': 'null'}, definitions, nested_models if is_callable_type(field_type): raise SkipField(f'Callable {field.name} was excluded from schema since JSON schema has no equivalent type.') diff --git a/pydantic/typing.py b/pydantic/typing.py index 1a3bf43de4..27f7e47260 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -211,6 +211,7 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]: 'NoArgAnyCallable', 'NoneType', 'NONE_TYPES', + 'is_none_type', 'display_as_type', 'resolve_annotations', 'is_callable_type', @@ -242,7 +243,16 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]: NoneType = None.__class__ -NONE_TYPES: Set[Any] = {None, NoneType, Literal[None]} + + +NONE_TYPES: Tuple[Any, ...] = (None, NoneType, Literal[None]) + + +def is_none_type(type_: Any) -> bool: + for none_type in NONE_TYPES: + if type_ is none_type: + return True + return False def display_as_type(v: Type[Any]) -> str: diff --git a/pydantic/validators.py b/pydantic/validators.py index f2ad2eb46c..f3a010e2af 100644 --- a/pydantic/validators.py +++ b/pydantic/validators.py @@ -30,7 +30,6 @@ from . import errors from .datetime_parse import parse_date, parse_datetime, parse_duration, parse_time from .typing import ( - NONE_TYPES, AnyCallable, ForwardRef, all_literal_values, @@ -39,6 +38,7 @@ is_callable_type, is_literal_type, is_namedtuple, + is_none_type, is_typeddict, ) from .utils import almost_equal_floats, lenient_issubclass, sequence_like @@ -656,12 +656,10 @@ def find_validators( # noqa: C901 (ignore complexity) type_type = type_.__class__ if type_type == ForwardRef or type_type == TypeVar: return - try: - if type_ in NONE_TYPES: - yield none_validator - return - except TypeError: - pass # in case unhashable types are in the type + + if is_none_type(type_): + yield none_validator + return if type_ is Pattern: yield pattern_validator return From 8808da0ea643346c0c27d7863bb75b7df6309477 Mon Sep 17 00:00:00 2001 From: Davis Kirkendall Date: Tue, 16 Mar 2021 09:23:08 +0100 Subject: [PATCH 3/9] Modify `typing.is_none_type` to work in python 3.6 and 3.7 --- pydantic/typing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pydantic/typing.py b/pydantic/typing.py index 27f7e47260..1c8e1700da 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -245,14 +245,14 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]: NoneType = None.__class__ -NONE_TYPES: Tuple[Any, ...] = (None, NoneType, Literal[None]) +NONE_TYPES: Set[Any] = {None, NoneType, Literal[None]} def is_none_type(type_: Any) -> bool: - for none_type in NONE_TYPES: - if type_ is none_type: - return True - return False + try: + return type_ in NONE_TYPES + except TypeError: + return False def display_as_type(v: Type[Any]) -> str: From 6b3562d55a1b39439f6cae3f84646f08a5b2270f Mon Sep 17 00:00:00 2001 From: Davis Kirkendall Date: Sun, 9 May 2021 20:31:39 +0200 Subject: [PATCH 4/9] Add tests for none types in typing.py * Apply review comments on #2519 * Add different implementations depending on python version * Add tests for is_none_type --- pydantic/typing.py | 22 ++++++++++++++++------ tests/test_typing.py | 16 ++++++++++++++-- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/pydantic/typing.py b/pydantic/typing.py index 1c8e1700da..0932e7c4c6 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -210,7 +210,6 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]: 'AnyCallable', 'NoArgAnyCallable', 'NoneType', - 'NONE_TYPES', 'is_none_type', 'display_as_type', 'resolve_annotations', @@ -245,13 +244,24 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]: NoneType = None.__class__ -NONE_TYPES: Set[Any] = {None, NoneType, Literal[None]} +NONE_TYPES: Tuple[Any, Any, Any] = (None, NoneType, Literal[None]) -def is_none_type(type_: Any) -> bool: - try: - return type_ in NONE_TYPES - except TypeError: +if sys.version_info < (3, 8): # noqa: C901 (ignore complexity) + + def is_none_type(type_: Any) -> bool: + try: + return type_ in NONE_TYPES + except TypeError: + return False + + +else: + + def is_none_type(type_: Any) -> bool: + for none_type in NONE_TYPES: + if type_ is none_type: + return True return False diff --git a/tests/test_typing.py b/tests/test_typing.py index d0d99125e0..dfffe0956f 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -1,9 +1,9 @@ from collections import namedtuple -from typing import NamedTuple +from typing import Callable as TypingCallable, NamedTuple import pytest -from pydantic.typing import is_namedtuple, is_typeddict +from pydantic.typing import Literal, is_namedtuple, is_none_type, is_typeddict try: from typing import TypedDict as typing_TypedDict @@ -54,3 +54,15 @@ class Other(dict): id: int assert is_typeddict(Other) is False + + +def test_is_none_type(): + assert is_none_type(Literal[None]) is True + assert is_none_type(None) is True + assert is_none_type(type(None)) is True + assert is_none_type(6) is False + assert is_none_type({}) is False + # WARNING: It's important to test typing.Callable not + # collections.abc.Callable (event with python >= 3.9) as they behave + # differently + assert is_none_type(TypingCallable) is False From 829d12cd40eb2fde12e332129c20551ae77c7dce Mon Sep 17 00:00:00 2001 From: Davis Kirkendall Date: Sun, 9 May 2021 20:46:12 +0200 Subject: [PATCH 5/9] Add change entry --- changes/2519-daviskirk.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2519-daviskirk.md diff --git a/changes/2519-daviskirk.md b/changes/2519-daviskirk.md new file mode 100644 index 0000000000..419646e3bf --- /dev/null +++ b/changes/2519-daviskirk.md @@ -0,0 +1 @@ +Allow `collections.abc.Callable` to be used as type in python 3.9. From 173b418f677d03cc76ed41f40d2fe5077659f9e0 Mon Sep 17 00:00:00 2001 From: Davis Kirkendall Date: Sun, 9 May 2021 20:51:46 +0200 Subject: [PATCH 6/9] Fix field info repr --- tests/test_main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 5709735371..2557433d20 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1865,9 +1865,9 @@ class Model(BaseModel): m = Model(a=1, b=2, c=3) assert repr(m) == 'Model(a=1, b=2)' - assert repr(m.__fields__['a'].field_info) == 'FieldInfo(default=Ellipsis, extra={})' - assert repr(m.__fields__['b'].field_info) == 'FieldInfo(default=Ellipsis, extra={})' - assert repr(m.__fields__['c'].field_info) == 'FieldInfo(default=Ellipsis, repr=False, extra={})' + assert repr(m.__fields__['a'].field_info) == 'FieldInfo(default=PydanticUndefined, extra={})' + assert repr(m.__fields__['b'].field_info) == 'FieldInfo(default=PydanticUndefined, extra={})' + assert repr(m.__fields__['c'].field_info) == 'FieldInfo(default=PydanticUndefined, repr=False, extra={})' def test_inherited_model_field_copy(): From 5ce721b681096406e6741383506da80a7c95beb4 Mon Sep 17 00:00:00 2001 From: Davis Kirkendall Date: Sun, 9 May 2021 21:09:01 +0200 Subject: [PATCH 7/9] Remove unneeded try/except for python < 3.8 --- pydantic/typing.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pydantic/typing.py b/pydantic/typing.py index 0932e7c4c6..30086744d3 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -250,10 +250,7 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]: if sys.version_info < (3, 8): # noqa: C901 (ignore complexity) def is_none_type(type_: Any) -> bool: - try: - return type_ in NONE_TYPES - except TypeError: - return False + return type_ in NONE_TYPES else: From 117d1c21c74a6edadb42f56e2404cc64b8ee278f Mon Sep 17 00:00:00 2001 From: Davis Kirkendall Date: Mon, 10 May 2021 09:38:46 +0200 Subject: [PATCH 8/9] Add comment explaining alternative is_none_type implementation --- pydantic/typing.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pydantic/typing.py b/pydantic/typing.py index 30086744d3..a3ce75d7cf 100644 --- a/pydantic/typing.py +++ b/pydantic/typing.py @@ -248,6 +248,11 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]: if sys.version_info < (3, 8): # noqa: C901 (ignore complexity) + # 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. def is_none_type(type_: Any) -> bool: return type_ in NONE_TYPES From 069e5b5eff40acb510ba51860a798972b9000f6a Mon Sep 17 00:00:00 2001 From: PrettyWood Date: Sat, 4 Sep 2021 00:12:43 +0200 Subject: [PATCH 9/9] fix: typo --- tests/test_typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index dfffe0956f..af0b8d9695 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -62,7 +62,7 @@ def test_is_none_type(): assert is_none_type(type(None)) is True assert is_none_type(6) is False assert is_none_type({}) is False - # WARNING: It's important to test typing.Callable not - # collections.abc.Callable (event with python >= 3.9) as they behave + # WARNING: It's important to test `typing.Callable` not + # `collections.abc.Callable` (even with python >= 3.9) as they behave # differently assert is_none_type(TypingCallable) is False