From 0e0fd39603900534ffd26ebe95c4090dad2a9d70 Mon Sep 17 00:00:00 2001 From: Robert Resch <robert@resch.dev> Date: Tue, 19 Dec 2023 16:37:21 +0100 Subject: [PATCH] Add dir_with_deprecated_constants function to deprecation helper (#106059) --- .../components/binary_sensor/__init__.py | 3 +++ homeassistant/helpers/deprecation.py | 16 ++++++++++-- tests/common.py | 26 +++++++++++++++++++ tests/components/binary_sensor/test_init.py | 16 +++--------- tests/helpers/test_deprecation.py | 20 ++++++++++++++ 5 files changed, 67 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index dbed80a83f4..4372c0ee55b 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -20,6 +20,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 from homeassistant.helpers.deprecation import ( DeprecatedConstantEnum, check_if_deprecated_constant, + dir_with_deprecated_constants, ) from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent @@ -211,7 +212,9 @@ _DEPRECATED_DEVICE_CLASS_WINDOW = DeprecatedConstantEnum( BinarySensorDeviceClass.WINDOW, "2025.1" ) +# Both can be removed if no deprecated constant are in this module anymore __getattr__ = partial(check_if_deprecated_constant, module_globals=globals()) +__dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) # mypy: disallow-any-generics diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 740f96044a5..fd3fb50efd4 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -237,6 +237,9 @@ class DeprecatedConstantEnum(NamedTuple): breaks_in_ha_version: str | None +_PREFIX_DEPRECATED = "_DEPRECATED_" + + def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> Any: """Check if the not found name is a deprecated constant. @@ -245,7 +248,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A """ module_name = module_globals.get("__name__") logger = logging.getLogger(module_name) - if (deprecated_const := module_globals.get(f"_DEPRECATED_{name}")) is None: + if (deprecated_const := module_globals.get(_PREFIX_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 @@ -259,7 +262,7 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A 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)} " + f"Value of {_PREFIX_DEPRECATED}{name!r} is an instance of {type(deprecated_const)} " "but an instance of DeprecatedConstant or DeprecatedConstantEnum is required" ) @@ -279,3 +282,12 @@ def check_if_deprecated_constant(name: str, module_globals: dict[str, Any]) -> A breaks_in_ha_version, ) return value + + +def dir_with_deprecated_constants(module_globals: dict[str, Any]) -> list[str]: + """Return dir() with deprecated constants.""" + return list(module_globals) + [ + name.removeprefix(_PREFIX_DEPRECATED) + for name in module_globals + if name.startswith(_PREFIX_DEPRECATED) + ] diff --git a/tests/common.py b/tests/common.py index 1d0b278a6cb..05bddec203c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -6,6 +6,7 @@ from collections import OrderedDict from collections.abc import Generator, Mapping, Sequence from contextlib import contextmanager from datetime import UTC, datetime, timedelta +from enum import Enum import functools as ft from functools import lru_cache from io import StringIO @@ -15,10 +16,12 @@ import os import pathlib import threading import time +from types import ModuleType from typing import Any, NoReturn from unittest.mock import AsyncMock, Mock, patch from aiohttp.test_utils import unused_port as get_test_instance_port # noqa: F401 +import pytest import voluptuous as vol from homeassistant import auth, bootstrap, config_entries, loader @@ -1460,3 +1463,26 @@ def async_mock_cloud_connection_status(hass: HomeAssistant, connected: bool) -> else: state = CloudConnectionState.CLOUD_DISCONNECTED async_dispatcher_send(hass, SIGNAL_CLOUD_CONNECTION_STATE, state) + + +def validate_deprecated_constant( + caplog: pytest.LogCaptureFixture, + module: ModuleType, + replacement: Enum, + constant_prefix: str, + breaks_in_ha_version: str, +) -> None: + """Validate deprecated constant creates a log entry and is included in the modules.__dir__().""" + assert ( + module.__name__, + logging.WARNING, + ( + f"{constant_prefix}{replacement.name} was used from test_constant_deprecation," + f" this is a deprecated constant which will be removed in HA Core {breaks_in_ha_version}. " + f"Use {replacement.__class__.__name__}.{replacement.name} instead, please report " + "it to the author of the 'test_constant_deprecation' custom integration" + ), + ) in caplog.record_tuples + + # verify deprecated constant is included in dir() + assert f"{constant_prefix}{replacement.name}" in dir(module) diff --git a/tests/components/binary_sensor/test_init.py b/tests/components/binary_sensor/test_init.py index 782896b4dce..ac957818be9 100644 --- a/tests/components/binary_sensor/test_init.py +++ b/tests/components/binary_sensor/test_init.py @@ -1,6 +1,5 @@ """The tests for the Binary sensor component.""" from collections.abc import Generator -import logging from unittest import mock import pytest @@ -18,6 +17,7 @@ from tests.common import ( mock_config_flow, mock_integration, mock_platform, + validate_deprecated_constant, ) from tests.testing_config.custom_components.test.binary_sensor import MockBinarySensor from tests.testing_config.custom_components.test_constant_deprecation.binary_sensor import ( @@ -210,14 +210,6 @@ def test_deprecated_constant_device_class( ) -> 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 + validate_deprecated_constant( + caplog, binary_sensor, device_class, "DEVICE_CLASS_", "2025.1" + ) diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index 6cff4781583..4ad1677a16f 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -1,6 +1,7 @@ """Test deprecation helpers.""" import logging import sys +from typing import Any from unittest.mock import MagicMock, Mock, patch import pytest @@ -13,6 +14,7 @@ from homeassistant.helpers.deprecation import ( deprecated_class, deprecated_function, deprecated_substitute, + dir_with_deprecated_constants, get_deprecated, ) @@ -341,3 +343,21 @@ def test_test_check_if_deprecated_constant_invalid( check_if_deprecated_constant(name, module_globals) assert (module_name, logging.DEBUG, excepted_msg) in caplog.record_tuples + + +@pytest.mark.parametrize( + ("module_global", "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_global: dict[str, Any], expected: list[str] +) -> None: + """Test dir() with deprecated constants.""" + assert dir_with_deprecated_constants(module_global) == expected -- GitLab