diff --git a/HISTORY.md b/HISTORY.md index 33666930..b5fce2bd 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -9,6 +9,16 @@ The third number is for emergencies when we need to start branches for older rel Our backwards-compatibility policy can be found [here](https://github.com/python-attrs/cattrs/blob/main/.github/SECURITY.md). + +## 24.2.0 (UNRELEASED) + +- **Potentially breaking**: The converters raise {class}`StructureHandlerNotFoundError` more eagerly (on hook creation, instead of on hook use). + This helps surfacing problems with missing hooks sooner. + See [Migrations](https://catt.rs/latest/migrations.html#the-default-structure-hook-fallback-factory) for steps to restore legacy behavior. + ([#577](https://github.com/python-attrs/cattrs/pull/577)) +- Add a [Migrations](https://catt.rs/latest/migrations.html) page, with instructions on migrating changed behavior for each version. + ([#577](https://github.com/python-attrs/cattrs/pull/577)) + ## 24.1.2 (2024-09-22) - Fix {meth}`BaseConverter.register_structure_hook` and {meth}`BaseConverter.register_unstructure_hook` type hints. diff --git a/docs/index.md b/docs/index.md index d8c2505b..e41634c7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,6 +47,7 @@ validation preconf unions usage +migrations indepth ``` diff --git a/docs/migrations.md b/docs/migrations.md new file mode 100644 index 00000000..aabe9bde --- /dev/null +++ b/docs/migrations.md @@ -0,0 +1,21 @@ +# Migrations + +_cattrs_ sometimes changes in backwards-incompatible ways. +This page contains guidance for changes and workarounds for restoring legacy behavior. + +## 24.2.0 + +### The default structure hook fallback factory + +The default structure hook fallback factory was changed to more eagerly raise errors for missing hooks. + +The old behavior can be restored by explicitly passing in the old hook fallback factory when instantiating the converter. + + +```python +>>> from cattrs.fns import raise_error + +>>> c = Converter(structure_fallback_factory=lambda _: raise_error) +# Or +>>> c = BaseConverter(structure_fallback_factory=lambda _: raise_error) +``` diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 1490ec26..4f291e7a 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -182,7 +182,9 @@ def __init__( prefer_attrib_converters: bool = False, detailed_validation: bool = True, unstructure_fallback_factory: HookFactory[UnstructureHook] = lambda _: identity, - structure_fallback_factory: HookFactory[StructureHook] = lambda _: raise_error, + structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error( + None, t + ), ) -> None: """ :param detailed_validation: Whether to use a slightly slower mode for detailed @@ -194,6 +196,9 @@ def __init__( .. versionadded:: 23.2.0 *unstructure_fallback_factory* .. versionadded:: 23.2.0 *structure_fallback_factory* + .. versionchanged:: 24.2.0 + The default `structure_fallback_factory` now raises errors for missing handlers + more eagerly, surfacing problems earlier. """ unstruct_strat = UnstructureStrategy(unstruct_strat) self._prefer_attrib_converters = prefer_attrib_converters @@ -1045,7 +1050,9 @@ def __init__( prefer_attrib_converters: bool = False, detailed_validation: bool = True, unstructure_fallback_factory: HookFactory[UnstructureHook] = lambda _: identity, - structure_fallback_factory: HookFactory[StructureHook] = lambda _: raise_error, + structure_fallback_factory: HookFactory[StructureHook] = lambda t: raise_error( + None, t + ), ): """ :param detailed_validation: Whether to use a slightly slower mode for detailed @@ -1057,6 +1064,9 @@ def __init__( .. versionadded:: 23.2.0 *unstructure_fallback_factory* .. versionadded:: 23.2.0 *structure_fallback_factory* + .. versionchanged:: 24.2.0 + The default `structure_fallback_factory` now raises errors for missing handlers + more eagerly, surfacing problems earlier. """ super().__init__( dict_factory=dict_factory, diff --git a/src/cattrs/gen/_shared.py b/src/cattrs/gen/_shared.py index 4e631437..904c7744 100644 --- a/src/cattrs/gen/_shared.py +++ b/src/cattrs/gen/_shared.py @@ -6,6 +6,7 @@ from .._compat import is_bare_final from ..dispatch import StructureHook +from ..errors import StructureHandlerNotFoundError from ..fns import raise_error if TYPE_CHECKING: @@ -27,9 +28,14 @@ def find_structure_handler( elif ( a.converter is not None and not prefer_attrs_converters and type is not None ): - handler = c.get_structure_hook(type, cache_result=False) - if handler == raise_error: + try: + handler = c.get_structure_hook(type, cache_result=False) + except StructureHandlerNotFoundError: handler = None + else: + # The legacy way, should still work. + if handler == raise_error: + handler = None elif type is not None: if ( is_bare_final(type) diff --git a/tests/test_converter.py b/tests/test_converter.py index 47e70edc..92c9bbb3 100644 --- a/tests/test_converter.py +++ b/tests/test_converter.py @@ -721,6 +721,18 @@ class Outer: assert structured == Outer(Inner(2), [Inner(2)], Inner(2)) +def test_default_structure_fallback(converter_cls: Type[BaseConverter]): + """The default structure fallback hook factory eagerly errors.""" + + class Test: + """Unsupported by default.""" + + c = converter_cls() + + with pytest.raises(StructureHandlerNotFoundError): + c.get_structure_hook(Test) + + def test_unstructure_fallbacks(converter_cls: Type[BaseConverter]): """Unstructure fallback factories work."""