Skip to content

Commit

Permalink
Fix TypeError for `asdict(<class with namedtuple>, retain_collection_…
Browse files Browse the repository at this point in the history
…types=True)` (#1165)

* Fix TypeError for asdict with namedtuples and retain_collection_types=True

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add news fragment in `changelog.d`

* fix pre-commit interrogate checker

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* fix flake8 issue

* also fixed `astuple`

* Add `_is_namedtuple` function

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix SyntaxError for python 3.7

* use `issubclass(..., tuple)`

* use issubclass(cf, tuple) if case of TypeError

* pragma: no cover

* Get rid of the `# no cover`

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* simplify a bit

* Update tests/test_funcs.py

* Update tests/test_funcs.py

* Update tests/test_funcs.py

* Update tests/test_funcs.py

* Update tests/test_funcs.py

* Update tests/test_funcs.py

* Update tests/test_funcs.py

* Update tests/test_funcs.py

* Update changelog.d/1165.change.md

* Escape patterns

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Hynek Schlawack <[email protected]>
  • Loading branch information
3 people authored Jul 29, 2023
1 parent a37b556 commit 0bf0678
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 29 deletions.
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)
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

0 comments on commit 0bf0678

Please sign in to comment.