Skip to content

Commit e536258

Browse files
committed
Raise errors for missing structure handlers more eagerly (#577)
* Raise errors for missing structure handlers more eagerly
1 parent 96ed9a1 commit e536258

File tree

6 files changed

+64
-4
lines changed

6 files changed

+64
-4
lines changed

HISTORY.md

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ The third number is for emergencies when we need to start branches for older rel
99

1010
Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md).
1111

12+
13+
## 24.2.0 (UNRELEASED)
14+
15+
- **Potentially breaking**: The converters raise {class}`StructureHandlerNotFoundError` more eagerly (on hook creation, instead of on hook use).
16+
This helps surfacing problems with missing hooks sooner.
17+
See [Migrations](https://catt.rs/latest/migrations.html#the-default-structure-hook-fallback-factory) for steps to restore legacy behavior.
18+
([#577](https://github.com/python-attrs/cattrs/pull/577))
19+
- Add a [Migrations](https://catt.rs/latest/migrations.html) page, with instructions on migrating changed behavior for each version.
20+
([#577](https://github.com/python-attrs/cattrs/pull/577))
21+
1222
## 24.1.2 (2024-09-22)
1323

1424
- Fix {meth}`BaseConverter.register_structure_hook` and {meth}`BaseConverter.register_unstructure_hook` type hints.

docs/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ validation
4747
preconf
4848
unions
4949
usage
50+
migrations
5051
indepth
5152
```
5253

docs/migrations.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Migrations
2+
3+
_cattrs_ sometimes changes in backwards-incompatible ways.
4+
This page contains guidance for changes and workarounds for restoring legacy behavior.
5+
6+
## 24.2.0
7+
8+
### The default structure hook fallback factory
9+
10+
The default structure hook fallback factory was changed to more eagerly raise errors for missing hooks.
11+
12+
The old behavior can be restored by explicitly passing in the old hook fallback factory when instantiating the converter.
13+
14+
15+
```python
16+
>>> from cattrs.fns import raise_error
17+
18+
>>> c = Converter(structure_fallback_factory=lambda _: raise_error)
19+
# Or
20+
>>> c = BaseConverter(structure_fallback_factory=lambda _: raise_error)
21+
```

src/cattrs/converters.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,9 @@ def __init__(
182182
prefer_attrib_converters: bool = False,
183183
detailed_validation: bool = True,
184184
unstructure_fallback_factory: HookFactory[UnstructureHook] = lambda _: identity,
185-
structure_fallback_factory: HookFactory[StructureHook] = lambda _: raise_error,
185+
structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error(
186+
None, t
187+
),
186188
) -> None:
187189
"""
188190
:param detailed_validation: Whether to use a slightly slower mode for detailed
@@ -194,6 +196,9 @@ def __init__(
194196
195197
.. versionadded:: 23.2.0 *unstructure_fallback_factory*
196198
.. versionadded:: 23.2.0 *structure_fallback_factory*
199+
.. versionchanged:: 24.2.0
200+
The default `structure_fallback_factory` now raises errors for missing handlers
201+
more eagerly, surfacing problems earlier.
197202
"""
198203
unstruct_strat = UnstructureStrategy(unstruct_strat)
199204
self._prefer_attrib_converters = prefer_attrib_converters
@@ -1045,7 +1050,9 @@ def __init__(
10451050
prefer_attrib_converters: bool = False,
10461051
detailed_validation: bool = True,
10471052
unstructure_fallback_factory: HookFactory[UnstructureHook] = lambda _: identity,
1048-
structure_fallback_factory: HookFactory[StructureHook] = lambda _: raise_error,
1053+
structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error(
1054+
None, t
1055+
),
10491056
):
10501057
"""
10511058
:param detailed_validation: Whether to use a slightly slower mode for detailed
@@ -1057,6 +1064,9 @@ def __init__(
10571064
10581065
.. versionadded:: 23.2.0 *unstructure_fallback_factory*
10591066
.. versionadded:: 23.2.0 *structure_fallback_factory*
1067+
.. versionchanged:: 24.2.0
1068+
The default `structure_fallback_factory` now raises errors for missing handlers
1069+
more eagerly, surfacing problems earlier.
10601070
"""
10611071
super().__init__(
10621072
dict_factory=dict_factory,

src/cattrs/gen/_shared.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from .._compat import is_bare_final
88
from ..dispatch import StructureHook
9+
from ..errors import StructureHandlerNotFoundError
910
from ..fns import raise_error
1011

1112
if TYPE_CHECKING:
@@ -27,9 +28,14 @@ def find_structure_handler(
2728
elif (
2829
a.converter is not None and not prefer_attrs_converters and type is not None
2930
):
30-
handler = c.get_structure_hook(type, cache_result=False)
31-
if handler == raise_error:
31+
try:
32+
handler = c.get_structure_hook(type, cache_result=False)
33+
except StructureHandlerNotFoundError:
3234
handler = None
35+
else:
36+
# The legacy way, should still work.
37+
if handler == raise_error:
38+
handler = None
3339
elif type is not None:
3440
if (
3541
is_bare_final(type)

tests/test_converter.py

+12
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,18 @@ class Outer:
721721
assert structured == Outer(Inner(2), [Inner(2)], Inner(2))
722722

723723

724+
def test_default_structure_fallback(converter_cls: Type[BaseConverter]):
725+
"""The default structure fallback hook factory eagerly errors."""
726+
727+
class Test:
728+
"""Unsupported by default."""
729+
730+
c = converter_cls()
731+
732+
with pytest.raises(StructureHandlerNotFoundError):
733+
c.get_structure_hook(Test)
734+
735+
724736
def test_unstructure_fallbacks(converter_cls: Type[BaseConverter]):
725737
"""Unstructure fallback factories work."""
726738

0 commit comments

Comments
 (0)