diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6b7179e3..de772e78 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,6 +38,7 @@ jobs: name: coverage-data-${{ matrix.python-version }} path: .coverage.* if-no-files-found: ignore + include-hidden-files: true coverage: name: "Combine & check coverage." diff --git a/HISTORY.md b/HISTORY.md index 78e261df..23682343 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -9,6 +9,11 @@ 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.1.1 (UNRELEASED) + +- 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/README.md b/README.md index acec772e..cb30014e 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ _cattrs_ works best with [_attrs_](https://www.attrs.org/) classes, and [datacla C(a=1, b=['x', 'y']) >>> unstructure(instance) {'a': 1, 'b': ['x', 'y']} - ``` diff --git a/src/cattrs/converters.py b/src/cattrs/converters.py index 8d283b30..2759ee7a 100644 --- a/src/cattrs/converters.py +++ b/src/cattrs/converters.py @@ -102,17 +102,35 @@ ) # The Extended factory also takes a converter. -ExtendedUnstructureHookFactory = TypeVar( - "ExtendedUnstructureHookFactory", - bound=Callable[[TargetType, "BaseConverter"], UnstructureHook], +ExtendedUnstructureHookFactory: TypeAlias = Callable[[TargetType, T], UnstructureHook] + +# This typevar for the BaseConverter. +AnyUnstructureHookFactoryBase = TypeVar( + "AnyUnstructureHookFactoryBase", + bound="HookFactory[UnstructureHook] | ExtendedUnstructureHookFactory[BaseConverter]", +) + +# This typevar for the Converter. +AnyUnstructureHookFactory = TypeVar( + "AnyUnstructureHookFactory", + bound="HookFactory[UnstructureHook] | ExtendedUnstructureHookFactory[Converter]", ) StructureHookFactory = TypeVar("StructureHookFactory", bound=HookFactory[StructureHook]) # The Extended factory also takes a converter. -ExtendedStructureHookFactory = TypeVar( - "ExtendedStructureHookFactory", - bound=Callable[[TargetType, "BaseConverter"], StructureHook], +ExtendedStructureHookFactory: TypeAlias = Callable[[TargetType, T], StructureHook] + +# This typevar for the BaseConverter. +AnyStructureHookFactoryBase = TypeVar( + "AnyStructureHookFactoryBase", + bound="HookFactory[StructureHook] | ExtendedStructureHookFactory[BaseConverter]", +) + +# This typevar for the Converter. +AnyStructureHookFactory = TypeVar( + "AnyStructureHookFactory", + bound="HookFactory[StructureHook] | ExtendedStructureHookFactory[Converter]", ) @@ -341,12 +359,7 @@ def register_unstructure_hook_func( @overload def register_unstructure_hook_factory( self, predicate: Predicate - ) -> Callable[[UnstructureHookFactory], UnstructureHookFactory]: ... - - @overload - def register_unstructure_hook_factory( - self, predicate: Predicate - ) -> Callable[[ExtendedUnstructureHookFactory], ExtendedUnstructureHookFactory]: ... + ) -> Callable[[AnyUnstructureHookFactoryBase], AnyUnstructureHookFactoryBase]: ... @overload def register_unstructure_hook_factory( @@ -355,8 +368,10 @@ def register_unstructure_hook_factory( @overload def register_unstructure_hook_factory( - self, predicate: Predicate, factory: ExtendedUnstructureHookFactory - ) -> ExtendedUnstructureHookFactory: ... + self, + predicate: Predicate, + factory: ExtendedUnstructureHookFactory[BaseConverter], + ) -> ExtendedUnstructureHookFactory[BaseConverter]: ... def register_unstructure_hook_factory(self, predicate, factory=None): """ @@ -478,12 +493,7 @@ def register_structure_hook_func( @overload def register_structure_hook_factory( self, predicate: Predicate - ) -> Callable[[StructureHookFactory, StructureHookFactory]]: ... - - @overload - def register_structure_hook_factory( - self, predicate: Predicate - ) -> Callable[[ExtendedStructureHookFactory, ExtendedStructureHookFactory]]: ... + ) -> Callable[[AnyStructureHookFactoryBase], AnyStructureHookFactoryBase]: ... @overload def register_structure_hook_factory( @@ -492,8 +502,8 @@ def register_structure_hook_factory( @overload def register_structure_hook_factory( - self, predicate: Predicate, factory: ExtendedStructureHookFactory - ) -> ExtendedStructureHookFactory: ... + self, predicate: Predicate, factory: ExtendedStructureHookFactory[BaseConverter] + ) -> ExtendedStructureHookFactory[BaseConverter]: ... def register_structure_hook_factory(self, predicate, factory=None): """ @@ -1159,6 +1169,44 @@ def __init__( self._struct_copy_skip = self._structure_func.get_num_fns() self._unstruct_copy_skip = self._unstructure_func.get_num_fns() + @overload + def register_unstructure_hook_factory( + self, predicate: Predicate + ) -> Callable[[AnyUnstructureHookFactory], AnyUnstructureHookFactory]: ... + + @overload + def register_unstructure_hook_factory( + self, predicate: Predicate, factory: UnstructureHookFactory + ) -> UnstructureHookFactory: ... + + @overload + def register_unstructure_hook_factory( + self, predicate: Predicate, factory: ExtendedUnstructureHookFactory[Converter] + ) -> ExtendedUnstructureHookFactory[Converter]: ... + + def register_unstructure_hook_factory(self, predicate, factory=None): + # This dummy wrapper is required due to how `@overload` works. + return super().register_unstructure_hook_factory(predicate, factory) + + @overload + def register_structure_hook_factory( + self, predicate: Predicate + ) -> Callable[[AnyStructureHookFactory], AnyStructureHookFactory]: ... + + @overload + def register_structure_hook_factory( + self, predicate: Predicate, factory: StructureHookFactory + ) -> StructureHookFactory: ... + + @overload + def register_structure_hook_factory( + self, predicate: Predicate, factory: ExtendedStructureHookFactory[Converter] + ) -> ExtendedStructureHookFactory[Converter]: ... + + def register_structure_hook_factory(self, predicate, factory=None): + # This dummy wrapper is required due to how `@overload` works. + return super().register_structure_hook_factory(predicate, factory) + def get_structure_newtype(self, type: type[T]) -> Callable[[Any, Any], T]: base = get_newtype_base(type) handler = self.get_structure_hook(base)