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 enum import StrEnum
from functools import partial
import logging
from typing import Literal, final
@ -16,6 +17,10 @@ from homeassistant.helpers.config_validation import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
)
from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum,
check_if_deprecated_constant,
)
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
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
# use the BinarySensorDeviceClass enum instead.
DEVICE_CLASSES = [cls.value for cls in BinarySensorDeviceClass]
DEVICE_CLASS_BATTERY = BinarySensorDeviceClass.BATTERY.value
DEVICE_CLASS_BATTERY_CHARGING = BinarySensorDeviceClass.BATTERY_CHARGING.value
DEVICE_CLASS_CO = BinarySensorDeviceClass.CO.value
DEVICE_CLASS_COLD = BinarySensorDeviceClass.COLD.value
DEVICE_CLASS_CONNECTIVITY = BinarySensorDeviceClass.CONNECTIVITY.value
DEVICE_CLASS_DOOR = BinarySensorDeviceClass.DOOR.value
DEVICE_CLASS_GARAGE_DOOR = BinarySensorDeviceClass.GARAGE_DOOR.value
DEVICE_CLASS_GAS = BinarySensorDeviceClass.GAS.value
DEVICE_CLASS_HEAT = BinarySensorDeviceClass.HEAT.value
DEVICE_CLASS_LIGHT = BinarySensorDeviceClass.LIGHT.value
DEVICE_CLASS_LOCK = BinarySensorDeviceClass.LOCK.value
DEVICE_CLASS_MOISTURE = BinarySensorDeviceClass.MOISTURE.value
DEVICE_CLASS_MOTION = BinarySensorDeviceClass.MOTION.value
DEVICE_CLASS_MOVING = BinarySensorDeviceClass.MOVING.value
DEVICE_CLASS_OCCUPANCY = BinarySensorDeviceClass.OCCUPANCY.value
DEVICE_CLASS_OPENING = BinarySensorDeviceClass.OPENING.value
DEVICE_CLASS_PLUG = BinarySensorDeviceClass.PLUG.value
DEVICE_CLASS_POWER = BinarySensorDeviceClass.POWER.value
DEVICE_CLASS_PRESENCE = BinarySensorDeviceClass.PRESENCE.value
DEVICE_CLASS_PROBLEM = BinarySensorDeviceClass.PROBLEM.value
DEVICE_CLASS_RUNNING = BinarySensorDeviceClass.RUNNING.value
DEVICE_CLASS_SAFETY = BinarySensorDeviceClass.SAFETY.value
DEVICE_CLASS_SMOKE = BinarySensorDeviceClass.SMOKE.value
DEVICE_CLASS_SOUND = BinarySensorDeviceClass.SOUND.value
DEVICE_CLASS_TAMPER = BinarySensorDeviceClass.TAMPER.value
DEVICE_CLASS_UPDATE = BinarySensorDeviceClass.UPDATE.value
DEVICE_CLASS_VIBRATION = BinarySensorDeviceClass.VIBRATION.value
DEVICE_CLASS_WINDOW = BinarySensorDeviceClass.WINDOW.value
_DEPRECATED_DEVICE_CLASS_BATTERY = DeprecatedConstantEnum(
BinarySensorDeviceClass.BATTERY, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_BATTERY_CHARGING = DeprecatedConstantEnum(
BinarySensorDeviceClass.BATTERY_CHARGING, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_CO = DeprecatedConstantEnum(
BinarySensorDeviceClass.CO, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_COLD = DeprecatedConstantEnum(
BinarySensorDeviceClass.COLD, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_CONNECTIVITY = DeprecatedConstantEnum(
BinarySensorDeviceClass.CONNECTIVITY, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_DOOR = DeprecatedConstantEnum(
BinarySensorDeviceClass.DOOR, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_GARAGE_DOOR = DeprecatedConstantEnum(
BinarySensorDeviceClass.GARAGE_DOOR, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_GAS = DeprecatedConstantEnum(
BinarySensorDeviceClass.GAS, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_HEAT = DeprecatedConstantEnum(
BinarySensorDeviceClass.HEAT, "2025.1"
)
_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

View file

@ -3,10 +3,11 @@ from __future__ import annotations
from collections.abc import Callable
from contextlib import suppress
from enum import Enum
import functools
import inspect
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.exceptions import HomeAssistantError
@ -153,7 +154,25 @@ def _print_deprecation_warning(
verb: str,
breaks_in_ha_version: str | 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:
breaks_in = f" which will be removed in HA Core {breaks_in_ha_version}"
else:
@ -163,7 +182,7 @@ def _print_deprecation_warning(
except MissingIntegrationFrame:
logger.warning(
"%s is a deprecated %s%s. Use %s instead",
obj.__name__,
obj_name,
description,
breaks_in,
replacement,
@ -183,7 +202,7 @@ def _print_deprecation_warning(
"%s was %s from %s, this is a deprecated %s%s. Use %s instead,"
" please %s"
),
obj.__name__,
obj_name,
verb,
integration_frame.integration,
description,
@ -194,10 +213,69 @@ def _print_deprecation_warning(
else:
logger.warning(
"%s was %s from %s, this is a deprecated %s%s. Use %s instead",
obj.__name__,
obj_name,
verb,
integration_frame.integration,
description,
breaks_in,
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."""
from collections.abc import Generator
import logging
from unittest import mock
import pytest
@ -19,6 +20,9 @@ from tests.common import (
mock_platform,
)
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"
@ -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"
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."""
import logging
import sys
from unittest.mock import MagicMock, Mock, patch
import pytest
from homeassistant.core import HomeAssistant
from homeassistant.helpers.deprecation import (
DeprecatedConstant,
DeprecatedConstantEnum,
check_if_deprecated_constant,
deprecated_class,
deprecated_function,
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 "
"'hue' custom integration"
) 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}")