"""Test deprecation helpers.""" from enum import StrEnum import logging import sys from typing import Any from unittest.mock import MagicMock, Mock, patch import pytest from homeassistant.core import HomeAssistant from homeassistant.helpers.deprecation import ( DeprecatedAlias, DeprecatedConstant, DeprecatedConstantEnum, EnumWithDeprecatedMembers, check_if_deprecated_constant, deprecated_class, deprecated_function, deprecated_substitute, dir_with_deprecated_constants, get_deprecated, ) from homeassistant.helpers.frame import MissingIntegrationFrame from tests.common import MockModule, extract_stack_to_frame, mock_integration class MockBaseClassDeprecatedProperty: """Mock base class for deprecated testing.""" @property @deprecated_substitute("old_property") def new_property(self): """Test property to fetch.""" return "default_new" @patch("logging.getLogger") def test_deprecated_substitute_old_class(mock_get_logger) -> None: """Test deprecated class object.""" class MockDeprecatedClass(MockBaseClassDeprecatedProperty): """Mock deprecated class object.""" @property def old_property(self): """Test property to fetch.""" return "old" mock_logger = MagicMock() mock_get_logger.return_value = mock_logger mock_object = MockDeprecatedClass() assert mock_object.new_property == "old" assert mock_logger.warning.called assert len(mock_logger.warning.mock_calls) == 1 @patch("logging.getLogger") def test_deprecated_substitute_default_class(mock_get_logger) -> None: """Test deprecated class object.""" class MockDefaultClass(MockBaseClassDeprecatedProperty): """Mock updated class object.""" mock_logger = MagicMock() mock_get_logger.return_value = mock_logger mock_object = MockDefaultClass() assert mock_object.new_property == "default_new" assert not mock_logger.warning.called @patch("logging.getLogger") def test_deprecated_substitute_new_class(mock_get_logger) -> None: """Test deprecated class object.""" class MockUpdatedClass(MockBaseClassDeprecatedProperty): """Mock updated class object.""" @property def new_property(self): """Test property to fetch.""" return "new" mock_logger = MagicMock() mock_get_logger.return_value = mock_logger mock_object = MockUpdatedClass() assert mock_object.new_property == "new" assert not mock_logger.warning.called @patch("logging.getLogger") def test_config_get_deprecated_old(mock_get_logger) -> None: """Test deprecated config.""" mock_logger = MagicMock() mock_get_logger.return_value = mock_logger config = {"old_name": True} assert get_deprecated(config, "new_name", "old_name") is True assert mock_logger.warning.called assert len(mock_logger.warning.mock_calls) == 1 @patch("logging.getLogger") def test_config_get_deprecated_new(mock_get_logger) -> None: """Test deprecated config.""" mock_logger = MagicMock() mock_get_logger.return_value = mock_logger config = {"new_name": True} assert get_deprecated(config, "new_name", "old_name") is True assert not mock_logger.warning.called @deprecated_class("homeassistant.blah.NewClass") class MockDeprecatedClass: """Mock class for deprecated testing.""" @patch("logging.getLogger") def test_deprecated_class(mock_get_logger) -> None: """Test deprecated class.""" mock_logger = MagicMock() mock_get_logger.return_value = mock_logger MockDeprecatedClass() assert mock_logger.warning.called assert len(mock_logger.warning.mock_calls) == 1 @pytest.mark.parametrize( ("breaks_in_ha_version", "extra_msg"), [ (None, ""), ("2099.1", " which will be removed in HA Core 2099.1"), ], ) def test_deprecated_function( caplog: pytest.LogCaptureFixture, breaks_in_ha_version: str | None, extra_msg: str, ) -> None: """Test deprecated_function decorator. This tests the behavior when the calling integration is not known. """ @deprecated_function("new_function", breaks_in_ha_version=breaks_in_ha_version) def mock_deprecated_function(): pass mock_deprecated_function() assert ( f"mock_deprecated_function is a deprecated function{extra_msg}. " "Use new_function instead" ) in caplog.text @pytest.mark.parametrize( ("breaks_in_ha_version", "extra_msg"), [ (None, ""), ("2099.1", " which will be removed in HA Core 2099.1"), ], ) def test_deprecated_function_called_from_built_in_integration( caplog: pytest.LogCaptureFixture, breaks_in_ha_version: str | None, extra_msg: str, ) -> None: """Test deprecated_function decorator. This tests the behavior when the calling integration is built-in. """ @deprecated_function("new_function", breaks_in_ha_version=breaks_in_ha_version) def mock_deprecated_function(): pass with ( patch( "homeassistant.helpers.frame.linecache.getline", return_value="await session.close()", ), patch( "homeassistant.helpers.frame.get_current_frame", return_value=extract_stack_to_frame( [ Mock( filename="/home/paulus/homeassistant/core.py", lineno="23", line="do_something()", ), Mock( filename="/home/paulus/homeassistant/components/hue/light.py", lineno="23", line="await session.close()", ), Mock( filename="/home/paulus/aiohue/lights.py", lineno="2", line="something()", ), ] ), ), ): mock_deprecated_function() assert ( "mock_deprecated_function was called from hue, " f"this is a deprecated function{extra_msg}. " "Use new_function instead" ) in caplog.text @pytest.mark.parametrize( ("breaks_in_ha_version", "extra_msg"), [ (None, ""), ("2099.1", " which will be removed in HA Core 2099.1"), ], ) def test_deprecated_function_called_from_custom_integration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, breaks_in_ha_version: str | None, extra_msg: str, ) -> None: """Test deprecated_function decorator. This tests the behavior when the calling integration is custom. """ mock_integration(hass, MockModule("hue"), built_in=False) @deprecated_function("new_function", breaks_in_ha_version=breaks_in_ha_version) def mock_deprecated_function(): pass with ( patch( "homeassistant.helpers.frame.linecache.getline", return_value="await session.close()", ), patch( "homeassistant.helpers.frame.get_current_frame", return_value=extract_stack_to_frame( [ Mock( filename="/home/paulus/homeassistant/core.py", lineno="23", line="do_something()", ), Mock( filename="/home/paulus/config/custom_components/hue/light.py", lineno="23", line="await session.close()", ), Mock( filename="/home/paulus/aiohue/lights.py", lineno="2", line="something()", ), ] ), ), ): mock_deprecated_function() assert ( "mock_deprecated_function was called from hue, " f"this is a deprecated function{extra_msg}. " "Use new_function instead, please report it to the author of the " "'hue' custom integration" ) in caplog.text class TestDeprecatedConstantEnum(StrEnum): """Test deprecated constant enum.""" __test__ = False # prevent test collection of class by pytest TEST = "value" def _get_value( obj: DeprecatedConstant | DeprecatedConstantEnum | DeprecatedAlias | tuple[Any, ...], ) -> Any: if isinstance(obj, DeprecatedConstant): return obj.value if isinstance(obj, DeprecatedConstantEnum): return obj.enum if isinstance(obj, DeprecatedAlias): return obj.value if len(obj) == 2: return obj[0].value return obj[0] @pytest.mark.parametrize( ("deprecated_constant", "extra_msg", "description"), [ ( DeprecatedConstant("value", "NEW_CONSTANT", None), ". Use NEW_CONSTANT instead", "constant", ), ( DeprecatedConstant(1, "NEW_CONSTANT", "2099.1"), " which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead", "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, None), ". Use TestDeprecatedConstantEnum.TEST instead", "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, "2099.1"), " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", "constant", ), ( DeprecatedAlias(1, "new_alias", None), ". Use new_alias instead", "alias", ), ( DeprecatedAlias(1, "new_alias", "2099.1"), " which will be removed in HA Core 2099.1. Use new_alias instead", "alias", ), ], ) @pytest.mark.parametrize( ("module_name", "extra_extra_msg"), [ ("homeassistant.components.hue.light", ""), # builtin integration ( "config.custom_components.hue.light", ", please report it to the author of the 'hue' custom integration", ), # custom component integration ], ) def test_check_if_deprecated_constant( caplog: pytest.LogCaptureFixture, deprecated_constant: DeprecatedConstant | DeprecatedConstantEnum | DeprecatedAlias | tuple, extra_msg: str, module_name: str, extra_extra_msg: str, description: str, ) -> None: """Test check_if_deprecated_constant.""" module_globals = { "__name__": module_name, "_DEPRECATED_TEST_CONSTANT": deprecated_constant, } filename = f"/home/paulus/{module_name.replace('.', '/')}.py" # mock sys.modules for homeassistant/helpers/frame.py#get_integration_frame with ( patch.dict(sys.modules, {module_name: Mock(__file__=filename)}), patch( "homeassistant.helpers.frame.linecache.getline", return_value="await session.close()", ), patch( "homeassistant.helpers.frame.get_current_frame", return_value=extract_stack_to_frame( [ Mock( filename="/home/paulus/homeassistant/core.py", lineno="23", line="do_something()", ), Mock( filename=filename, lineno="23", line="await session.close()", ), Mock( filename="/home/paulus/aiohue/lights.py", lineno="2", line="something()", ), ] ), ), ): value = check_if_deprecated_constant("TEST_CONSTANT", module_globals) assert value == _get_value(deprecated_constant) assert ( module_name, logging.WARNING, f"TEST_CONSTANT was used from hue, this is a deprecated {description}{extra_msg}{extra_extra_msg}", ) in caplog.record_tuples @pytest.mark.parametrize( ("deprecated_constant", "extra_msg", "description"), [ ( DeprecatedConstant("value", "NEW_CONSTANT", None), ". Use NEW_CONSTANT instead", "constant", ), ( DeprecatedConstant(1, "NEW_CONSTANT", "2099.1"), " which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead", "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, None), ". Use TestDeprecatedConstantEnum.TEST instead", "constant", ), ( DeprecatedConstantEnum(TestDeprecatedConstantEnum.TEST, "2099.1"), " which will be removed in HA Core 2099.1. Use TestDeprecatedConstantEnum.TEST instead", "constant", ), ( DeprecatedAlias(1, "new_alias", None), ". Use new_alias instead", "alias", ), ( DeprecatedAlias(1, "new_alias", "2099.1"), " which will be removed in HA Core 2099.1. Use new_alias instead", "alias", ), ], ) @pytest.mark.parametrize( ("module_name"), [ "homeassistant.components.hue.light", # builtin integration "config.custom_components.hue.light", # custom component integration ], ) def test_check_if_deprecated_constant_integration_not_found( caplog: pytest.LogCaptureFixture, deprecated_constant: DeprecatedConstant | DeprecatedConstantEnum | DeprecatedAlias | tuple, extra_msg: str, module_name: str, description: str, ) -> None: """Test check_if_deprecated_constant.""" module_globals = { "__name__": module_name, "_DEPRECATED_TEST_CONSTANT": deprecated_constant, } with patch( "homeassistant.helpers.frame.get_current_frame", side_effect=MissingIntegrationFrame, ): value = check_if_deprecated_constant("TEST_CONSTANT", module_globals) assert value == _get_value(deprecated_constant) assert ( module_name, logging.WARNING, f"TEST_CONSTANT is a deprecated {description}{extra_msg}", ) not in caplog.record_tuples def test_test_check_if_deprecated_constant_invalid( caplog: pytest.LogCaptureFixture, ) -> None: """Test check_if_deprecated_constant error handling. Test check_if_deprecated_constant raises an attribute error and creates a log entry on an invalid deprecation type. """ module_name = "homeassistant.components.hue.light" module_globals = {"__name__": module_name, "_DEPRECATED_TEST_CONSTANT": 1} name = "TEST_CONSTANT" excepted_msg = ( f"Value of _DEPRECATED_{name} is an instance of but an instance " "of DeprecatedAlias, DeferredDeprecatedAlias, DeprecatedConstant or " "DeprecatedConstantEnum is required" ) with pytest.raises(AttributeError, match=excepted_msg): check_if_deprecated_constant(name, module_globals) assert (module_name, logging.DEBUG, excepted_msg) in caplog.record_tuples @pytest.mark.parametrize( ("module_globals", "expected"), [ ({"CONSTANT": 1}, ["CONSTANT"]), ({"_DEPRECATED_CONSTANT": 1}, ["_DEPRECATED_CONSTANT", "CONSTANT"]), ( {"_DEPRECATED_CONSTANT": 1, "SOMETHING": 2}, ["_DEPRECATED_CONSTANT", "SOMETHING", "CONSTANT"], ), ], ) def test_dir_with_deprecated_constants( module_globals: dict[str, Any], expected: list[str] ) -> None: """Test dir() with deprecated constants.""" assert dir_with_deprecated_constants([*module_globals.keys()]) == expected @pytest.mark.parametrize( ("module_name", "extra_extra_msg"), [ ("homeassistant.components.hue.light", ""), # builtin integration ( "config.custom_components.hue.light", ", please report it to the author of the 'hue' custom integration", ), # custom component integration ], ) def test_enum_with_deprecated_members( caplog: pytest.LogCaptureFixture, module_name: str, extra_extra_msg: str, ) -> None: """Test EnumWithDeprecatedMembers.""" filename = f"/home/paulus/{module_name.replace('.', '/')}.py" class TestEnum( StrEnum, metaclass=EnumWithDeprecatedMembers, deprecated={ "CATS": ("TestEnum.CATS_PER_CM", "2025.11.0"), "DOGS": ("TestEnum.DOGS_PER_CM", None), }, ): """Zoo units.""" CATS_PER_CM = "cats/cm" DOGS_PER_CM = "dogs/cm" CATS = "cats/cm" DOGS = "dogs/cm" # mock sys.modules for homeassistant/helpers/frame.py#get_integration_frame with ( patch.dict(sys.modules, {module_name: Mock(__file__=filename)}), patch( "homeassistant.helpers.frame.linecache.getline", return_value="await session.close()", ), patch( "homeassistant.helpers.frame.get_current_frame", return_value=extract_stack_to_frame( [ Mock( filename="/home/paulus/homeassistant/core.py", lineno="23", line="do_something()", ), Mock( filename=filename, lineno="23", line="await session.close()", ), Mock( filename="/home/paulus/aiohue/lights.py", lineno="2", line="something()", ), ] ), ), ): TestEnum.CATS # noqa: B018 TestEnum.DOGS # noqa: B018 assert len(caplog.record_tuples) == 2 assert ( "tests.helpers.test_deprecation", logging.WARNING, ( "TestEnum.CATS was used from hue, this is a deprecated enum member which " "will be removed in HA Core 2025.11.0. Use TestEnum.CATS_PER_CM instead" f"{extra_extra_msg}" ), ) in caplog.record_tuples assert ( "tests.helpers.test_deprecation", logging.WARNING, ( "TestEnum.DOGS was used from hue, this is a deprecated enum member. Use " f"TestEnum.DOGS_PER_CM instead{extra_extra_msg}" ), ) in caplog.record_tuples def test_enum_with_deprecated_members_integration_not_found( caplog: pytest.LogCaptureFixture, ) -> None: """Test check_if_deprecated_constant.""" class TestEnum( StrEnum, metaclass=EnumWithDeprecatedMembers, deprecated={ "CATS": ("TestEnum.CATS_PER_CM", "2025.11.0"), "DOGS": ("TestEnum.DOGS_PER_CM", None), }, ): """Zoo units.""" CATS_PER_CM = "cats/cm" DOGS_PER_CM = "dogs/cm" CATS = "cats/cm" DOGS = "dogs/cm" with patch( "homeassistant.helpers.frame.get_current_frame", side_effect=MissingIntegrationFrame, ): TestEnum.CATS # noqa: B018 TestEnum.DOGS # noqa: B018 assert len(caplog.record_tuples) == 0