Skip to content

Make Python versions known statically #382

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

Merged
merged 1 commit into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
307 changes: 144 additions & 163 deletions src/cattrs/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,25 +30,10 @@
except ImportError:
ExtensionsTypedDictMeta = None

__all__ = [
"ExtensionsTypedDict",
"is_py37",
"is_py38",
"is_py39_plus",
"is_py310_plus",
"is_py311_plus",
"is_typeddict",
"TypedDict",
]

version_info = sys.version_info[0:3]
is_py37 = version_info[:2] == (3, 7)
is_py38 = version_info[:2] == (3, 8)
is_py39_plus = version_info[:2] >= (3, 9)
is_py310_plus = version_info[:2] >= (3, 10)
is_py311_plus = version_info[:2] >= (3, 11)

if is_py37:
if sys.version_info >= (3, 8):
from typing import Final, Protocol, get_args, get_origin

else:

def get_args(cl):
return cl.__args__
Expand All @@ -58,10 +43,7 @@ def get_origin(cl):

from typing_extensions import Final, Protocol

else:
from typing import Final, Protocol, get_args, get_origin

if is_py311_plus:
if sys.version_info >= (3, 11):
ExceptionGroup = ExceptionGroup
else:
from exceptiongroup import ExceptionGroup as ExceptionGroup # noqa: PLC0414
Expand Down Expand Up @@ -157,144 +139,7 @@ def get_final_base(type) -> Optional[type]:
OriginAbstractSet = AbcSet
OriginMutableSet = AbcMutableSet

if is_py37 or is_py38:
Set = TypingSet
AbstractSet = TypingAbstractSet
MutableSet = TypingMutableSet

Sequence = TypingSequence
MutableSequence = TypingMutableSequence
MutableMapping = TypingMutableMapping
Mapping = TypingMapping
FrozenSetSubscriptable = FrozenSet
TupleSubscriptable = Tuple

from collections import Counter as ColCounter
from typing import Counter, Union, _GenericAlias

from typing_extensions import Annotated, NotRequired, Required
from typing_extensions import get_origin as te_get_origin

if is_py38:
from typing import TypedDict, _TypedDictMeta
else:
_TypedDictMeta = None
TypedDict = ExtensionsTypedDict

def is_annotated(type) -> bool:
return te_get_origin(type) is Annotated

def is_tuple(type):
return type in (Tuple, tuple) or (
type.__class__ is _GenericAlias and issubclass(type.__origin__, Tuple)
)

def is_union_type(obj):
return (
obj is Union or isinstance(obj, _GenericAlias) and obj.__origin__ is Union
)

def get_newtype_base(typ: Any) -> Optional[type]:
supertype = getattr(typ, "__supertype__", None)
if (
supertype is not None
and getattr(typ, "__qualname__", "") == "NewType.<locals>.new_type"
and typ.__module__ in ("typing", "typing_extensions")
):
return supertype
return None

def is_sequence(type: Any) -> bool:
return type in (List, list, Tuple, tuple) or (
type.__class__ is _GenericAlias
and (
type.__origin__ not in (Union, Tuple, tuple)
and issubclass(type.__origin__, TypingSequence)
)
or (type.__origin__ in (Tuple, tuple) and type.__args__[1] is ...)
)

def is_deque(type: Any) -> bool:
return (
type in (deque, Deque)
or (type.__class__ is _GenericAlias and issubclass(type.__origin__, deque))
or type.__origin__ is deque
)

def is_mutable_set(type):
return type is set or (
type.__class__ is _GenericAlias and issubclass(type.__origin__, MutableSet)
)

def is_frozenset(type):
return type is frozenset or (
type.__class__ is _GenericAlias and issubclass(type.__origin__, FrozenSet)
)

def is_mapping(type):
return type in (TypingMapping, dict) or (
type.__class__ is _GenericAlias
and issubclass(type.__origin__, TypingMapping)
)

