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: deeply nested generics #171

Closed
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
6 changes: 6 additions & 0 deletions src/cattr/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def __init__(
(int, self._structure_call),
(float, self._structure_call),
(Enum, self._structure_call),
(NoneType, self._structure_none)
]
)

Expand Down Expand Up @@ -515,6 +516,11 @@ def _structure_dict(self, obj, cl):
for k, v in obj.items()
}

def _structure_none(self, obj, type):
if obj is not None:
raise ValueError(f"Unable to structure NoneType with a value of {obj}")
return None

def _structure_optional(self, obj, union):
if obj is None:
return None
Expand Down
37 changes: 23 additions & 14 deletions src/cattr/gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,20 @@ def _generate_mapping(cl: Type, old_mapping):

return cls(**mapping)

def _get_type(cl: Type, mapping) -> Type:
if isinstance(cl, TypeVar):
return getattr(mapping, cl.__name__, cl)

if not is_generic(cl):
return cl

base = get_origin(cl)
return base[
tuple(
_get_type(x, mapping)
for x in get_args(cl)
)
]

def make_dict_structure_fn(
cl: Type,
Expand Down Expand Up @@ -184,12 +198,14 @@ def make_dict_structure_fn(
fn_name = "structure_" + cl_name

# We have generic parameters and need to generate a unique name for the function
for p in getattr(cl, "__parameters__", ()):
# This is nasty, I am not sure how best to handle `typing.List[str]` or `TClass[int, int]` as a parameter type here
name_base = getattr(mapping, p.__name__)
name = getattr(name_base, "__name__", None) or str(name_base)
name = re.sub(r"[\[\.\] ,]", "_", name)
fn_name += f"_{name}"
# If there is no mapping, then there is no need for a unique name
if mapping:
for p in getattr(cl, "__parameters__", ()):
# This is nasty, I am not sure how best to handle `typing.List[str]` or `TClass[int, int]` as a parameter type here
name_base = getattr(mapping, p.__name__)
name = getattr(name_base, "__name__", None) or str(name_base)
name = re.sub(r"[\[\.\] ,]", "_", name)
fn_name += f"_{name}"

globs = {"__c_s": converter.structure, "__cl": cl, "__m": mapping}
lines = []
Expand All @@ -207,14 +223,7 @@ def make_dict_structure_fn(
for a in attrs:
an = a.name
override = kwargs.pop(an, _neutral)
t = a.type
if isinstance(t, TypeVar):
t = getattr(mapping, t.__name__, t)
elif is_generic(t):
concrete_types = tuple(
getattr(mapping, t.__name__, t) for t in get_args(t)
)
t = t.__origin__[concrete_types]
t = _get_type(a.type, mapping)

# For each attribute, we try resolving the type here and now.
# If a type is manually overwritten, this function should be
Expand Down
17 changes: 16 additions & 1 deletion tests/test_generics.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Dict, Generic, List, TypeVar, Union
from typing import Dict, Generic, List, TypeVar, Union, Optional

import pytest
from attr import asdict, attrs, define
Expand Down Expand Up @@ -69,6 +69,21 @@ class GenericCols(Generic[T]):

assert res == expected

@pytest.mark.parametrize(
("t", "result"),
((int, (1, [1,2,3])), (int, (1, None))),
)
def test_structure_nested_generics_with_cols(t, result):
@define
class GenericCols(Generic[T]):
a: T
b: Optional[List[T]]

expected = GenericCols(*result)

res = GenConverter().structure(asdict(expected), GenericCols[t])

assert res == expected

@pytest.mark.parametrize(
("t", "t2", "result"),
Expand Down