Skip to content

Commit

Permalink
make typing-extensions required WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelcolvin committed Feb 17, 2021
1 parent bc17abc commit 281450b
Show file tree
Hide file tree
Showing 13 changed files with 52 additions and 85 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Expand Up @@ -81,7 +81,7 @@ jobs:
DEPS: yes

- name: uninstall deps
run: pip uninstall -y cython email-validator typing-extensions devtools python-dotenv
run: pip uninstall -y cython email-validator devtools python-dotenv

- name: test compiled without deps
run: make test
Expand Down
1 change: 0 additions & 1 deletion docs/requirements.txt
Expand Up @@ -7,6 +7,5 @@ mkdocs-exclude==1.0.2
mkdocs-material==6.2.8
markdown-include==0.6.0
sqlalchemy
typing-extensions==3.7.4.3
orjson
ujson
7 changes: 4 additions & 3 deletions pydantic/fields.py
Expand Up @@ -22,14 +22,15 @@
Union,
)

from typing_extensions import Annotated

from . import errors as errors_
from .class_validators import Validator, make_generic_validator, prep_validators
from .error_wrappers import ErrorWrapper
from .errors import ConfigError, NoneIsNotAllowedError
from .types import Json, JsonWrapper
from .typing import (
NONE_TYPES,
Annotated,
Callable,
ForwardRef,
NoArgAnyCallable,
Expand Down Expand Up @@ -343,7 +344,7 @@ def _get_field_info(
field_info_from_config = config.get_field_info(field_name)

field_info = None
if Annotated and get_origin(annotation) is Annotated:
if get_origin(annotation) is Annotated:
field_infos = [arg for arg in get_args(annotation)[1:] if isinstance(arg, FieldInfo)]
if len(field_infos) > 1:
raise ValueError(f'cannot specify multiple `Annotated` `Field`s for {field_name!r}')
Expand Down Expand Up @@ -497,7 +498,7 @@ def _type_analysis(self) -> None: # noqa: C901 (ignore complexity)
if isinstance(self.type_, type) and isinstance(None, self.type_):
self.allow_none = True
return
if Annotated and origin is Annotated:
if origin is Annotated:
self.type_ = get_args(self.type_)[0]
self._type_analysis()
return
Expand Down
4 changes: 2 additions & 2 deletions pydantic/main.py
Expand Up @@ -61,7 +61,7 @@
if TYPE_CHECKING:
from inspect import Signature

import typing_extensions
from typing_extensions import Protocol

from .class_validators import ValidatorListDict
from .types import ModelOrDc
Expand All @@ -79,7 +79,7 @@
ConfigType = Type['BaseConfig']
Model = TypeVar('Model', bound='BaseModel')

class SchemaExtraCallable(typing_extensions.Protocol):
class SchemaExtraCallable(Protocol):
@overload
def __call__(self, schema: Dict[str, Any]) -> None:
pass
Expand Down
6 changes: 3 additions & 3 deletions pydantic/schema.py
Expand Up @@ -26,6 +26,8 @@
)
from uuid import UUID

from typing_extensions import Annotated, Literal

from .fields import (
SHAPE_FROZENSET,
SHAPE_GENERIC,
Expand Down Expand Up @@ -61,9 +63,7 @@
)
from .typing import (
NONE_TYPES,
Annotated,
ForwardRef,
Literal,
get_args,
get_origin,
is_callable_type,
Expand Down Expand Up @@ -944,7 +944,7 @@ def go(type_: Any) -> Type[Any]:
# forward refs cause infinite recursion below
return type_

if Annotated and origin is Annotated:
if origin is Annotated:
return go(args[0])
if origin is Union:
return Union[tuple(go(a) for a in args)] # type: ignore
Expand Down
88 changes: 33 additions & 55 deletions pydantic/typing.py
Expand Up @@ -20,6 +20,8 @@
cast,
)

from typing_extensions import Annotated, Literal

try:
from typing import _TypingBase as typing_base # type: ignore
except ImportError:
Expand Down Expand Up @@ -81,64 +83,44 @@ def evaluate_forwardref(type_: ForwardRef, globalns: Any, localns: Any) -> Any:
NoArgAnyCallable = TypingCallable[[], Any]


if sys.version_info >= (3, 9):
from typing import Annotated
else:
if TYPE_CHECKING:
from typing_extensions import Annotated
else: # due to different mypy warnings raised during CI for python 3.7 and 3.8
try:
from typing_extensions import Annotated
except ImportError:
Annotated = None


# Annotated[...] is implemented by returning an instance of one of these classes, depending on
# python/typing_extensions version.
AnnotatedTypeNames = ('AnnotatedMeta', '_AnnotatedAlias')
AnnotatedTypeNames = {'AnnotatedMeta', '_AnnotatedAlias'}


if sys.version_info < (3, 8): # noqa: C901
if TYPE_CHECKING:
from typing_extensions import Literal
else: # due to different mypy warnings raised during CI for python 3.7 and 3.8
try:
from typing_extensions import Literal
except ImportError:
Literal = None
if sys.version_info < (3, 7):

if sys.version_info < (3, 7):
def get_args(t: Type[Any]) -> Tuple[Any, ...]:
"""Simplest get_args compatibility layer possible.
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).
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 Annotated and type(t).__name__ in AnnotatedTypeNames:
return t.__args__ + t.__metadata__
return getattr(t, '__args__', ())

"""
if Annotated and type(t).__name__ in AnnotatedTypeNames:
return t.__args__ + t.__metadata__
return getattr(t, '__args__', ())

else:
from typing import _GenericAlias

def get_args(t: Type[Any]) -> Tuple[Any, ...]:
"""Compatibility version of get_args for python 3.7.
Mostly compatible with the python 3.8 `typing` module version
and able to handle almost all use cases.
"""
if Annotated and type(t).__name__ in AnnotatedTypeNames:
return t.__args__ + t.__metadata__
if isinstance(t, _GenericAlias):
res = t.__args__
if t.__origin__ is Callable and res and res[0] is not Ellipsis:
res = (list(res[:-1]), res[-1])
return res
return getattr(t, '__args__', ())
elif sys.version_info < (3, 8): # noqa: C901
from typing import _GenericAlias

def get_args(t: Type[Any]) -> Tuple[Any, ...]:
"""Compatibility version of get_args for python 3.7.
Mostly compatible with the python 3.8 `typing` module version
and able to handle almost all use cases.
"""
if Annotated and type(t).__name__ in AnnotatedTypeNames:
return t.__args__ + t.__metadata__
if isinstance(t, _GenericAlias):
res = t.__args__
if t.__origin__ is Callable and res and res[0] is not Ellipsis:
res = (list(res[:-1]), res[-1])
return res
return getattr(t, '__args__', ())

def get_origin(t: Type[Any]) -> Optional[Type[Any]]:
if Annotated and type(t).__name__ in AnnotatedTypeNames:
Expand All @@ -147,7 +129,7 @@ def get_origin(t: Type[Any]) -> Optional[Type[Any]]:


else:
from typing import Literal, get_args as typing_get_args, get_origin as typing_get_origin
from typing import get_args as typing_get_args, get_origin as typing_get_origin

def get_origin(tp: Type[Any]) -> Type[Any]:
"""
Expand Down Expand Up @@ -206,7 +188,6 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]:
__all__ = (
'ForwardRef',
'Callable',
'Annotated',
'AnyCallable',
'NoArgAnyCallable',
'NoneType',
Expand All @@ -216,7 +197,6 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]:
'is_callable_type',
'is_literal_type',
'literal_values',
'Literal',
'is_namedtuple',
'is_typeddict',
'is_new_type',
Expand All @@ -241,9 +221,7 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]:


NoneType = None.__class__
NONE_TYPES: Set[Any] = {None, NoneType}
if Literal:
NONE_TYPES.add(Literal[None])
NONE_TYPES: Set[Any] = {None, NoneType, Literal[None]}


def display_as_type(v: Type[Any]) -> str:
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Expand Up @@ -4,5 +4,5 @@ Cython==0.29.21;sys_platform!='win32'
devtools==0.6.1
email-validator==1.1.2
dataclasses==0.6; python_version < '3.7'
typing-extensions==3.7.4.3; python_version < '3.9'
typing-extensions==3.7.4.3
python-dotenv==0.15.0
4 changes: 2 additions & 2 deletions setup.py
Expand Up @@ -127,11 +127,11 @@ def extra(self):
python_requires='>=3.6.1',
zip_safe=False, # https://mypy.readthedocs.io/en/latest/installed_packages.html
install_requires=[
'dataclasses>=0.6;python_version<"3.7"'
'dataclasses>=0.6;python_version<"3.7"',
'typing-extensions>=3.7.4.3'
],
extras_require={
'email': ['email-validator>=1.0.3'],
'typing_extensions': ['typing-extensions>=3.7.4'],
'dotenv': ['python-dotenv>=0.10.4'],
},
ext_modules=ext_modules,
Expand Down
7 changes: 1 addition & 6 deletions tests/mypy/test_mypy.py
Expand Up @@ -10,11 +10,6 @@
except ImportError:
mypy_api = None

try:
import typing_extensions
except ImportError:
typing_extensions = None

# This ensures mypy can find the test files, no matter where tests are run from:
os.chdir(Path(__file__).parent.parent.parent)

Expand All @@ -33,7 +28,7 @@
executable_modules = list({fname[:-3] for _, fname, out_fname in cases if out_fname is None})


@pytest.mark.skipif(not (typing_extensions and mypy_api), reason='typing_extensions or mypy are not installed')
@pytest.mark.skipif(not mypy_api, reason='mypy is not installed')
@pytest.mark.parametrize('config_filename,python_filename,output_filename', cases)
def test_mypy_results(config_filename, python_filename, output_filename):
full_config_filename = f'tests/mypy/configs/{config_filename}'
Expand Down
3 changes: 1 addition & 2 deletions tests/test_forward_ref.py
Expand Up @@ -2,9 +2,9 @@
from typing import Optional, Tuple

import pytest
from typing_extensions import Literal

from pydantic import BaseModel, ConfigError, ValidationError
from pydantic.typing import Literal

skip_pre_37 = pytest.mark.skipif(sys.version_info < (3, 7), reason='testing >= 3.7 behaviour only')

Expand Down Expand Up @@ -482,7 +482,6 @@ def module():


@skip_pre_37
@pytest.mark.skipif(not Literal, reason='typing_extensions not installed')
def test_resolve_forward_ref_dataclass(create_module):
module = create_module(
# language=Python
Expand Down
1 change: 0 additions & 1 deletion tests/test_main.py
Expand Up @@ -703,7 +703,6 @@ class Config:
assert m.foo == 'foo'


@pytest.mark.skipif(not Literal, reason='typing_extensions not installed')
def test_literal_enum_values():
FooEnum = Enum('FooEnum', {'foo': 'foo_value', 'bar': 'bar_value'})

Expand Down
3 changes: 1 addition & 2 deletions tests/test_schema.py
Expand Up @@ -25,6 +25,7 @@
from uuid import UUID

import pytest
from typing_extensions import Literal

from pydantic import BaseModel, Extra, Field, ValidationError, conlist, conset, validator
from pydantic.color import Color
Expand Down Expand Up @@ -74,7 +75,6 @@
conint,
constr,
)
from pydantic.typing import Literal

try:
import email_validator
Expand Down Expand Up @@ -1754,7 +1754,6 @@ class Model(BaseModel):
}


@pytest.mark.skipif(not Literal, reason='typing_extensions not installed and python version < 3.8')
def test_literal_schema():
class Model(BaseModel):
a: Literal[1]
Expand Down
9 changes: 3 additions & 6 deletions tests/test_types.py
Expand Up @@ -26,6 +26,7 @@
from uuid import UUID

import pytest
from typing_extensions import Literal

from pydantic import (
UUID1,
Expand Down Expand Up @@ -69,7 +70,7 @@
errors,
validator,
)
from pydantic.typing import Literal, NoneType
from pydantic.typing import NoneType

try:
import email_validator
Expand Down Expand Up @@ -2508,7 +2509,6 @@ class Model(BaseModel):
]


@pytest.mark.skipif(not Literal, reason='typing_extensions not installed')
def test_literal_single():
class Model(BaseModel):
a: Literal['a']
Expand All @@ -2526,7 +2526,6 @@ class Model(BaseModel):
]


@pytest.mark.skipif(not Literal, reason='typing_extensions not installed')
def test_literal_multiple():
class Model(BaseModel):
a_or_b: Literal['a', 'b']
Expand Down Expand Up @@ -2723,9 +2722,7 @@ class Model(BaseModel):
assert Model(v=deque((1, 2, 3))).json() == '{"v": [1, 2, 3]}'


none_value_type_cases = (None, type(None), NoneType)
if Literal:
none_value_type_cases += (Literal[None],)
none_value_type_cases = None, type(None), NoneType, Literal[None]


@pytest.mark.parametrize('value_type', none_value_type_cases)
Expand Down

0 comments on commit 281450b

Please sign in to comment.