bare_generic_args = {
List.__args__,
TypingSequence.__args__,
TypingMapping.__args__,
Dict.__args__,
TypingMutableSequence.__args__,
Tuple.__args__,
None, # non-parametrized containers do not have `__args__ attribute in py3.7-8
}

def is_bare(type):
return getattr(type, "__args__", None) in bare_generic_args

def is_counter(type):
return (
type in (Counter, ColCounter)
or getattr(type, "__origin__", None) is ColCounter
)

if is_py38:
from typing import Literal

def is_literal(type) -> bool:
return type.__class__ is _GenericAlias and type.__origin__ is Literal

else:
# No literals in 3.7.
def is_literal(_) -> bool:
return False

def is_generic(obj):
return isinstance(obj, _GenericAlias)

def copy_with(type, args):
"""Replace a generic type's arguments."""
return type.copy_with(args)

def is_typeddict(cls) -> bool:
return (
cls.__class__ is _TypedDictMeta
or (is_generic(cls) and (cls.__origin__.__class__ is _TypedDictMeta))
or (
ExtensionsTypedDictMeta is not None
and cls.__class__ is ExtensionsTypedDictMeta
or (
is_generic(cls)
and (cls.__origin__.__class__ is ExtensionsTypedDictMeta)
)
)
)

def get_notrequired_base(type) -> "Union[Any, Literal[NOTHING]]":
if get_origin(type) in (NotRequired, Required):
return get_args(type)[0]
return NOTHING

else:
# 3.9+
if sys.version_info >= (3, 9):
from collections import Counter
from collections.abc import Mapping as AbcMapping
from collections.abc import MutableMapping as AbcMutableMapping
Expand Down Expand Up @@ -348,7 +193,7 @@ def is_tuple(type):
or (getattr(type, "__origin__", None) is tuple)
)

if is_py310_plus:
if sys.version_info >= (3, 10):

def is_union_type(obj):
from types import UnionType
Expand All @@ -364,7 +209,7 @@ def get_newtype_base(typ: Any) -> Optional[type]:
return typ.__supertype__
return None

if is_py311_plus:
if sys.version_info >= (3, 11):
from typing import NotRequired, Required
else:
from typing_extensions import NotRequired, Required
Expand Down Expand Up @@ -500,6 +345,142 @@ def copy_with(type, args):
return Annotated[args]
return type.__origin__[args]

else:
Set = TypingSet
AbstractSet = TypingAbstractSet
MutableSet = TypingMutableSet

Sequence = TypingSequence
MutableSequence = TypingMutableSequence
MutableMapping = TypingMutableMapping
Mapping = TypingMapping
FrozenSetSubscriptable = FrozenSet
TupleSubscriptable = Tuple

from collections import Counter as ColCounter
from typing import Counter, Union, _GenericAlias

from typing_extensions import Annotated, NotRequired, Required
from typing_extensions import get_origin as te_get_origin

if sys.version_info >= (3, 8):
from typing import TypedDict, _TypedDictMeta
else:
_TypedDictMeta = None
TypedDict = ExtensionsTypedDict

def is_annotated(type) -> bool:
return te_get_origin(type) is Annotated

def is_tuple(type):
return type in (Tuple, tuple) or (
type.__class__ is _GenericAlias and issubclass(type.__origin__, Tuple)
)

def is_union_type(obj):
return (
obj is Union or isinstance(obj, _GenericAlias) and obj.__origin__ is Union
)

def get_newtype_base(typ: Any) -> Optional[type]:
supertype = getattr(typ, "__supertype__", None)
if (
supertype is not None
and getattr(typ, "__qualname__", "") == "NewType.<locals>.new_type"
and typ.__module__ in ("typing", "typing_extensions")
):
return supertype
return None

