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

fix coverage and make typing-extensions a required dependency #2368

Merged
merged 10 commits into from Feb 17, 2021
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
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change should probably be explicitly noted as a breaking change in the changelog, just in case there are people ignoring the warning.

I can also think of scenarios where people are using another libraries TypedDict, but I'm not sure if that's common or not (I've never done this before).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need it's in the same new version v1.8 ;)

'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
139 changes: 55 additions & 84 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,16 @@ 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')


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 +101,55 @@ 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)


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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be moved up since it's referenced in the branches above?



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