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 TypeError for asdict(<class with namedtuple>, retain_collection_types=True) #1165

Merged
merged 27 commits into from
Jul 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3076922
Fix TypeError for asdict with namedtuples and retain_collection_types…
kwist-sgr Jul 19, 2023
048ca40
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2023
7bcb3d7
Add news fragment in `changelog.d`
kwist-sgr Jul 19, 2023
3db94ad
fix pre-commit interrogate checker
kwist-sgr Jul 20, 2023
daddbe4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 20, 2023
fb47d74
fix flake8 issue
kwist-sgr Jul 20, 2023
03c214d
also fixed `astuple`
kwist-sgr Jul 20, 2023
55bead0
Add `_is_namedtuple` function
kwist-sgr Jul 22, 2023
2c3914f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 22, 2023
3786bc2
Fix SyntaxError for python 3.7
kwist-sgr Jul 22, 2023
bb13faa
use `issubclass(..., tuple)`
kwist-sgr Jul 22, 2023
208020c
use issubclass(cf, tuple) if case of TypeError
kwist-sgr Jul 22, 2023
633dbf4
pragma: no cover
kwist-sgr Jul 22, 2023
8720ff2
Get rid of the `# no cover`
kwist-sgr Jul 24, 2023
f937b85
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 24, 2023
53ecab3
simplify a bit
kwist-sgr Jul 24, 2023
c2c6d7e
Merge branch 'main' into main
hynek Jul 28, 2023
0c18eca
Update tests/test_funcs.py
hynek Jul 28, 2023
10e806f
Update tests/test_funcs.py
hynek Jul 28, 2023
79965e4
Update tests/test_funcs.py
hynek Jul 28, 2023
65e9442
Update tests/test_funcs.py
hynek Jul 28, 2023
86ffe2d
Update tests/test_funcs.py
hynek Jul 28, 2023
727eb92
Update tests/test_funcs.py
hynek Jul 28, 2023
5b63751
Update tests/test_funcs.py
hynek Jul 28, 2023
fd5287d
Update tests/test_funcs.py
hynek Jul 28, 2023
a868560
Update changelog.d/1165.change.md
hynek Jul 28, 2023
d47cf1b
Escape patterns
hynek Jul 29, 2023
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
1 change: 1 addition & 0 deletions changelog.d/1165.change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed serialization of namedtuple fields using `attrs.asdict/astuple()` with `retain_collection_types=True`.
66 changes: 38 additions & 28 deletions src/attr/_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,19 +72,25 @@ def asdict(
)
elif isinstance(v, (tuple, list, set, frozenset)):
cf = v.__class__ if retain_collection_types is True else list
rv[a.name] = cf(
[
_asdict_anything(
i,
is_key=False,
filter=filter,
dict_factory=dict_factory,
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
for i in v
]
)
items = [
_asdict_anything(
i,
is_key=False,
filter=filter,
dict_factory=dict_factory,
retain_collection_types=retain_collection_types,
value_serializer=value_serializer,
)
for i in v
]
try:
rv[a.name] = cf(items)
except TypeError:
if not issubclass(cf, tuple):
raise
# Workaround for TypeError: cf.__new__() missing 1 required
# positional argument (which appears, for a namedturle)
rv[a.name] = cf(*items)
hynek marked this conversation as resolved.
Show resolved Hide resolved
elif isinstance(v, dict):
df = dict_factory
rv[a.name] = df(
Expand Down Expand Up @@ -241,22 +247,26 @@ def astuple(
)
elif isinstance(v, (tuple, list, set, frozenset)):
cf = v.__class__ if retain is True else list
rv.append(
cf(
[
astuple(
j,
recurse=True,
filter=filter,
tuple_factory=tuple_factory,
retain_collection_types=retain,
)
if has(j.__class__)
else j
for j in v
]
items = [
astuple(
j,
recurse=True,
filter=filter,
tuple_factory=tuple_factory,
retain_collection_types=retain,
)
)
if has(j.__class__)
else j
for j in v
]
try:
rv.append(cf(items))
except TypeError:
if not issubclass(cf, tuple):
raise
# Workaround for TypeError: cf.__new__() missing 1 required
# positional argument (which appears, for a namedturle)
rv.append(cf(*items))
elif isinstance(v, dict):
df = v.__class__ if retain is True else dict
rv.append(
Expand Down
95 changes: 94 additions & 1 deletion tests/test_funcs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
Tests for `attr._funcs`.
"""

import re

from collections import OrderedDict
from typing import Generic, TypeVar
from typing import Generic, NamedTuple, TypeVar

import pytest

Expand Down Expand Up @@ -232,6 +233,52 @@ class A:

assert {"a": {(1,): 1}} == attr.asdict(instance)

def test_named_tuple_retain_type(self):
"""
Namedtuples can be serialized if retain_collection_types is True.

See #1164
"""

class Coordinates(NamedTuple):
lat: float
lon: float

@attr.s
class A:
coords: Coordinates = attr.ib()

instance = A(Coordinates(50.419019, 30.516225))

assert {"coords": Coordinates(50.419019, 30.516225)} == attr.asdict(
instance, retain_collection_types=True
)

def test_type_error_with_retain_type(self):
"""
Serialization that fails with TypeError leaves the error through if
they're not tuples.

See #1164
"""

message = "__new__() missing 1 required positional argument (asdict)"

class Coordinates(list):
def __init__(self, first, *rest):
if isinstance(first, list):
raise TypeError(message)
super().__init__([first, *rest])

@attr.s
class A:
coords: Coordinates = attr.ib()

instance = A(Coordinates(50.419019, 30.516225))

with pytest.raises(TypeError, match=re.escape(message)):
attr.asdict(instance, retain_collection_types=True)


class TestAsTuple:
"""
Expand Down Expand Up @@ -390,6 +437,52 @@ def test_sets_no_retain(self, C, set_type):

assert (1, [1, 2, 3]) == d

def test_named_tuple_retain_type(self):
"""
Namedtuples can be serialized if retain_collection_types is True.

See #1164
"""

class Coordinates(NamedTuple):
lat: float
lon: float

@attr.s
class A:
coords: Coordinates = attr.ib()

instance = A(Coordinates(50.419019, 30.516225))

assert (Coordinates(50.419019, 30.516225),) == attr.astuple(
instance, retain_collection_types=True
)

def test_type_error_with_retain_type(self):
"""
Serialization that fails with TypeError leaves the error through if
they're not tuples.

See #1164
"""

message = "__new__() missing 1 required positional argument (astuple)"

class Coordinates(list):
def __init__(self, first, *rest):
if isinstance(first, list):
raise TypeError(message)
super().__init__([first, *rest])

@attr.s
class A:
coords: Coordinates = attr.ib()

instance = A(Coordinates(50.419019, 30.516225))

with pytest.raises(TypeError, match=re.escape(message)):
attr.astuple(instance, retain_collection_types=True)


class TestHas:
"""
Expand Down