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