From 03c35b43aeeff996a8faf4117681b5f6cab88078 Mon Sep 17 00:00:00 2001 From: Joseph Perez Date: Mon, 12 Dec 2022 00:02:20 +0100 Subject: [PATCH] Remove hacks for 3.6 support --- .github/workflows/ci.yml | 8 ++-- .github/workflows/doc.yml | 2 +- README.md | 4 +- apischema/conversions/visitor.py | 1 + apischema/discriminators.py | 7 +-- .../graphql/relay/global_identification.py | 7 +-- apischema/typing.py | 45 ++++--------------- apischema/utils.py | 4 +- apischema/visitor.py | 4 +- docs/de_serialization.md | 2 - docs/difference_with_pydantic.md | 4 +- docs/index.md | 4 +- examples/serialized_generic.py | 5 +-- scripts/test_wrapper.py | 2 +- setup.py | 5 +-- tests/requirements.txt | 1 - tests/unit/test_visitor.py | 21 +++------ 17 files changed, 33 insertions(+), 93 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9c909e3..e6540ca5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: matrix: python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy-3.9'] include: - - python-version: '3.10' + - python-version: '3.11' pytest-args: --cov=apischema --cov-branch --cov-report=xml --cov-report=html steps: - uses: actions/cache@v3.0.1 @@ -58,13 +58,13 @@ jobs: htmlcov - name: Cythonize run: scripts/cythonize.sh - if: matrix.python-version != 'pypy3' + if: matrix.python-version != 'pypy-3.9' - name: Compile run: python setup.py build_ext --inplace - if: matrix.python-version != 'pypy3' + if: matrix.python-version != 'pypy-3.9' - name: Run tests (compiled) run: pytest tests ${{ matrix.pytest-args }} - if: matrix.python-version != 'pypy3' + if: matrix.python-version != 'pypy-3.9' concurrency: group: ci-${{ github.head_ref }} diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index f79038a5..bcb8fc93 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v3 with: - python-version: '3.10' + python-version: '3.11' - name: Cythonize run: scripts/cythonize.sh - name: Install apischema diff --git a/README.md b/README.md index 3c2565d4..bd43004d 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,7 @@ JSON (de)serialization, GraphQL and JSON schema generation using Python typing. ```shell pip install apischema ``` -It requires only Python 3.6+ (and dataclasses [official backport](https://pypi.org/project/dataclasses/) for version 3.6 only) - -*PyPy3* is fully supported. +It requires only Python 3.7+. *PyPy3* is also fully supported. ## Why another library? diff --git a/apischema/conversions/visitor.py b/apischema/conversions/visitor.py index b3cdb15e..a2c7b23e 100644 --- a/apischema/conversions/visitor.py +++ b/apischema/conversions/visitor.py @@ -134,6 +134,7 @@ def visit(self, tp: AnyType) -> Result: def sub_conversion( conversion: ResolvedConversion, next_conversion: Optional[AnyConversion] ) -> Optional[AnyConversion]: + # TODO why did I use LazyConversion here? return ( LazyConversion(lambda: conversion.sub_conversion), LazyConversion(lambda: next_conversion), diff --git a/apischema/discriminators.py b/apischema/discriminators.py index 7001eb70..e687b963 100644 --- a/apischema/discriminators.py +++ b/apischema/discriminators.py @@ -1,5 +1,4 @@ import operator -import sys from dataclasses import dataclass from functools import reduce from typing import ( @@ -39,11 +38,7 @@ def get_discriminated(alias: str, tp: AnyType) -> Sequence[str]: has_field = True field_type = no_annotated(field.type) if is_literal(field_type): - if sys.version_info < (3, 7): # py36 - literal_args = field_type.__values__ - else: - literal_args = get_args(field_type) - return [v for v in literal_args if isinstance(v, str)] + return [v for v in get_args(field_type) if isinstance(v, str)] if ( is_typed_dict(cls) and not has_field ): # TypedDict must have a discriminator field diff --git a/apischema/graphql/relay/global_identification.py b/apischema/graphql/relay/global_identification.py index f1796556..f38c2de8 100644 --- a/apischema/graphql/relay/global_identification.py +++ b/apischema/graphql/relay/global_identification.py @@ -81,7 +81,8 @@ class Node(Generic[Id], ABC): id: Id = field(metadata=skip) global_id: ClassVar[property] - @property # type: ignore + @resolver("id", order=order(-1)) # type: ignore + @property def global_id(self: Node_) -> GlobalId[Node_]: return self.id_to_global(self.id) @@ -121,10 +122,6 @@ def __init_subclass__(cls, not_a_node: bool = False, **kwargs): _tmp_nodes.append(cls) -resolver("id", order=order(-1))( - Node.global_id -) # cannot directly decorate property because py36 - _tmp_nodes: List[Type[Node]] = [] _nodes: Dict[str, Type[Node]] = {} diff --git a/apischema/typing.py b/apischema/typing.py index e558fef7..8a00a424 100644 --- a/apischema/typing.py +++ b/apischema/typing.py @@ -2,20 +2,8 @@ __all__ = ["get_args", "get_origin", "get_type_hints"] import sys -from contextlib import suppress from types import ModuleType, new_class -from typing import ( - Any, - Callable, - Collection, - Dict, - Generic, - Set, - Tuple, - Type, - TypeVar, - Union, -) +from typing import Any, Callable, Collection, Dict, Generic, Set, Type, TypeVar, Union class _FakeType: @@ -53,20 +41,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): from typing_extensions import get_args, get_origin except ImportError: - def _assemble_tree(tree: Tuple[Any]) -> Any: - if not isinstance(tree, tuple): - return tree - else: - origin, *args = tree - with suppress(NameError): - if origin is Annotated: - return Annotated[(_assemble_tree(args[0]), *args[1])] - return origin[tuple(map(_assemble_tree, args))] - def get_origin(tp): - # In Python 3.6: List[Collection[T]][int].__args__ == int != Collection[int] - if hasattr(tp, "_subs_tree"): - tp = _assemble_tree(tp._subs_tree()) if isinstance(tp, _AnnotatedAlias): return None if tp.__args__ is None else Annotated if tp is Generic: @@ -74,13 +49,9 @@ def get_origin(tp): return getattr(tp, "__origin__", None) def get_args(tp): - # In Python 3.6: List[Collection[T]][int].__args__ == int != Collection[int] - if hasattr(tp, "_subs_tree"): - tp = _assemble_tree(tp._subs_tree()) if isinstance(tp, _AnnotatedAlias): return () if tp.__args__ is None else (tp.__args__[0], *tp.__metadata__) - # __args__ can be None in 3.6 inside __set_name__ - res = getattr(tp, "__args__", ()) or () + res = tp.__args__ if get_origin(tp) is Callable and res[0] is not Ellipsis: res = (list(res[:-1]), res[-1]) return res @@ -95,11 +66,11 @@ def get_args(tp): pass if sys.version_info >= (3, 11): - from typing import _collect_parameters as _collect_type_vars + from typing import _collect_parameters elif sys.version_info >= (3, 7): - from typing import _collect_type_vars # type: ignore + from typing import _collect_type_vars as _collect_parameters # type: ignore else: - from typing import _type_vars as _collect_type_vars + from typing import _type_vars as _collect_parameters def _generic_mro(result, tp): @@ -108,7 +79,7 @@ def _generic_mro(result, tp): origin = tp result[origin] = tp if hasattr(origin, "__orig_bases__"): - parameters = _collect_type_vars(origin.__orig_bases__) + parameters = _collect_parameters(origin.__orig_bases__) substitution = dict(zip(parameters, get_args(tp))) for base in origin.__orig_bases__: if get_origin(base) in result: @@ -208,12 +179,12 @@ def is_literal(tp: Any) -> bool: try: from typing import Literal - return get_origin(tp) == Literal or isinstance(tp, type(Literal)) # py36 + return get_origin(tp) == Literal except ImportError: try: from typing_extensions import Literal # type: ignore - return get_origin(tp) == Literal or isinstance(tp, type(Literal)) # py36 + return get_origin(tp) == Literal except ImportError: return False diff --git a/apischema/utils.py b/apischema/utils.py index d32dea2a..1b0c1a16 100644 --- a/apischema/utils.py +++ b/apischema/utils.py @@ -34,7 +34,7 @@ from apischema.types import COLLECTION_TYPES, MAPPING_TYPES, PRIMITIVE_TYPES, AnyType from apischema.typing import ( - _collect_type_vars, + _collect_parameters, generic_mro, get_args, get_origin, @@ -138,7 +138,7 @@ def get_parameters(tp: AnyType) -> Iterable[TV]: if hasattr(tp, "__parameters__"): return tp.__parameters__ elif hasattr(tp, "__orig_bases__"): - return _collect_type_vars(tp.__orig_bases__) + return _collect_parameters(tp.__orig_bases__) elif is_type_var(tp): return (tp,) else: diff --git a/apischema/visitor.py b/apischema/visitor.py index 6fdffd6c..f4b3faf2 100644 --- a/apischema/visitor.py +++ b/apischema/visitor.py @@ -164,7 +164,7 @@ def visit(self, tp: AnyType) -> Result: return self.collection(origin, args[0]) if origin in MAPPING_TYPES: return self.mapping(origin, args[0], args[1]) - if is_literal(tp): # pragma: no cover py37+ + if is_literal(tp): return self.literal(args) if origin in PRIMITIVE_TYPES: return self.primitive(origin) @@ -195,8 +195,6 @@ def visit(self, tp: AnyType) -> Result: return self.named_tuple( origin, types, origin._field_defaults # type: ignore ) - if is_literal(origin): # pragma: no cover py36 - return self.literal(origin.__values__) if is_typed_dict(origin): return self.typed_dict( origin, resolve_type_hints(origin), required_keys(origin) diff --git a/docs/de_serialization.md b/docs/de_serialization.md index 4d90cac9..034eaf40 100644 --- a/docs/de_serialization.md +++ b/docs/de_serialization.md @@ -201,8 +201,6 @@ Serialized methods of generic classes get the right type when their owning class ```python {!serialized_generic.py!} ``` -!!! warning - `serialized` cannot decorate methods of `Generic` classes in Python 3.6, it has to be used outside of class. ### Exclude unset fields diff --git a/docs/difference_with_pydantic.md b/docs/difference_with_pydantic.md index e1b912bd..e773899c 100644 --- a/docs/difference_with_pydantic.md +++ b/docs/difference_with_pydantic.md @@ -34,9 +34,9 @@ While *pydantic* mixes up model constructor with deserializer, *apischema* uses *pydantic* requires calling `update_forward_refs` method on recursive types, while *apischema* "just works". -### *apischema* supports `Generic` in Python 3.6 and without requiring additional stuff +### *apischema* supports `Generic` without requiring additional stuff -*pydantic* `BaseModel` cannot be used with generic model, you have to use `GenericModel`, and it's not supported in Python 3.6. +*pydantic* `BaseModel` cannot be used with generic model, you have to use `GenericModel`. With *apischema*, you just write your generic classes normally. diff --git a/docs/index.md b/docs/index.md index 23d3a86d..67497423 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,9 +16,7 @@ JSON (de)serialization, GraphQL and JSON schema generation using Python typing. ```shell pip install apischema ``` -It requires only Python 3.6+ (and dataclasses [official backport](https://pypi.org/project/dataclasses/) for version 3.6 only) - -*PyPy3* is fully supported. +It requires only Python 3.7+. *PyPy3* is also fully supported. ## Why another library? diff --git a/examples/serialized_generic.py b/examples/serialized_generic.py index 364a7a34..3757b45b 100644 --- a/examples/serialized_generic.py +++ b/examples/serialized_generic.py @@ -10,14 +10,11 @@ @dataclass class Foo(Generic[T]): - # serialized decorator for methods of generic class is not supported in Python 3.6 + @serialized def bar(self) -> T: ... -serialized(Foo.bar) - - @serialized def baz(foo: Foo[U]) -> U: ... diff --git a/scripts/test_wrapper.py b/scripts/test_wrapper.py index a6454956..013a0419 100644 --- a/scripts/test_wrapper.py +++ b/scripts/test_wrapper.py @@ -40,7 +40,7 @@ def __getattribute__(self, name): class Wrapper: def __init__(self, cls): self.cls = cls - self.implem = cls.__origin__ or cls.__extra__ # extra in 3.6 + self.implem = cls.__origin__ def __getitem__(self, item): return self.cls[item] diff --git a/setup.py b/setup.py index 013c6734..1c5ff0b6 100644 --- a/setup.py +++ b/setup.py @@ -100,8 +100,7 @@ def build_extension(self, ext): description="JSON (de)serialization, GraphQL and JSON schema generation using Python typing.", long_description=pathlib.Path("README.md").read_text(), long_description_content_type="text/markdown", - python_requires=">=3.6", - install_requires=["dataclasses>=0.7;python_version<'3.7'"], + python_requires=">=3.7", extras_require={ "graphql": ["graphql-core>=3.0.0"], "examples": [ @@ -119,11 +118,11 @@ def build_extension(self, ext): "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Software Development :: Libraries :: Python Modules", ], cmdclass={"build_ext": custom_build_ext}, diff --git a/tests/requirements.txt b/tests/requirements.txt index 2f3e6e22..764ebb2f 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,3 @@ -dataclasses==0.8;python_version<'3.7' graphql-core==3.2.0 attrs==21.4.0 bson==0.5.10 diff --git a/tests/unit/test_visitor.py b/tests/unit/test_visitor.py index 560754f8..c513440f 100644 --- a/tests/unit/test_visitor.py +++ b/tests/unit/test_visitor.py @@ -64,21 +64,6 @@ class MyInt(int): pass -py36 = [ - (List[int], Visitor.collection, [List, int]), - (Tuple[str, ...], Visitor.collection, [Tuple, str]), - (Collection[int], Visitor.collection, [Collection, int]), - (Mapping[str, int], Visitor.mapping, [Mapping, str, int]), - (Dict[str, int], Visitor.mapping, [Dict, str, int]), -] -py37 = [ - (List[int], Visitor.collection, [list, int]), - (Tuple[str, ...], Visitor.collection, [tuple, str]), - (Collection[int], Visitor.collection, [collections.abc.Collection, int]), - (Mapping[str, int], Visitor.mapping, [collections.abc.Mapping, str, int]), - (Dict[str, int], Visitor.mapping, [dict, str, int]), -] - pep_585: list = [] if sys.version_info >= (3, 9): pep_585 = [ @@ -105,7 +90,11 @@ class MyInt(int): @pytest.mark.parametrize( "cls, method, args", [ - *(py37 if sys.version_info >= (3, 7) else py36), + (List[int], Visitor.collection, [list, int]), + (Tuple[str, ...], Visitor.collection, [tuple, str]), + (Collection[int], Visitor.collection, [collections.abc.Collection, int]), + (Mapping[str, int], Visitor.mapping, [collections.abc.Mapping, str, int]), + (Dict[str, int], Visitor.mapping, [dict, str, int]), *pep_585, *py310, (Annotated[int, 42, "42"], Visitor.annotated, [int, (42, "42")]),