From 9286dd8d7af99d03a8df8a1503b1534d8066d145 Mon Sep 17 00:00:00 2001 From: abmantis Date: Fri, 8 May 2026 17:47:23 +0100 Subject: [PATCH] Fix deprecated_class to work with inheritance --- homeassistant/helpers/deprecation.py | 35 +++++++++++++++++++++------- tests/helpers/test_deprecation.py | 7 ++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 8f4dfca21102cf..0be8dfeb2b1546 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -6,7 +6,7 @@ import functools import inspect import logging -from typing import Any, NamedTuple +from typing import Any, NamedTuple, cast def deprecated_substitute[_ObjectT: object]( @@ -86,27 +86,44 @@ def get_deprecated( return config.get(new_name, default) -def deprecated_class[**_P, _R]( +def deprecated_class[_T]( replacement: str, *, breaks_in_ha_version: str | None = None -) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: +) -> Callable[[type[_T]], type[_T]]: """Mark class as deprecated and provide a replacement class to be used instead. If the deprecated function was called from a custom integration, ask the user to report an issue. """ - def deprecated_decorator(cls: Callable[_P, _R]) -> Callable[_P, _R]: + def deprecated_decorator(cls: type[_T]) -> type[_T]: """Decorate class as deprecated.""" + base_meta = type(cls) - @functools.wraps(cls) - def deprecated_cls(*args: _P.args, **kwargs: _P.kwargs) -> _R: - """Wrap for the original class.""" + def __call__(self: type[Any], *args: Any, **kwargs: Any) -> Any: _print_deprecation_warning( cls, replacement, "class", "instantiated", breaks_in_ha_version ) - return cls(*args, **kwargs) + return base_meta.__call__(self, *args, **kwargs) + + deprecated_meta = type( + f"Deprecated{base_meta.__name__}", + (base_meta,), + {"__call__": __call__}, + ) + + deprecated_cls = deprecated_meta( + cls.__name__, + (cls,), + { + "__module__": cls.__module__, + "__qualname__": cls.__qualname__, + "__doc__": cls.__doc__, + "__slots__": (), + "__wrapped__": cls, + }, + ) - return deprecated_cls + return cast(type[_T], deprecated_cls) return deprecated_decorator diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index b77e7e1ef44f0a..825b688afbf203 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -121,6 +121,10 @@ class MockDeprecatedClass: """Mock class for deprecated testing.""" +class MockDeprecatedSubClass(MockDeprecatedClass): + """Mock subclass for deprecated testing.""" + + @patch("logging.getLogger") def test_deprecated_class(mock_get_logger) -> None: """Test deprecated class.""" @@ -131,6 +135,9 @@ def test_deprecated_class(mock_get_logger) -> None: assert mock_logger.warning.called assert len(mock_logger.warning.mock_calls) == 1 + MockDeprecatedSubClass() + assert len(mock_logger.warning.mock_calls) == 2 + @pytest.mark.parametrize( ("breaks_in_ha_version", "extra_msg"),