diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2ec0fb4..832feb0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,3 +21,5 @@ Every commit is checked with pre-commit hooks for : - type safety with [mypy](http://mypy-lang.org/) - test conformance by running [tests](./tests) with [pytest](https://docs.pytest.org/en/latest/) - You can run `pytest` from the command line. + + - You can also run `tox` from the command line to test in all supported python versions. Note that this will require you to have all supported python versions installed. \ No newline at end of file diff --git a/marshmallow_dataclass/__init__.py b/marshmallow_dataclass/__init__.py index 8fd52ec..f104f06 100644 --- a/marshmallow_dataclass/__init__.py +++ b/marshmallow_dataclass/__init__.py @@ -54,13 +54,12 @@ class User: TypeVar, Union, cast, - get_args, - get_origin, get_type_hints, overload, ) import marshmallow +import typing_extensions import typing_inspect from marshmallow_dataclass.lazy_class_attribute import lazy_class_attribute @@ -392,7 +391,13 @@ def _internal_class_schema( base_schema: Optional[Type[marshmallow.Schema]] = None, clazz_frame: Optional[types.FrameType] = None, ) -> Type[marshmallow.Schema]: - _RECURSION_GUARD.seen_classes[clazz] = clazz.__name__ + if typing_extensions.get_origin(clazz) is Annotated and sys.version_info < (3, 10): + # https://github.com/python/cpython/blob/3.10/Lib/typing.py#L977 + class_name = clazz._name or clazz.__origin__.__name__ # type: ignore[attr-defined] + else: + class_name = clazz.__name__ + + _RECURSION_GUARD.seen_classes[clazz] = class_name try: # noinspection PyDataclass fields: Tuple[dataclasses.Field, ...] = dataclasses.fields(clazz) @@ -427,11 +432,18 @@ def _internal_class_schema( include_non_init = getattr(getattr(clazz, "Meta", None), "include_non_init", False) # Update the schema members to contain marshmallow fields instead of dataclass fields - type_hints = get_type_hints( - clazz, - localns=clazz_frame.f_locals if clazz_frame else None, - include_extras=True, - ) + + if sys.version_info >= (3, 9): + type_hints = get_type_hints( + clazz, + localns=clazz_frame.f_locals if clazz_frame else None, + include_extras=True, + ) + else: + type_hints = get_type_hints( + clazz, + localns=clazz_frame.f_locals if clazz_frame else None, + ) attributes.update( ( field.name, @@ -526,8 +538,8 @@ def _field_for_generic_type( """ If the type is a generic interface, resolve the arguments and construct the appropriate Field. """ - origin = get_origin(typ) - arguments = get_args(typ) + origin = typing_extensions.get_origin(typ) + arguments = typing_extensions.get_args(typ) if origin: # Override base_schema.TYPE_MAPPING to change the class used for generic types below type_mapping = base_schema.TYPE_MAPPING if base_schema else {} @@ -764,7 +776,7 @@ def field_for_schema( ) # enumerations - if issubclass(typ, Enum): + if inspect.isclass(typ) and issubclass(typ, Enum): return marshmallow.fields.Enum(typ, **metadata) # Nested marshmallow dataclass diff --git a/setup.py b/setup.py index d446bf8..bce74f6 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,8 @@ # re: pypy: typed-ast (a dependency of mypy) fails to install on pypy # https://github.com/python/typed_ast/issues/111 "pytest-mypy-plugins>=1.2.0; implementation_name != 'pypy'", + "tox>=4", + "virtualenv-pyenv", ], } EXTRAS_REQUIRE["dev"] = ( diff --git a/tests/test_annotated.py b/tests/test_annotated.py index 9b57ac5..e9105a6 100644 --- a/tests/test_annotated.py +++ b/tests/test_annotated.py @@ -1,11 +1,17 @@ +import sys import unittest -from typing import Annotated, Optional +from typing import Optional import marshmallow import marshmallow.fields from marshmallow_dataclass import dataclass +if sys.version_info >= (3, 9): + from typing import Annotated +else: + from typing_extensions import Annotated + class TestAnnotatedField(unittest.TestCase): def test_annotated_field(self): diff --git a/tests/test_mypy.yml b/tests/test_mypy.yml index 1487cb7..479abd5 100644 --- a/tests/test_mypy.yml +++ b/tests/test_mypy.yml @@ -27,7 +27,7 @@ reveal_type(user.email) # N: Revealed type is "builtins.str" User(id=42, email="user@email.com") # E: Argument "id" to "User" has incompatible type "int"; expected "str" [arg-type] - User(id="a"*32, email=["not", "a", "string"]) # E: Argument "email" to "User" has incompatible type "list[str]"; expected "str" [arg-type] + User(id="a"*32, email=["not", "a", "string"]) # E: Argument "email" to "User" has incompatible type "List[str]"; expected "str" [arg-type] - case: marshmallow_dataclass_keyword_arguments mypy_config: | follow_imports = silent diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6bcf7dd --- /dev/null +++ b/tox.ini @@ -0,0 +1,11 @@ +[tox] +requires = + tox>=4 +env_list = py{38,39,310,311,312} + +[testenv] +deps = pytest +commands = pytest +extras = dev +set_env = + VIRTUALENV_DISCOVERY = pyenv