diff --git a/HISTORY.md b/HISTORY.md index c9e97e01..06f6ddd2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -9,11 +9,21 @@ 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.1 (2024-09-11) - Fix {meth}`BaseConverter.register_structure_hook_factory` and {meth}`BaseConverter.register_unstructure_hook_factory` type hints. ([#578](https://github.com/python-attrs/cattrs/issues/578) [#579](https://github.com/python-attrs/cattrs/pull/579)) + ## 24.1.0 (2024-08-28) - **Potentially breaking**: Unstructuring hooks for `typing.Any` are consistent now: values are unstructured using their runtime type. 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 2759ee7a..ef0ada9d 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -179,7 +179,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 @@ -191,6 +193,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 @@ -1041,7 +1046,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 @@ -1053,6 +1060,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."""