Skip to content

Commit 58c7ba6

Browse files
authored
register_un/structure_hook: support type aliases (#647)
* register_un/structure_hook: support type aliases * Docs
1 parent 0b6586a commit 58c7ba6

File tree

7 files changed

+99
-152
lines changed

7 files changed

+99
-152
lines changed

HISTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
2222
- [`typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) is now supported in _attrs_ classes, dataclasses, TypedDicts and the dict NamedTuple factories.
2323
See [`typing.Self`](https://catt.rs/en/latest/defaulthooks.html#typing-self) for details.
2424
([#299](https://github.com/python-attrs/cattrs/issues/299) [#627](https://github.com/python-attrs/cattrs/pull/627))
25+
- PEP 695 type aliases can now be used with {meth}`Converter.register_structure_hook` and {meth}`Converter.register_unstructure_hook`.
26+
Previously, they required the use of {meth}`Converter.register_structure_hook_func` (which is still supported).
2527
- Expose {func}`cattrs.cols.mapping_unstructure_factory` through {mod}`cattrs.cols`.
2628
- Some `defaultdicts` are now [supported by default](https://catt.rs/en/latest/defaulthooks.html#defaultdicts), and
2729
{func}`cattrs.cols.is_defaultdict` and {func}`cattrs.cols.defaultdict_structure_factory` are exposed through {mod}`cattrs.cols`.

docs/basics.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
```
44

55
All _cattrs_ functionality is exposed through a {class}`cattrs.Converter` object.
6-
A global converter is provided for convenience as {data}`cattrs.global_converter` but more complex customizations should be performed on private instances, any number of which can be made.
6+
A global converter is provided for convenience as {data}`cattrs.global_converter`
7+
but more complex customizations should be performed on private instances, any number of which can be made.
78

89

910
## Converters and Hooks

docs/customizing.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ the hook for `int`, which should be already present.
1919
## Custom (Un-)structuring Hooks
2020

2121
You can write your own structuring and unstructuring functions and register them for types using {meth}`Converter.register_structure_hook() <cattrs.BaseConverter.register_structure_hook>` and {meth}`Converter.register_unstructure_hook() <cattrs.BaseConverter.register_unstructure_hook>`.
22-
This approach is the most flexible but also requires the most amount of boilerplate.
2322

2423
{meth}`register_structure_hook() <cattrs.BaseConverter.register_structure_hook>` and {meth}`register_unstructure_hook() <cattrs.BaseConverter.register_unstructure_hook>` use a Python [_singledispatch_](https://docs.python.org/3/library/functools.html#functools.singledispatch) under the hood.
2524
_singledispatch_ is powerful and fast but comes with some limitations; namely that it performs checks using `issubclass()` which doesn't work with many Python types.
@@ -30,10 +29,15 @@ Some examples of this are:
3029
- generics (`MyClass[int]` is not a _subclass_ of `MyClass`)
3130
- protocols, unless they are `runtime_checkable`
3231
- various modifiers, such as `Final` and `NotRequired`
33-
- newtypes and 3.12 type aliases
3432
- `typing.Annotated`
3533

36-
... and many others. In these cases, predicate functions should be used instead.
34+
... and many others. In these cases, [predicate hooks](#predicate-hooks) should be used instead.
35+
36+
Even though unions, [newtypes](https://docs.python.org/3/library/typing.html#newtype)
37+
and [modern type aliases](https://docs.python.org/3/library/typing.html#type-aliases)
38+
do not work with _singledispatch_,
39+
these methods have special support for these type forms and can be used with them.
40+
Instead of using _singledispatch_, predicate hooks will automatically be used instead.
3741

3842
### Use as Decorators
3943

pdm.lock

Lines changed: 67 additions & 147 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ docs = [
1919
"furo>=2024.1.29",
2020
"sphinx-copybutton>=0.5.2",
2121
"myst-parser>=1.0.0",
22-
"pendulum>=2.1.2",
22+
"pendulum>=3.1.0",
2323
"sphinx-autobuild",
2424
]
2525
bench = [

src/cattrs/converters.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,8 @@ def register_unstructure_hook(
339339
340340
.. versionchanged:: 24.1.0
341341
This method may now be used as a decorator.
342+
.. versionchanged:: 25.1.0
343+
Modern type aliases are now supported.
342344
"""
343345
if func is None:
344346
# Autodetecting decorator.
@@ -353,6 +355,8 @@ def register_unstructure_hook(
353355
resolve_types(cls)
354356
if is_union_type(cls):
355357
self._unstructure_func.register_func_list([(lambda t: t == cls, func)])
358+
elif is_type_alias(cls):
359+
self._unstructure_func.register_func_list([(lambda t: t is cls, func)])
356360
elif get_newtype_base(cls) is not None:
357361
# This is a newtype, so we handle it specially.
358362
self._unstructure_func.register_func_list([(lambda t: t is cls, func)])
@@ -475,6 +479,8 @@ def register_structure_hook(
475479
476480
.. versionchanged:: 24.1.0
477481
This method may now be used as a decorator.
482+
.. versionchanged:: 25.1.0
483+
Modern type aliases are now supported.
478484
"""
479485
if func is None:
480486
# The autodetecting decorator.
@@ -488,6 +494,9 @@ def register_structure_hook(
488494
if is_union_type(cl):
489495
self._union_struct_registry[cl] = func
490496
self._structure_func.clear_cache()
497+
elif is_type_alias(cl):
498+
# Type aliases are special-cased.
499+
self._structure_func.register_func_list([(lambda t: t is cl, func)])
491500
elif get_newtype_base(cl) is not None:
492501
# This is a newtype, so we handle it specially.
493502
self._structure_func.register_func_list([(lambda t: t is cl, func)])

tests/test_generics_695.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,17 @@ def test_type_aliases(converter: BaseConverter):
6464
assert converter.unstructure(100, my_other_int) == 80
6565

6666

67+
def test_type_aliases_simple_hooks(converter: BaseConverter):
68+
"""PEP 695 type aliases work with `register_un/structure_hook`."""
69+
type my_other_int = int
70+
71+
converter.register_structure_hook(my_other_int, lambda v, _: v + 10)
72+
converter.register_unstructure_hook(my_other_int, lambda v: v - 20)
73+
74+
assert converter.structure(1, my_other_int) == 11
75+
assert converter.unstructure(100, my_other_int) == 80
76+
77+
6778
def test_type_aliases_overwrite_base_hooks(converter: BaseConverter):
6879
"""Overwriting base hooks should affect type aliases."""
6980
converter.register_structure_hook(int, lambda v, _: v + 10)

0 commit comments

Comments
 (0)