mirror of https://github.com/home-assistant/core
640 lines
19 KiB
Python
640 lines
19 KiB
Python
"""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.value
|
|
|
|
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 <class 'int'> 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
|