Skip to content

Commit

Permalink
fix coverage and make typing-extensions a required dependency (#2368)
Browse files Browse the repository at this point in the history
* fixing coverage by simplifying Annotated import logic, fix #2367

* avoid checking against annotated if it's None

* make typing-extensions required WIP

* more making typing-extensions required

* fix docs and get_origin for python 3.6

* fix mypy test

* fix docs

* update docs, cleanup and add change

* clean docs/examples/schema_annotated.py

* move AnnotatedTypeNames
  • Loading branch information
samuelcolvin committed Feb 17, 2021
1 parent c33ced6 commit b7a8ef2
Show file tree
Hide file tree
Showing 24 changed files with 100 additions and 178 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: 1 addition & 0 deletions changes/2368-samuelcolvin.md
@@ -0,0 +1 @@
Making `typing-extensions` a required dependency.
2 changes: 1 addition & 1 deletion docs/examples/annotated_types_typed_dict.py
@@ -1,4 +1,4 @@
from typing import TypedDict
from typing_extensions import TypedDict

from pydantic import BaseModel, Extra, ValidationError

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/models_from_typeddict.py
@@ -1,4 +1,4 @@
from typing import TypedDict
from typing_extensions import TypedDict

from pydantic import ValidationError, create_model_from_typeddict

Expand Down
6 changes: 1 addition & 5 deletions docs/examples/schema_annotated.py
@@ -1,11 +1,7 @@
from uuid import uuid4

try:
from typing import Annotated
except ImportError:
from typing_extensions import Annotated

from pydantic import BaseModel, Field
from typing_extensions import Annotated


class Foo(BaseModel):
Expand Down
13 changes: 6 additions & 7 deletions docs/install.md
Expand Up @@ -4,7 +4,9 @@ Installation is as simple as:
pip install pydantic
```

*pydantic* has no required dependencies except python 3.6, 3.7, 3.8, or 3.9 (and the dataclasses package for python 3.6).
*pydantic* has no required dependencies except python 3.6, 3.7, 3.8, or 3.9,
[`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 is also available on [conda](https://www.anaconda.com) under the [conda-forge](https://conda-forge.org)
Expand All @@ -30,26 +32,23 @@ print('compiled:', pydantic.compiled)
*pydantic* has three optional dependencies:

* If you require email validation you can add [email-validator](https://github.com/JoshData/python-email-validator)
* use of `Literal` prior to python 3.8 relies on [typing-extensions](https://pypi.org/project/typing-extensions/)
* [dotenv file support](usage/settings.md#dotenv-env-support) with `Settings` requires
[python-dotenv](https://pypi.org/project/python-dotenv)

To install these along with *pydantic*:
```bash
pip install pydantic[email]
# or
pip install pydantic[typing_extensions]
# or
pip install pydantic[dotenv]
# or just
pip install pydantic[email,typing_extensions,dotenv]
pip install pydantic[email,dotenv]
```

Of course, you can also install these requirements manually with `pip install email-validator` and/or `pip install typing_extensions`.
Of course, you can also install these requirements manually with `pip install email-validator` and/or `pip install`.

And if you prefer to install *pydantic* directly from the repository:
```bash
pip install git+git://github.com/samuelcolvin/pydantic@master#egg=pydantic
# or with extras
pip install git+git://github.com/samuelcolvin/pydantic@master#egg=pydantic[email,typing_extensions]
pip install git+git://github.com/samuelcolvin/pydantic@master#egg=pydantic[email,dotenv]
```
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
28 changes: 10 additions & 18 deletions pydantic/annotated_types.py
Expand Up @@ -16,29 +16,21 @@ def create_model_from_typeddict(typeddict_cls: Type['TypedDict'], **kwargs: Any)
"""
Create a `BaseModel` based on the fields of a `TypedDict`.
Since `typing.TypedDict` in Python 3.8 does not store runtime information about optional keys,
we warn the user if that's the case (see https://bugs.python.org/issue38834).
we raise an error if this happens (see https://bugs.python.org/issue38834).
"""
field_definitions: Dict[str, Any]

# Best case scenario: with python 3.9+ or when `TypedDict` is imported from `typing_extensions`
if hasattr(typeddict_cls, '__required_keys__'):
field_definitions = {
field_name: (field_type, Required if field_name in typeddict_cls.__required_keys__ else None)
for field_name, field_type in typeddict_cls.__annotations__.items()
}
else:
import warnings

warnings.warn(
'You should use `typing_extensions.TypedDict` instead of `typing.TypedDict` for better support! '
'Without it, there is no way to differentiate required and optional fields when subclassed. '
'Fields will therefore be considered all required or all optional depending on class totality.',
UserWarning,
if not hasattr(typeddict_cls, '__required_keys__'):
raise TypeError(
'You should use `typing_extensions.TypedDict` instead of `typing.TypedDict`. '
'Without it, there is no way to differentiate required and optional fields when subclassed.'
)
default_value = Required if typeddict_cls.__total__ else None
field_definitions = {
field_name: (field_type, default_value) for field_name, field_type in typeddict_cls.__annotations__.items()
}

field_definitions = {
field_name: (field_type, Required if field_name in typeddict_cls.__required_keys__ else None)
for field_name, field_type in typeddict_cls.__annotations__.items()
}

return create_model(typeddict_cls.__name__, **kwargs, **field_definitions)

Expand Down
3 changes: 2 additions & 1 deletion 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
4 changes: 2 additions & 2 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
131 changes: 51 additions & 80 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,87 +83,21 @@ 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:
# Create mock Annotated values distinct from `None`, which is a valid `get_origin`
# return value.
class _FalseMeta(type):
# Allow short circuiting with "Annotated[...] if Annotated else None".
def __bool__(cls):
return False

# Give a nice suggestion for unguarded use
def __getitem__(cls, key):
raise RuntimeError(
'Annotated is not supported in this python version, please `pip install typing-extensions`.'
)

class Annotated(metaclass=_FalseMeta):
pass


# 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):

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 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__', ())
if sys.version_info < (3, 8):

def get_origin(t: Type[Any]) -> Optional[Type[Any]]:
if Annotated and type(t).__name__ in AnnotatedTypeNames:
if type(t).__name__ in AnnotatedTypeNames:
return cast(Type[Any], Annotated) # mypy complains about _SpecialForm in py3.6
return getattr(t, '__origin__', None)


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

def get_origin(tp: Type[Any]) -> Type[Any]:
"""
Expand All @@ -170,11 +106,50 @@ def get_origin(tp: Type[Any]) -> Type[Any]:
It should be useless once https://github.com/cython/cython/issues/3537 is
solved and https://github.com/samuelcolvin/pydantic/pull/1753 is merged.
"""
if Annotated and type(tp).__name__ in AnnotatedTypeNames:
if type(tp).__name__ in AnnotatedTypeNames:
return cast(Type[Any], Annotated) # mypy complains about _SpecialForm
return typing_get_origin(tp) or getattr(tp, '__origin__', None)
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
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 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__', ())


else:
from typing import get_args as _typing_get_args

def generic_get_args(tp: Type[Any]) -> Tuple[Any, ...]:
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).
Expand All @@ -196,10 +171,10 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]:
get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
get_args(Callable[[], T][int]) == ([], int)
"""
if Annotated and type(tp).__name__ in AnnotatedTypeNames:
if type(tp).__name__ in AnnotatedTypeNames:
return tp.__args__ + tp.__metadata__
# the fallback is needed for the same reasons as `get_origin` (see above)
return typing_get_args(tp) or getattr(tp, '__args__', ()) or generic_get_args(tp)
return _typing_get_args(tp) or getattr(tp, '__args__', ()) or _generic_get_args(tp)


if TYPE_CHECKING:
Expand All @@ -220,7 +195,6 @@ def get_args(tp: Type[Any]) -> Tuple[Any, ...]:
__all__ = (
'ForwardRef',
'Callable',
'Annotated',
'AnyCallable',
'NoArgAnyCallable',
'NoneType',
Expand All @@ -230,7 +204,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 @@ -255,9 +228,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
3 changes: 2 additions & 1 deletion pydantic/validators.py
Expand Up @@ -25,13 +25,14 @@
)
from uuid import UUID

from typing_extensions import Literal

from . import errors
from .datetime_parse import parse_date, parse_datetime, parse_duration, parse_time
from .typing import (
NONE_TYPES,
AnyCallable,
ForwardRef,
Literal,
all_literal_values,
display_as_type,
get_class,
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

0 comments on commit b7a8ef2

Please sign in to comment.