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). 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}