From 4e1f8f3d4b9586bcae1c0ff1ab3757cfcb472298 Mon Sep 17 00:00:00 2001 From: Johnny Kao <19386515+Johnny-Kao@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:05:38 +0900 Subject: [PATCH] perf: skip redundant atomic dict attrs detection in astuple --- changelog.d/0.change.md | 1 + src/attr/_funcs.py | 6 ++++-- tests/test_funcs.py | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 changelog.d/0.change.md diff --git a/changelog.d/0.change.md b/changelog.d/0.change.md new file mode 100644 index 000000000..1fc6c3ab9 --- /dev/null +++ b/changelog.d/0.change.md @@ -0,0 +1 @@ +Improve `attrs.astuple()` performance for dictionary keys and values of exact atomic types by avoiding redundant attrs-class detection. diff --git a/src/attr/_funcs.py b/src/attr/_funcs.py index c88c175a1..9dd9d1e17 100644 --- a/src/attr/_funcs.py +++ b/src/attr/_funcs.py @@ -325,7 +325,8 @@ def astuple( tuple_factory=tuple_factory, retain_collection_types=retain, ) - if has(kk.__class__) + if type(kk) not in _ATOMIC_TYPES + and has(kk.__class__) else kk ), ( @@ -334,7 +335,8 @@ def astuple( tuple_factory=tuple_factory, retain_collection_types=retain, ) - if has(vv.__class__) + if type(vv) not in _ATOMIC_TYPES + and has(vv.__class__) else vv ), ) diff --git a/tests/test_funcs.py b/tests/test_funcs.py index f35a9a564..7bcf64adb 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_dict_atomic_and_subclass(self, C, retain, container): + """Exact atomic keys/values and int subclasses retain behavior.""" + + class IntSub(int): + pass + + mapping = container({"one": 1, "two": IntSub(2), "three": C(3, 4)}) + result = astuple(C(mapping, 9), retain_collection_types=retain) + assert result == ({"one": 1, "two": 2, "three": (3, 4)}, 9) + assert type(result[0]) is (container if retain else dict) + assert type(result[0]["two"]) is IntSub + + @pytest.mark.parametrize("retain", [False, True]) + @pytest.mark.parametrize("container", MAPPING_TYPES) + def test_dict_atomic_keys(self, C, retain, container): + """Dictionary atomic keys are preserved during conversion.""" + mapping = container({1: "one", 2: "two"}) + result = astuple(C(mapping, 9), retain_collection_types=retain) + assert result == (mapping, 9) + assert type(result[0]) is (container if retain else dict) + @given(container=st.sampled_from(SEQUENCE_TYPES)) def test_lists_tuples_retain_type(self, container, C): """