From 2a4c97a5795923e845257be49eb881df7de5aae8 Mon Sep 17 00:00:00 2001 From: uttam12331 Date: Wed, 5 Aug 2026 01:07:00 +0530 Subject: [PATCH 1/2] Unpack the single type arg for Counter keys when unstructuring `mapping_unstructure_factory`'s single-type-arg branch ("Probably a Counter") assigned the whole `args` tuple to `key_arg`, then passed it to `get_unstructure_hook`, which expects a single type. As a result the key hook was resolved for `(KeyType,)` (falling back to identity) instead of `KeyType`, so Counter keys that need real unstructuring were left unchanged. Unpack `args[0]`, matching the structure factory's `(key_type,) = args`, and add a regression test. --- src/cattrs/gen/__init__.py | 2 +- tests/test_unstructure_collections.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/cattrs/gen/__init__.py b/src/cattrs/gen/__init__.py index b7873ab4..f558b1f6 100644 --- a/src/cattrs/gen/__init__.py +++ b/src/cattrs/gen/__init__.py @@ -1029,7 +1029,7 @@ def mapping_unstructure_factory( key_arg, val_arg = args else: # Probably a Counter - key_arg, val_arg = args, Any + key_arg, val_arg = args[0], Any # We can do the dispatch here and now. kh = key_handler or converter.get_unstructure_hook(key_arg, cache_result=False) if kh == identity: diff --git a/tests/test_unstructure_collections.py b/tests/test_unstructure_collections.py index 6654c889..03bd2431 100644 --- a/tests/test_unstructure_collections.py +++ b/tests/test_unstructure_collections.py @@ -148,3 +148,23 @@ def test_collection_unstructure_override_mapping(): assert c.unstructure({1: 2}) == Map({1: 2}) assert c.unstructure({1: 2}, unstructure_as=MutableMapping[int, int]) == Map({1: 2}) assert c.unstructure({1: 2}, unstructure_as=Mapping[int, int]) == Map({1: 2}) + + +def test_counter_unstructure_applies_key_hook(): + """Counter keys must be unstructured with the key type's own hook. + + Regression: the single-type-arg ("Probably a Counter") branch assigned the + whole ``args`` tuple to ``key_arg``, so the key hook was resolved for + ``(KeyType,)`` (falling back to identity) instead of ``KeyType``, leaving + keys un-unstructured. + """ + + @define(frozen=True) + class Key: + v: int + + c = Converter() + c.register_unstructure_hook(Key, lambda k: k.v) + + result = c.unstructure(Counter({Key(1): 5, Key(2): 3}), unstructure_as=Counter[Key]) + assert result == {1: 5, 2: 3} From cca9472641ad9a57d90ae70094b8324177aa665d Mon Sep 17 00:00:00 2001 From: uttam12331 Date: Wed, 5 Aug 2026 01:07:46 +0530 Subject: [PATCH 2/2] Add HISTORY entry for Counter key unstructure fix (#768) --- HISTORY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index 07b1c970..2120900a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -13,6 +13,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python ## NEXT (UNRELEASED) +- Fix `Counter` keys not being unstructured with the key type's own hook; the single-type-arg branch passed the whole type-args tuple to the key hook lookup instead of the key type. + ([#768](https://github.com/python-attrs/cattrs/pull/768)) - Fix `create_default_dis_func ` (aka `create_uniq_field_dis_func`) failing to disambiguate valid unions depending on the order of the member classes; unique fields are now resolved iteratively to a fixpoint. ([#230](https://github.com/python-attrs/cattrs/issues/230) [#765](https://github.com/python-attrs/cattrs/pull/765)) - Support more recursive types on 3.14+ with specialized factories for [`annotationlib.ForwardRef`](https://docs.python.org/3/library/annotationlib.html#annotationlib.ForwardRef).