From d49368081d61886e8c98a4b1eaf61b05e7f7ea13 Mon Sep 17 00:00:00 2001 From: "arne.vanlondersele" Date: Sat, 11 Jan 2025 16:14:12 +0100 Subject: [PATCH 1/5] support generics and forward references --- dacite/config.py | 1 + dacite/core.py | 24 +++++++---- dacite/generics.py | 91 +++++++++++++++++++++++++++++++++++++++++ dacite/types.py | 100 ++++++++++++++++++++++++--------------------- 4 files changed, 162 insertions(+), 54 deletions(-) create mode 100644 dacite/generics.py diff --git a/dacite/config.py b/dacite/config.py index 81e660b..78359da 100644 --- a/dacite/config.py +++ b/dacite/config.py @@ -19,6 +19,7 @@ class Config: check_types: bool = True strict: bool = False strict_unions_match: bool = False + convert_key: Callable[[str], str] = field(default_factory=lambda: lambda x: x) @cached_property def hashable_forward_references(self) -> Optional[FrozenDict]: diff --git a/dacite/core.py b/dacite/core.py index 9e45129..aa2a0b4 100644 --- a/dacite/core.py +++ b/dacite/core.py @@ -1,6 +1,6 @@ from dataclasses import is_dataclass from itertools import zip_longest -from typing import TypeVar, Type, Optional, get_type_hints, Mapping, Any, Collection, MutableMapping +from typing import TypeVar, Type, Optional, Mapping, Any, Collection, MutableMapping from dacite.cache import cache from dacite.config import Config @@ -8,7 +8,6 @@ from dacite.dataclasses import ( get_default_value_for_field, DefaultValueNotFoundError, - get_fields, is_frozen, ) from dacite.exceptions import ( @@ -33,6 +32,8 @@ is_subclass, ) +from dacite.generics import get_concrete_type_hints, get_fields, orig + T = TypeVar("T") @@ -47,21 +48,26 @@ def from_dict(data_class: Type[T], data: Data, config: Optional[Config] = None) init_values: MutableMapping[str, Any] = {} post_init_values: MutableMapping[str, Any] = {} config = config or Config() + try: - data_class_hints = cache(get_type_hints)(data_class, localns=config.hashable_forward_references) + data_class_hints = cache(get_concrete_type_hints)(data_class, localns=config.hashable_forward_references) except NameError as error: raise ForwardReferenceError(str(error)) + data_class_fields = cache(get_fields)(data_class) + if config.strict: extra_fields = set(data.keys()) - {f.name for f in data_class_fields} if extra_fields: raise UnexpectedDataError(keys=extra_fields) + for field in data_class_fields: field_type = data_class_hints[field.name] - if field.name in data: + key = config.convert_key(field.name) + + if key in data: try: - field_data = data[field.name] - value = _build_value(type_=field_type, data=field_data, config=config) + value = _build_value(type_=field_type, data=data[key], config=config) except DaciteFieldError as error: error.update_path(field.name) raise @@ -74,13 +80,17 @@ def from_dict(data_class: Type[T], data: Data, config: Optional[Config] = None) if not field.init: continue raise MissingValueError(field.name) + if field.init: init_values[field.name] = value elif not is_frozen(data_class): post_init_values[field.name] = value + instance = data_class(**init_values) + for key, value in post_init_values.items(): setattr(instance, key, value) + return instance @@ -95,7 +105,7 @@ def _build_value(type_: Type, data: Any, config: Config) -> Any: data = _build_value_for_union(union=type_, data=data, config=config) elif is_generic_collection(type_): data = _build_value_for_collection(collection=type_, data=data, config=config) - elif cache(is_dataclass)(type_) and isinstance(data, Mapping): + elif cache(is_dataclass)(orig(type_)) and isinstance(data, Mapping): data = from_dict(data_class=type_, data=data, config=config) for cast_type in config.cast: if is_subclass(type_, cast_type): diff --git a/dacite/generics.py b/dacite/generics.py new file mode 100644 index 0000000..a1e30df --- /dev/null +++ b/dacite/generics.py @@ -0,0 +1,91 @@ +import sys +from dataclasses import Field, is_dataclass +from typing import Any, Generic, Literal, TypeVar, get_args, get_origin, get_type_hints + +from .dataclasses import get_fields as dataclasses_get_fields + + +def _add_generics(type_origin: Any, type_args: tuple, generics: dict) -> None: + """Adds (type var, concrete type) entries derived from a type's origin and args to the provided generics dict.""" + if type_origin and type_args and hasattr(type_origin, '__parameters__'): + for param, arg in zip(type_origin.__parameters__, type_args): + if param.__class__ is TypeVar: + if param in generics and generics[param] != arg: + raise Exception('Generics error.') + generics[param] = arg + + +def _dereference(type_name: str, data_class: type) -> type: + """ + Try to find the class belonging to the reference in the provided module and, + if not found, iteratively look in parent modules. + """ + if data_class.__class__.__name__ == type_name: + return data_class + + module_name = data_class.__module__ + parts = module_name.split('.') + for i in range(len(parts)): + try: + module = sys.modules['.'.join(parts[:-i]) if i else module_name] + return getattr(module, type_name) + except AttributeError: + pass + raise AttributeError('Could not find reference.') + + +def _concretize(hint: type, generics: dict[type, type], data_class: type) -> type: + """Recursively replace type vars and forward references by concrete types.""" + + if hint.__class__ is str: + return _dereference(hint, data_class) + + if hint.__class__ is TypeVar: + return generics.get(hint, hint) + + hint_origin = get_origin(hint) + hint_args = get_args(hint) + if hint_origin and hint_args and hint_origin is not Literal: + concrete_hint_args = tuple(_concretize( + a, generics, data_class) for a in hint_args) + return hint_origin[concrete_hint_args] + + return hint + + +def orig(data_class: type) -> Any: + if is_dataclass(data_class): + return data_class + return get_origin(data_class) + + +def get_concrete_type_hints(data_class: type, *args, **kwargs) -> dict[str, Any]: + """ + An overwrite of typing.get_type_hints supporting generics and forward references, + i.e. substituting concrete types in type vars and references. + """ + generics = {} + + dc_origin = get_origin(data_class) + dc_args = get_args(data_class) + _add_generics(dc_origin, dc_args, generics) + + if hasattr(data_class, '__orig_bases__'): + for base in data_class.__orig_bases__: + base_origin = get_origin(base) + base_args = get_args(base) + if base_origin is not Generic: + _add_generics(base_origin, base_args, generics) + + data_class = orig(data_class) + hints = get_type_hints(data_class, *args, **kwargs) + + for key, hint in hints.copy().items(): + hints[key] = _concretize(hint, generics, data_class) + + return hints + + +def get_fields(data_class: type) -> list[Field]: + """An overwrite of dacite's get_fields function, supporting generics.""" + return dataclasses_get_fields(orig(data_class)) diff --git a/dacite/types.py b/dacite/types.py index af7b495..8198415 100644 --- a/dacite/types.py +++ b/dacite/types.py @@ -1,4 +1,4 @@ -from dataclasses import InitVar +from dataclasses import InitVar, is_dataclass from typing import ( Type, Any, @@ -9,6 +9,7 @@ Mapping, Tuple, cast as typing_cast, + get_origin, ) from dacite.cache import cache @@ -94,52 +95,6 @@ def extract_init_var(type_: Type) -> Union[Type, Any]: return Any -def is_instance(value: Any, type_: Type) -> bool: - try: - # As described in PEP 484 - section: "The numeric tower" - if (type_ in [float, complex] and isinstance(value, (int, float))) or isinstance(value, type_): - return True - except TypeError: - pass - if type_ == Any: - return True - elif is_union(type_): - return any(is_instance(value, t) for t in extract_generic(type_)) - elif is_generic_collection(type_): - origin = extract_origin_collection(type_) - if not isinstance(value, origin): - return False - if not extract_generic(type_): - return True - if isinstance(value, tuple) and is_tuple(type_): - tuple_types = extract_generic(type_) - if len(tuple_types) == 1 and tuple_types[0] == (): - return len(value) == 0 - elif len(tuple_types) == 2 and tuple_types[1] is ...: - return all(is_instance(item, tuple_types[0]) for item in value) - else: - if len(tuple_types) != len(value): - return False - return all(is_instance(item, item_type) for item, item_type in zip(value, tuple_types)) - if isinstance(value, Mapping): - key_type, val_type = extract_generic(type_, defaults=(Any, Any)) - for key, val in value.items(): - if not is_instance(key, key_type) or not is_instance(val, val_type): - return False - return True - return all(is_instance(item, extract_generic(type_, defaults=(Any,))[0]) for item in value) - elif is_new_type(type_): - return is_instance(value, extract_new_type(type_)) - elif is_literal(type_): - return value in extract_generic(type_) - elif is_init_var(type_): - return is_instance(value, extract_init_var(type_)) - elif is_type_generic(type_): - return is_subclass(value, extract_generic(type_)[0]) - else: - return False - - @cache def is_generic_collection(type_: Type) -> bool: if not is_generic(type_): @@ -179,3 +134,54 @@ def is_type_generic(type_: Type) -> bool: return type_.__origin__ in (type, Type) except AttributeError: return False + + +@cache +def is_generic_dataclass(type_: Type) -> bool: + return is_dataclass(get_origin(type_)) + + +def is_instance(value: Any, type_: Type) -> bool: + try: + # As described in PEP 484 - section: "The numeric tower" + if (type_ in [float, complex] and isinstance(value, (int, float))) or isinstance(value, type_): + return True + except TypeError: + pass + if type_ == Any: + return True + if is_union(type_): + return any(is_instance(value, t) for t in extract_generic(type_)) + if is_generic_collection(type_): + origin = extract_origin_collection(type_) + if not isinstance(value, origin): + return False + if not extract_generic(type_): + return True + if isinstance(value, tuple) and is_tuple(type_): + tuple_types = extract_generic(type_) + if len(tuple_types) == 1 and tuple_types[0] == (): + return len(value) == 0 + if len(tuple_types) == 2 and tuple_types[1] is ...: + return all(is_instance(item, tuple_types[0]) for item in value) + if len(tuple_types) != len(value): + return False + return all(is_instance(item, item_type) for item, item_type in zip(value, tuple_types)) + if isinstance(value, Mapping): + key_type, val_type = extract_generic(type_, defaults=(Any, Any)) + for key, val in value.items(): + if not is_instance(key, key_type) or not is_instance(val, val_type): + return False + return True + return all(is_instance(item, extract_generic(type_, defaults=(Any,))[0]) for item in value) + if is_new_type(type_): + return is_instance(value, extract_new_type(type_)) + if is_literal(type_): + return value in extract_generic(type_) + if is_init_var(type_): + return is_instance(value, extract_init_var(type_)) + if is_type_generic(type_): + return is_subclass(value, extract_generic(type_)[0]) + if is_generic_dataclass(type_): + return isinstance(value, get_origin(type_)) + return False From a89bc168943786118f1515260eb165d06e0d426d Mon Sep 17 00:00:00 2001 From: "arne.vanlondersele" Date: Sat, 11 Jan 2025 20:46:06 +0100 Subject: [PATCH 2/5] update docs --- README.md | 117 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 98 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index da32ad7..b4e9ece 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,14 @@ assert user == User(name='John', age=30, is_active=True) Dacite supports following features: - nested structures -- (basic) types checking +- (basic) type checking - optional fields (i.e. `typing.Optional`) - unions +- generics - forward references - collections - custom type hooks +- case conversion ## Motivation @@ -109,6 +111,7 @@ Configuration is a (data) class with following fields: - `check_types` - `strict` - `strict_unions_match` +- `convert_key` The examples below show all features of `from_dict` function and usage of all `Config` parameters. @@ -233,6 +236,71 @@ result = from_dict(data_class=B, data=data) assert result == B(a_list=[A(x='test1', y=1), A(x='test2', y=2)]) ``` +### Generics + +Dacite supports generics: (multi-)generic dataclasses, but also dataclasses that inherit from a generic dataclass, or dataclasses that have a generic dataclass field. + +```python +T = TypeVar('T') +U = TypeVar('U') + +@dataclass +class X: + a: str + + +@dataclass +class A(Generic[T, U]): + x: T + y: list[U] + +data = { + 'x': { + 'a': 'foo', + }, + 'y': [1, 2, 3] +} + +result = from_dict(data_class=A[X, int], data=data) + +assert result == A(x=X(a='foo'), y=[1,2,3]) + + +@dataclass +class B(A[X, int]): + z: str + +data = { + 'x': { + 'a': 'foo', + }, + 'y': [1, 2, 3], + 'z': 'bar' +} + +result = from_dict(data_class=B, data=data) + +assert result == B(x=X(a='foo'), y=[1,2,3], z='bar') + + +@dataclass +class C: + z: A[X, int] + +data = { + 'z': { + 'x': { + 'a': 'foo', + }, + 'y': [1, 2, 3], + } +} + +result = from_dict(data_class=C, data=data) + +assert result == C(z=A(x=X(a='foo'), y=[1,2,3])) +``` + ### Type hooks You can use `Config.type_hooks` argument if you want to transform the input @@ -313,30 +381,17 @@ data = from_dict(X, {"y": {"s": "text"}}, Config(forward_references={"Y": Y})) assert data == X(Y("text")) ``` -### Types checking +### Type checking -There are rare cases when `dacite` built-in type checker can not validate -your types (e.g. custom generic class) or you have such functionality -covered by other library and you don't want to validate your types twice. -In such case you can disable type checking with `Config(check_types=False)`. -By default types checking is enabled. +If you want to trade-off type checking for speed, you can disabled type checking by setting `check_types` to `False`. ```python -T = TypeVar('T') - - -class X(Generic[T]): - pass - - @dataclass class A: - x: X[str] - - -x = X[str]() + x: str -assert from_dict(A, {'x': x}, config=Config(check_types=False)) == A(x=x) +# won't throw an error even though the type is wrong +from_dict(A, {'x': 4}, config=Config(check_types=False)) ``` ### Strict mode @@ -354,6 +409,30 @@ returns instance of this type. It means that it's possible that there are other matching types further on the `Union` types list. With `strict_unions_match` only a single match is allowed, otherwise `dacite` raises `StrictUnionMatchError`. +## Convert key + +You can pass a callable to the `convert_key` configuration parameter to convert camelCase to snake_case. + +```python +def to_camel_case(key: str) -> str: + first_part, *remaining_parts = key.split('_') + return first_part + ''.join(part.title() for part in remaining_parts) + +@dataclass +class Person: + first_name: str + last_name: str + +data = { + 'firstName': 'John', + 'lastName': 'Doe' +} + +result = from_dict(Person, data, Config(convert_key=to_camel_case)) + +assert result == Person(first_name='John', last_name='Doe') +``` + ## Exceptions Whenever something goes wrong, `from_dict` will raise adequate From deb629ea08d977ca8bf53f4265b14b207c6d5929 Mon Sep 17 00:00:00 2001 From: "arne.vanlondersele" Date: Sun, 12 Jan 2025 09:09:05 +0100 Subject: [PATCH 3/5] unit tests --- tests/core/test_convert_key.py | 19 ++++++++ tests/core/test_forward_reference.py | 31 +++++++++++++ tests/core/test_generics.py | 67 ++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 tests/core/test_convert_key.py create mode 100644 tests/core/test_forward_reference.py create mode 100644 tests/core/test_generics.py diff --git a/tests/core/test_convert_key.py b/tests/core/test_convert_key.py new file mode 100644 index 0000000..c818fe0 --- /dev/null +++ b/tests/core/test_convert_key.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from dacite import Config, from_dict + + +def test_convert_key(): + def to_camel_case(key: str) -> str: + first_part, *remaining_parts = key.split("_") + return first_part + "".join(part.title() for part in remaining_parts) + + @dataclass + class Person: + first_name: str + last_name: str + + data = {"firstName": "John", "lastName": "Doe"} + + result = from_dict(Person, data, Config(convert_key=to_camel_case)) + + assert result == Person(first_name="John", last_name="Doe") diff --git a/tests/core/test_forward_reference.py b/tests/core/test_forward_reference.py new file mode 100644 index 0000000..5b6e572 --- /dev/null +++ b/tests/core/test_forward_reference.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +from typing import List, Optional +from dacite import from_dict + + +@dataclass +class Person: + name: str + children: Optional[List["Person"]] = None + + +@dataclass +class Club: + name: str + members: list["Person"] + + +def test_self_reference(): + data = {"name": "John Doe", "children": [{"name": "Jane Doe"}]} + + result = from_dict(Person, data) + + assert result == Person(name="John Doe", children=[Person(name="Jane Doe")]) + + +def test_other_reference(): + data = {"name": "FooBar", "members": [{"name": "John Doe", "children": [{"name": "Jane Doe"}]}]} + + result = from_dict(Club, data) + + assert result == Club(name="FooBar", members=[Person(name="John Doe", children=[Person(name="Jane Doe")])]) diff --git a/tests/core/test_generics.py b/tests/core/test_generics.py new file mode 100644 index 0000000..5c83421 --- /dev/null +++ b/tests/core/test_generics.py @@ -0,0 +1,67 @@ +from dataclasses import dataclass +from typing import Generic, List, TypeVar +from dacite import from_dict + +T = TypeVar("T") +U = TypeVar("U") + + +@dataclass +class X: + a: str + + +@dataclass +class A(Generic[T, U]): + x: T + y: List[U] + + +def test_multi_generic(): + data = { + "x": { + "a": "foo", + }, + "y": [1, 2, 3], + } + + result = from_dict(data_class=A[X, int], data=data) + + assert result == A(x=X(a="foo"), y=[1, 2, 3]) + + +def test_inherited_generic(): + @dataclass + class B(A[X, int]): + z: str + + data = { + "x": { + "a": "foo", + }, + "y": [1, 2, 3], + "z": "bar", + } + + result = from_dict(data_class=B, data=data) + + assert result == B(x=X(a="foo"), y=[1, 2, 3], z="bar") + + +def test_generic_field(): + @dataclass + class C: + z: A[X, int] + + data = { + "z": { + "x": { + "a": "foo", + }, + "y": [1, 2, 3], + } + } + + result = from_dict(data_class=C, data=data) + + assert result == C(z=A(x=X(a="foo"), y=[1, 2, 3])) From 91a5f852a7acc1bfbc8d9567126bea60d23dc3ff Mon Sep 17 00:00:00 2001 From: "arne.vanlondersele" Date: Mon, 13 Jan 2025 13:08:29 +0100 Subject: [PATCH 4/5] fix get_origin import error --- dacite/generics.py | 24 ++++++++++++++---------- dacite/types.py | 18 ++++++------------ 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/dacite/generics.py b/dacite/generics.py index a1e30df..b9ef9ba 100644 --- a/dacite/generics.py +++ b/dacite/generics.py @@ -1,17 +1,22 @@ import sys from dataclasses import Field, is_dataclass -from typing import Any, Generic, Literal, TypeVar, get_args, get_origin, get_type_hints +from typing import Any, Generic, List, Literal, TypeVar, get_type_hints + +try: + from typing import get_args, get_origin +except ImportError: + from typing_extensions import get_args, get_origin from .dataclasses import get_fields as dataclasses_get_fields def _add_generics(type_origin: Any, type_args: tuple, generics: dict) -> None: """Adds (type var, concrete type) entries derived from a type's origin and args to the provided generics dict.""" - if type_origin and type_args and hasattr(type_origin, '__parameters__'): + if type_origin and type_args and hasattr(type_origin, "__parameters__"): for param, arg in zip(type_origin.__parameters__, type_args): if param.__class__ is TypeVar: if param in generics and generics[param] != arg: - raise Exception('Generics error.') + raise Exception("Generics error.") generics[param] = arg @@ -24,14 +29,14 @@ def _dereference(type_name: str, data_class: type) -> type: return data_class module_name = data_class.__module__ - parts = module_name.split('.') + parts = module_name.split(".") for i in range(len(parts)): try: - module = sys.modules['.'.join(parts[:-i]) if i else module_name] + module = sys.modules[".".join(parts[:-i]) if i else module_name] return getattr(module, type_name) except AttributeError: pass - raise AttributeError('Could not find reference.') + raise AttributeError("Could not find reference.") def _concretize(hint: type, generics: dict[type, type], data_class: type) -> type: @@ -46,8 +51,7 @@ def _concretize(hint: type, generics: dict[type, type], data_class: type) -> typ hint_origin = get_origin(hint) hint_args = get_args(hint) if hint_origin and hint_args and hint_origin is not Literal: - concrete_hint_args = tuple(_concretize( - a, generics, data_class) for a in hint_args) + concrete_hint_args = tuple(_concretize(a, generics, data_class) for a in hint_args) return hint_origin[concrete_hint_args] return hint @@ -70,7 +74,7 @@ def get_concrete_type_hints(data_class: type, *args, **kwargs) -> dict[str, Any] dc_args = get_args(data_class) _add_generics(dc_origin, dc_args, generics) - if hasattr(data_class, '__orig_bases__'): + if hasattr(data_class, "__orig_bases__"): for base in data_class.__orig_bases__: base_origin = get_origin(base) base_args = get_args(base) @@ -86,6 +90,6 @@ def get_concrete_type_hints(data_class: type, *args, **kwargs) -> dict[str, Any] return hints -def get_fields(data_class: type) -> list[Field]: +def get_fields(data_class: type) -> List[Field]: """An overwrite of dacite's get_fields function, supporting generics.""" return dataclasses_get_fields(orig(data_class)) diff --git a/dacite/types.py b/dacite/types.py index 8198415..e62996c 100644 --- a/dacite/types.py +++ b/dacite/types.py @@ -1,16 +1,10 @@ from dataclasses import InitVar, is_dataclass -from typing import ( - Type, - Any, - Optional, - Union, - Collection, - TypeVar, - Mapping, - Tuple, - cast as typing_cast, - get_origin, -) +from typing import Type, Any, Optional, Union, Collection, TypeVar, Mapping, Tuple, cast as typing_cast + +try: + from typing import get_origin +except ImportError: + from typing_extensions import get_origin from dacite.cache import cache From b98223682538040c24dad3230e17b44403298ad1 Mon Sep 17 00:00:00 2001 From: "arne.vanlondersele" Date: Mon, 13 Jan 2025 17:34:03 +0100 Subject: [PATCH 5/5] refactor type hints to be backwards compatible with python 3.8 --- dacite/generics.py | 14 +++++++------- tests/core/test_forward_reference.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dacite/generics.py b/dacite/generics.py index b9ef9ba..81e2f0a 100644 --- a/dacite/generics.py +++ b/dacite/generics.py @@ -1,6 +1,6 @@ import sys from dataclasses import Field, is_dataclass -from typing import Any, Generic, List, Literal, TypeVar, get_type_hints +from typing import Any, Dict, Generic, List, Literal, Tuple, Type, TypeVar, get_type_hints try: from typing import get_args, get_origin @@ -10,7 +10,7 @@ from .dataclasses import get_fields as dataclasses_get_fields -def _add_generics(type_origin: Any, type_args: tuple, generics: dict) -> None: +def _add_generics(type_origin: Any, type_args: Tuple, generics: Dict[TypeVar, Type]) -> None: """Adds (type var, concrete type) entries derived from a type's origin and args to the provided generics dict.""" if type_origin and type_args and hasattr(type_origin, "__parameters__"): for param, arg in zip(type_origin.__parameters__, type_args): @@ -20,7 +20,7 @@ def _add_generics(type_origin: Any, type_args: tuple, generics: dict) -> None: generics[param] = arg -def _dereference(type_name: str, data_class: type) -> type: +def _dereference(type_name: str, data_class: Type) -> Type: """ Try to find the class belonging to the reference in the provided module and, if not found, iteratively look in parent modules. @@ -39,7 +39,7 @@ def _dereference(type_name: str, data_class: type) -> type: raise AttributeError("Could not find reference.") -def _concretize(hint: type, generics: dict[type, type], data_class: type) -> type: +def _concretize(hint: Type, generics: Dict[TypeVar, Type], data_class: Type) -> Type: """Recursively replace type vars and forward references by concrete types.""" if hint.__class__ is str: @@ -57,13 +57,13 @@ def _concretize(hint: type, generics: dict[type, type], data_class: type) -> typ return hint -def orig(data_class: type) -> Any: +def orig(data_class: Type) -> Any: if is_dataclass(data_class): return data_class return get_origin(data_class) -def get_concrete_type_hints(data_class: type, *args, **kwargs) -> dict[str, Any]: +def get_concrete_type_hints(data_class: Type, *args, **kwargs) -> Dict[str, Any]: """ An overwrite of typing.get_type_hints supporting generics and forward references, i.e. substituting concrete types in type vars and references. @@ -90,6 +90,6 @@ def get_concrete_type_hints(data_class: type, *args, **kwargs) -> dict[str, Any] return hints -def get_fields(data_class: type) -> List[Field]: +def get_fields(data_class: Type) -> List[Field]: """An overwrite of dacite's get_fields function, supporting generics.""" return dataclasses_get_fields(orig(data_class)) diff --git a/tests/core/test_forward_reference.py b/tests/core/test_forward_reference.py index 5b6e572..8512845 100644 --- a/tests/core/test_forward_reference.py +++ b/tests/core/test_forward_reference.py @@ -12,7 +12,7 @@ class Person: @dataclass class Club: name: str - members: list["Person"] + members: List["Person"] def test_self_reference():