Deprecate binary sensor device class constants (#105736)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
This commit is contained in:
Robert Resch 2023-12-19 12:45:32 +01:00 committed by GitHub
parent c64c1c8f08
commit a4ccd6e13b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 302 additions and 33 deletions

View File

@ -3,6 +3,7 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from enum import StrEnum from enum import StrEnum
from functools import partial
import logging import logging
from typing import Literal, final from typing import Literal, final
@ -16,6 +17,10 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA, PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE, PLATFORM_SCHEMA_BASE,
) )
from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum,
check_if_deprecated_constant,
)
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -121,34 +126,92 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass))
# DEVICE_CLASS* below are deprecated as of 2021.12 # DEVICE_CLASS* below are deprecated as of 2021.12
# use the BinarySensorDeviceClass enum instead. # use the BinarySensorDeviceClass enum instead.
DEVICE_CLASSES = [cls.value for cls in BinarySensorDeviceClass] DEVICE_CLASSES = [cls.value for cls in BinarySensorDeviceClass]
DEVICE_CLASS_BATTERY = BinarySensorDeviceClass.BATTERY.value _DEPRECATED_DEVICE_CLASS_BATTERY = DeprecatedConstantEnum(
DEVICE_CLASS_BATTERY_CHARGING = BinarySensorDeviceClass.BATTERY_CHARGING.value BinarySensorDeviceClass.BATTERY, "2025.1"
DEVICE_CLASS_CO = BinarySensorDeviceClass.CO.value )
DEVICE_CLASS_COLD = BinarySensorDeviceClass.COLD.value _DEPRECATED_DEVICE_CLASS_BATTERY_CHARGING = DeprecatedConstantEnum(
DEVICE_CLASS_CONNECTIVITY = BinarySensorDeviceClass.CONNECTIVITY.value BinarySensorDeviceClass.BATTERY_CHARGING, "2025.1"
DEVICE_CLASS_DOOR = BinarySensorDeviceClass.DOOR.value )
DEVICE_CLASS_GARAGE_DOOR = BinarySensorDeviceClass.GARAGE_DOOR.value _DEPRECATED_DEVICE_CLASS_CO = DeprecatedConstantEnum(
DEVICE_CLASS_GAS = BinarySensorDeviceClass.GAS.value BinarySensorDeviceClass.CO, "2025.1"
DEVICE_CLASS_HEAT = BinarySensorDeviceClass.HEAT.value )
DEVICE_CLASS_LIGHT = BinarySensorDeviceClass.LIGHT.value _DEPRECATED_DEVICE_CLASS_COLD = DeprecatedConstantEnum(
DEVICE_CLASS_LOCK = BinarySensorDeviceClass.LOCK.value BinarySensorDeviceClass.COLD, "2025.1"
DEVICE_CLASS_MOISTURE = BinarySensorDeviceClass.MOISTURE.value )
DEVICE_CLASS_MOTION = BinarySensorDeviceClass.MOTION.value _DEPRECATED_DEVICE_CLASS_CONNECTIVITY = DeprecatedConstantEnum(
DEVICE_CLASS_MOVING = BinarySensorDeviceClass.MOVING.value BinarySensorDeviceClass.CONNECTIVITY, "2025.1"
DEVICE_CLASS_OCCUPANCY = BinarySensorDeviceClass.OCCUPANCY.value )
DEVICE_CLASS_OPENING = BinarySensorDeviceClass.OPENING.value _DEPRECATED_DEVICE_CLASS_DOOR = DeprecatedConstantEnum(
DEVICE_CLASS_PLUG = BinarySensorDeviceClass.PLUG.value BinarySensorDeviceClass.DOOR, "2025.1"
DEVICE_CLASS_POWER = BinarySensorDeviceClass.POWER.value )
DEVICE_CLASS_PRESENCE = BinarySensorDeviceClass.PRESENCE.value _DEPRECATED_DEVICE_CLASS_GARAGE_DOOR = DeprecatedConstantEnum(
DEVICE_CLASS_PROBLEM = BinarySensorDeviceClass.PROBLEM.value BinarySensorDeviceClass.GARAGE_DOOR, "2025.1"
DEVICE_CLASS_RUNNING = BinarySensorDeviceClass.RUNNING.value )
DEVICE_CLASS_SAFETY = BinarySensorDeviceClass.SAFETY.value _DEPRECATED_DEVICE_CLASS_GAS = DeprecatedConstantEnum(
DEVICE_CLASS_SMOKE = BinarySensorDeviceClass.SMOKE.value BinarySensorDeviceClass.GAS, "2025.1"
DEVICE_CLASS_SOUND = BinarySensorDeviceClass.SOUND.value )
DEVICE_CLASS_TAMPER = BinarySensorDeviceClass.TAMPER.value _DEPRECATED_DEVICE_CLASS_HEAT = DeprecatedConstantEnum(
DEVICE_CLASS_UPDATE = BinarySensorDeviceClass.UPDATE.value BinarySensorDeviceClass.HEAT, "2025.1"
DEVICE_CLASS_VIBRATION = BinarySensorDeviceClass.VIBRATION.value )
DEVICE_CLASS_WINDOW = BinarySensorDeviceClass.WINDOW.value _DEPRECATED_DEVICE_CLASS_LIGHT = DeprecatedConstantEnum(
BinarySensorDeviceClass.LIGHT, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_LOCK = DeprecatedConstantEnum(
BinarySensorDeviceClass.LOCK, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_MOISTURE = DeprecatedConstantEnum(
BinarySensorDeviceClass.MOISTURE, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_MOTION = DeprecatedConstantEnum(
BinarySensorDeviceClass.MOTION, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_MOVING = DeprecatedConstantEnum(
BinarySensorDeviceClass.MOVING, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_OCCUPANCY = DeprecatedConstantEnum(
BinarySensorDeviceClass.OCCUPANCY, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_OPENING = DeprecatedConstantEnum(
BinarySensorDeviceClass.OPENING, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_PLUG = DeprecatedConstantEnum(
BinarySensorDeviceClass.PLUG, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_POWER = DeprecatedConstantEnum(
BinarySensorDeviceClass.POWER, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_PRESENCE = DeprecatedConstantEnum(
BinarySensorDeviceClass.PRESENCE, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_PROBLEM = DeprecatedConstantEnum(
BinarySensorDeviceClass.PROBLEM, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_RUNNING = DeprecatedConstantEnum(
BinarySensorDeviceClass.RUNNING, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_SAFETY = DeprecatedConstantEnum(
BinarySensorDeviceClass.SAFETY, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_SMOKE = DeprecatedConstantEnum(
BinarySensorDeviceClass.SMOKE, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_SOUND = DeprecatedConstantEnum(
BinarySensorDeviceClass.SOUND, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_TAMPER = DeprecatedConstantEnum(
BinarySensorDeviceClass.TAMPER, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_UPDATE = DeprecatedConstantEnum(
BinarySensorDeviceClass.UPDATE, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_VIBRATION = DeprecatedConstantEnum(
BinarySensorDeviceClass.VIBRATION, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_WINDOW = DeprecatedConstantEnum(
BinarySensorDeviceClass.WINDOW, "2025.1"
)
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
# mypy: disallow-any-generics # mypy: disallow-any-generics

View File

@ -3,10 +3,11 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from contextlib import suppress from contextlib import suppress
from enum import Enum
import functools import functools
import inspect import inspect
import logging import logging
from typing import Any, ParamSpec, TypeVar from typing import Any, NamedTuple, ParamSpec, TypeVar
from homeassistant.core import HomeAssistant, async_get_hass from homeassistant.core import HomeAssistant, async_get_hass
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@ -153,7 +154,25 @@ def _print_deprecation_warning(
verb: str, verb: str,
breaks_in_ha_version: str | None, breaks_in_ha_version: str | None,
) -> None: ) -> None:
logger = logging.getLogger(obj.__module__) _print_deprecation_warning_internal(
obj.__name__,
obj.__module__,
replacement,
description,
verb,
breaks_in_ha_version,
)
def _print_deprecation_warning_internal(
obj_name: str,
module_name: str,
replacement: str,
description: str,
verb: str,
breaks_in_ha_version: str | None,
) -> None:
logger = logging.getLogger(module_name)
if breaks_in_ha_version: if breaks_in_ha_version:
breaks_in = f" which will be removed in HA Core {breaks_in_ha_version}" breaks_in = f" which will be removed in HA Core {breaks_in_ha_version}"
else: else:
@ -163,7 +182,7 @@ def _print_deprecation_warning(
except MissingIntegrationFrame: except MissingIntegrationFrame:
logger.warning( logger.warning(
"%s is a deprecated %s%s. Use %s instead", "%s is a deprecated %s%s. Use %s instead",
obj.__name__, obj_name,
description, description,
breaks_in, breaks_in,
replacement, replacement,
@ -183,7 +202,7 @@ def _print_deprecation_warning(
"%s was %s from %s, this is a deprecated %s%s. Use %s instead," "%s was %s from %s, this is a deprecated %s%s. Use %s instead,"
" please %s" " please %s"
), ),
obj.__name__, obj_name,
verb, verb,
integration_frame.integration, integration_frame.integration,
description, description,
@ -194,10 +213,69 @@ def _print_deprecation_warning(
else: else:
logger.warning( logger.warning(
"%s was %s from %s, this is a deprecated %s%s. Use %s instead", "%s was %s from %s, this is a deprecated %s%s. Use %s instead",
obj.__name__, obj_name,
verb, verb,
integration_frame.integration, integration_frame.integration,
description, description,
breaks_in, breaks_in,
replacement, replacement,
) )
class DeprecatedConstant(NamedTuple):
"""Deprecated constant."""
value: Any
replacement: str
breaks_in_ha_version: str | None
class DeprecatedConstantEnum(NamedTuple):
"""Deprecated constant."""
enum: Enum
breaks_in_ha_version: str | None
def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> Any:
"""Check if the not found name is a deprecated constant.
If it is, print a deprecation warning and return the value of the constant.
Otherwise raise AttributeError.
"""
module_name = module_globals.get("__name__")
logger = logging.getLogger(module_name)
if (deprecated_const := module_globals.get(f"_DEPRECATED_{name}")) is None:
raise AttributeError(f"Module {module_name!r} has no attribute {name!r}")
if isinstance(deprecated_const, DeprecatedConstant):
value = deprecated_const.value
replacement = deprecated_const.replacement
breaks_in_ha_version = deprecated_const.breaks_in_ha_version
elif isinstance(deprecated_const, DeprecatedConstantEnum):
value = deprecated_const.enum.value
replacement = (
f"{deprecated_const.enum.__class__.__name__}.{deprecated_const.enum.name}"
)
breaks_in_ha_version = deprecated_const.breaks_in_ha_version
else:
msg = (
f"Value of _DEPRECATED_{name!r} is an instance of {type(deprecated_const)} "
"but an instance of DeprecatedConstant or DeprecatedConstantEnum is required"
)
logger.debug(msg)
# PEP 562 -- Module __getattr__ and __dir__
# specifies that __getattr__ should raise AttributeError if the attribute is not
# found.
# https://peps.python.org/pep-0562/#specification
raise AttributeError(msg) # noqa: TRY004
_print_deprecation_warning_internal(
name,
module_name or __name__,
replacement,
"constant",
"used",
breaks_in_ha_version,
)
return value

View File

@ -1,5 +1,6 @@
"""The tests for the Binary sensor component.""" """The tests for the Binary sensor component."""
from collections.abc import Generator from collections.abc import Generator
import logging
from unittest import mock from unittest import mock
import pytest import pytest
@ -19,6 +20,9 @@ from tests.common import (
mock_platform, mock_platform,
) )
from tests.testing_config.custom_components.test.binary_sensor import MockBinarySensor from tests.testing_config.custom_components.test.binary_sensor import MockBinarySensor
from tests.testing_config.custom_components.test_constant_deprecation.binary_sensor import (
import_deprecated,
)
TEST_DOMAIN = "test" TEST_DOMAIN = "test"
@ -194,3 +198,26 @@ async def test_entity_category_config_raises_error(
"Entity binary_sensor.test2 cannot be added as the entity category is set to config" "Entity binary_sensor.test2 cannot be added as the entity category is set to config"
in caplog.text in caplog.text
) )
@pytest.mark.parametrize(
"device_class",
list(binary_sensor.BinarySensorDeviceClass),
)
def test_deprecated_constant_device_class(
caplog: pytest.LogCaptureFixture,
device_class: binary_sensor.BinarySensorDeviceClass,
) -> None:
"""Test deprecated binary sensor device classes."""
import_deprecated(device_class)
assert (
"homeassistant.components.binary_sensor",
logging.WARNING,
(
f"DEVICE_CLASS_{device_class.name} was used from test_constant_deprecation,"
" this is a deprecated constant which will be removed in HA Core 2025.1. "
f"Use BinarySensorDeviceClass.{device_class.name} instead, please report "
"it to the author of the 'test_constant_deprecation' custom integration"
),
) in caplog.record_tuples

View File

@ -1,10 +1,15 @@
"""Test deprecation helpers.""" """Test deprecation helpers."""
import logging
import sys
from unittest.mock import MagicMock, Mock, patch from unittest.mock import MagicMock, Mock, patch
import pytest import pytest
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.deprecation import ( from homeassistant.helpers.deprecation import (
DeprecatedConstant,
DeprecatedConstantEnum,
check_if_deprecated_constant,
deprecated_class, deprecated_class,
deprecated_function, deprecated_function,
deprecated_substitute, deprecated_substitute,
@ -247,3 +252,92 @@ def test_deprecated_function_called_from_custom_integration(
"Use new_function instead, please report it to the author of the " "Use new_function instead, please report it to the author of the "
"'hue' custom integration" "'hue' custom integration"
) in caplog.text ) in caplog.text
@pytest.mark.parametrize(
("deprecated_constant", "extra_msg"),
[
(
DeprecatedConstant("value", "NEW_CONSTANT", None),
". Use NEW_CONSTANT instead",
),
(
DeprecatedConstant(1, "NEW_CONSTANT", "2099.1"),
" which will be removed in HA Core 2099.1. Use NEW_CONSTANT instead",
),
],
)
@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,
extra_msg: str,
module_name: str,
extra_extra_msg: 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 module for homeassistant/helpers/frame.py#get_integration_frame
sys.modules[module_name] = Mock(__file__=filename)
with patch(
"homeassistant.helpers.frame.extract_stack",
return_value=[
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 == deprecated_constant.value
assert (
module_name,
logging.WARNING,
f"TEST_CONSTANT was used from hue, this is a deprecated constant{extra_msg}{extra_extra_msg}",
) in caplog.record_tuples
def test_test_check_if_deprecated_constant_invalid(
caplog: pytest.LogCaptureFixture
) -> None:
"""Test check_if_deprecated_constant will raise an attribute error and create an 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!r} is an instance of <class 'int'> "
"but an instance of 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

View File

@ -0,0 +1,7 @@
"""Test deprecated binary sensor device classes."""
from homeassistant.components import binary_sensor
def import_deprecated(device_class: binary_sensor.BinarySensorDeviceClass):
"""Import deprecated device class constant."""
getattr(binary_sensor, f"DEVICE_CLASS_{device_class.name}")