From d826f854e7014e58ec0597fe2789dc6758d3cdfa Mon Sep 17 00:00:00 2001 From: Kadir Can Ozden <101993364+bysiber@users.noreply.github.com> Date: Fri, 20 Feb 2026 14:30:09 +0300 Subject: [PATCH 1/2] Return factory from hook factory decorator functions register_unstructure_hook_factory and register_structure_hook_factory can be used as decorators, but the inner decorator function doesn't return the factory. This means: @converter.register_structure_hook_factory(has) def my_factory(type): ... # my_factory is now None The type annotations declare these decorators should return the factory (Callable[[Factory], Factory]) and the non-decorator path already works correctly. This just adds the missing return statement. --- src/cattrs/converters.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index fd645cb4..0e6bed07 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -443,6 +443,7 @@ def decorator(factory): self._unstructure_func.register_func_list( [(predicate, factory, True)] ) + return factory return decorator @@ -581,6 +582,7 @@ def decorator(factory): self._structure_func.register_func_list( [(predicate, factory, True)] ) + return factory return decorator self._structure_func.register_func_list( From 4d7776350c3b5890a8a06d2ff0b8e3d5debe6de6 Mon Sep 17 00:00:00 2001 From: Tin Tvrtkovic Date: Sat, 13 Jun 2026 23:28:34 +0200 Subject: [PATCH 2/2] Add changelog entry and typing test Signed-off-by: Tin Tvrtkovic --- HISTORY.md | 2 ++ tests/test_typing_structure.md | 30 ++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/HISTORY.md b/HISTORY.md index dcb87542..b999f978 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -30,6 +30,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python ([#748](https://github.com/python-attrs/cattrs/pull/748)) - The [union passthrough strategy](https://catt.rs/en/stable/strategies.html#union-passthrough) now supports PEP 695 type aliases as union members. ([#753](https://github.com/python-attrs/cattrs/pull/753)) +- {meth}`BaseConverter.register_structure_hook_factory` and {meth}`BaseConverter.register_unstructure_hook_factory` now properly return the factory when used as decorators. + ([#724](https://github.com/python-attrs/cattrs/pull/724)) ## 26.1.0 (2026-02-18) diff --git a/tests/test_typing_structure.md b/tests/test_typing_structure.md index e0b75b73..50204856 100644 --- a/tests/test_typing_structure.md +++ b/tests/test_typing_structure.md @@ -102,3 +102,33 @@ converter = Converter() value: int = converter.structure("1", int) bad: str = converter.structure("1", int) # mypy-error: [assignment] ``` + +## Hook factory decorators preserve factory types + +```python +from collections.abc import Callable +from typing import Any + +from cattrs import Converter + + +converter = Converter() + + +def accepts_int(cl: Any) -> bool: + return cl is int + + +@converter.register_unstructure_hook_factory(accepts_int) +def unstructure_factory(cl: type[int]) -> Callable[[int], str]: + return str + + +@converter.register_structure_hook_factory(accepts_int) +def structure_factory(cl: type[int]) -> Callable[[str, type[int]], int]: + return lambda value, _: int(value) + + +reveal_type(unstructure_factory) # revealed: def (cl: type[int]) -> def (int) -> str +reveal_type(structure_factory) # revealed: def (cl: type[int]) -> def (str, type[int]) -> int +```