From 6f81e84fa7ae601d36b560086a1eaf418cc378d7 Mon Sep 17 00:00:00 2001 From: "s.kyon" <251157463+skyon0522-ai@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:29:12 +0900 Subject: [PATCH 1/3] Preserve astuple filters for dictionary keys and values --- changelog.d/astuple-mapping-filter.change.md | 1 + src/attr/_funcs.py | 4 ++++ tests/test_funcs.py | 23 ++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 changelog.d/astuple-mapping-filter.change.md diff --git a/changelog.d/astuple-mapping-filter.change.md b/changelog.d/astuple-mapping-filter.change.md new file mode 100644 index 000000000..7caf59646 --- /dev/null +++ b/changelog.d/astuple-mapping-filter.change.md @@ -0,0 +1 @@ +`attrs.astuple()` and `attr.astuple()` now apply the *filter* to attrs instances used as dictionary keys and values, consistently with other recursive conversions. diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index d213efc22..e7f7784b0 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -238,6 +238,8 @@ def astuple( Return the *attrs* attribute values of *inst* as a tuple. Optionally recurse into other *attrs*-decorated classes. + When recursing, *filter* also applies to *attrs* instances used as dictionary + keys and values. Args: inst: Instance of an *attrs*-decorated class. @@ -322,6 +324,7 @@ def astuple( ( astuple( kk, + filter=filter, tuple_factory=tuple_factory, retain_collection_types=retain, ) @@ -331,6 +334,7 @@ def astuple( ( astuple( vv, + filter=filter, tuple_factory=tuple_factory, retain_collection_types=retain, ) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index f35a9a564..9c60dcfab 100644 --- a/tests/test_funcs.py +++ b/tests/test_funcs.py @@ -409,6 +409,29 @@ def test_dicts(self, C, tuple_factory): assert tuple_factory([1, {"a": tuple_factory([4, 5])}]) == res assert isinstance(res, tuple_factory) + @pytest.mark.parametrize("retain", [False, True]) + @pytest.mark.parametrize("container", MAPPING_TYPES) + def test_dicts_filter(self, C, retain, container): + """ + Filters apply to attrs instances used as dictionary keys and values. + """ + + @attr.s(frozen=True) + class Key: + x = attr.ib() + y = attr.ib() + + inst = C(container({Key(1, 2): C(3, 4)}), 5) + + result = astuple( + inst, + filter=lambda a, v: a.name != "y", + retain_collection_types=retain, + ) + + assert ({(1,): (3,)},) == result + assert (container if retain else dict) is type(result[0]) + @given(container=st.sampled_from(SEQUENCE_TYPES)) def test_lists_tuples_retain_type(self, container, C): """ From 3eb74883d6a04ee3a9198cef3291f86b94f97efc Mon Sep 17 00:00:00 2001 From: "s.kyon" <251157463+skyon0522-ai@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:44:04 +0900 Subject: [PATCH 2/3] Document mapping filter behavior change --- src/attr/_funcs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index e7f7784b0..ffa7a7a86 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -270,6 +270,8 @@ def astuple( If *cls* is not an *attrs* class. .. versionadded:: 16.2.0 + .. versionchanged:: 26.2.0 + Apply *filter* to attrs instances in dictionary keys and values. """ attrs = fields(inst.__class__) rv = [] From 9dbbede1f14c1f60ad155b75a208d53fbce55f98 Mon Sep 17 00:00:00 2001 From: "s.kyon" <251157463+skyon0522-ai@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:55:26 +0900 Subject: [PATCH 3/3] Name changelog fragment for PR 1641 --- changelog.d/{astuple-mapping-filter.change.md => 1641.change.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{astuple-mapping-filter.change.md => 1641.change.md} (100%) diff --git a/changelog.d/astuple-mapping-filter.change.md b/changelog.d/1641.change.md similarity index 100% rename from changelog.d/astuple-mapping-filter.change.md rename to changelog.d/1641.change.md