def is_sequence(type: Any) -> bool:
return type in (List, list, Tuple, tuple) or (
type.__class__ is _GenericAlias
and (
type.__origin__ not in (Union, Tuple, tuple)
and issubclass(type.__origin__, TypingSequence)
)
or (type.__origin__ in (Tuple, tuple) and type.__args__[1] is ...)
)

def is_deque(type: Any) -> bool:
return (
type in (deque, Deque)
or (type.__class__ is _GenericAlias and issubclass(type.__origin__, deque))
or type.__origin__ is deque
)

def is_mutable_set(type):
return type is set or (
type.__class__ is _GenericAlias and issubclass(type.__origin__, MutableSet)
)

def is_frozenset(type):
return type is frozenset or (
type.__class__ is _GenericAlias and issubclass(type.__origin__, FrozenSet)
)

def is_mapping(type):
return type in (TypingMapping, dict) or (
type.__class__ is _GenericAlias
and issubclass(type.__origin__, TypingMapping)
)

bare_generic_args = {
List.__args__,
TypingSequence.__args__,
TypingMapping.__args__,
Dict.__args__,
TypingMutableSequence.__args__,
Tuple.__args__,
None, # non-parametrized containers do not have `__args__ attribute in py3.7-8
}

def is_bare(type):
return getattr(type, "__args__", None) in bare_generic_args

def is_counter(type):
return (
type in (Counter, ColCounter)
or getattr(type, "__origin__", None) is ColCounter
)

if sys.version_info >= (3, 8):
from typing import Literal

def is_literal(type) -> bool:
return type.__class__ is _GenericAlias and type.__origin__ is Literal

else:
# No literals in 3.7.
def is_literal(_) -> bool:
return False

def is_generic(obj):
return isinstance(obj, _GenericAlias)

def copy_with(type, args):
"""Replace a generic type's arguments."""
return type.copy_with(args)

def is_typeddict(cls) -> bool:
return (
cls.__class__ is _TypedDictMeta
or (is_generic(cls) and (cls.__origin__.__class__ is _TypedDictMeta))
or (
ExtensionsTypedDictMeta is not None
and cls.__class__ is ExtensionsTypedDictMeta
or (
is_generic(cls)
and (cls.__origin__.__class__ is ExtensionsTypedDictMeta)
)
)
)

def get_notrequired_base(type) -> "Union[Any, Literal[NOTHING]]":
if get_origin(type) in (NotRequired, Required):
return get_args(type)[0]
return NOTHING


def is_generic_attrs(type):
return is_generic(type) and has(type.__origin__)
7 changes: 3 additions & 4 deletions src/cattrs/gen/typeddicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import linecache
import re
import sys
from typing import TYPE_CHECKING, Any, Callable, TypeVar

from attr import NOTHING, Attribute
Expand Down Expand Up @@ -34,8 +35,6 @@ def get_annots(cl):
is_annotated,
is_bare,
is_generic,
is_py39_plus,
is_py311_plus,
)
from .._generics import deep_copy_with
from ..errors import (
Expand Down Expand Up @@ -533,12 +532,12 @@ def _is_extensions_typeddict(cls) -> bool:
)


if is_py311_plus:
if sys.version_info >= (3, 11):

def _required_keys(cls: type) -> set[str]:
return cls.__required_keys__

elif is_py39_plus:
elif sys.version_info >= (3, 9):
from typing_extensions import Annotated, NotRequired, Required, get_args

def _required_keys(cls: type) -> set[str]:
Expand Down
8 changes: 7 additions & 1 deletion tests/_compat.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from cattrs._compat import is_py37, is_py38
import sys

is_py37 = sys.version_info[:2] == (3, 7)
is_py38 = sys.version_info[:2] == (3, 8)
is_py39_plus = sys.version_info >= (3, 9)
is_py310_plus = sys.version_info >= (3, 10)
is_py311_plus = sys.version_info >= (3, 11)

if is_py37 or is_py38:
from typing import Dict, List
Expand Down
Loading