Add new frame helper to better distinguish custom and core integrations (#130025)
* Add new frame helper to clarify options available * Adjust * Improve * Use report_usage in core * Add tests * Use is/is not Co-authored-by: J. Nick Koston <nick@koston.org> * Use enum.auto() --------- Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
ee30520b57
commit
a3b0909e3f
6 changed files with 177 additions and 33 deletions
|
@ -656,12 +656,12 @@ class HomeAssistant:
|
|||
# late import to avoid circular imports
|
||||
from .helpers import frame # pylint: disable=import-outside-toplevel
|
||||
|
||||
frame.report(
|
||||
frame.report_usage(
|
||||
"calls `async_add_job`, which is deprecated and will be removed in Home "
|
||||
"Assistant 2025.4; Please review "
|
||||
"https://developers.home-assistant.io/blog/2024/03/13/deprecate_add_run_job"
|
||||
" for replacement options",
|
||||
error_if_core=False,
|
||||
core_behavior=frame.ReportBehavior.LOG,
|
||||
)
|
||||
|
||||
if target is None:
|
||||
|
@ -712,12 +712,12 @@ class HomeAssistant:
|
|||
# late import to avoid circular imports
|
||||
from .helpers import frame # pylint: disable=import-outside-toplevel
|
||||
|
||||
frame.report(
|
||||
frame.report_usage(
|
||||
"calls `async_add_hass_job`, which is deprecated and will be removed in Home "
|
||||
"Assistant 2025.5; Please review "
|
||||
"https://developers.home-assistant.io/blog/2024/04/07/deprecate_add_hass_job"
|
||||
" for replacement options",
|
||||
error_if_core=False,
|
||||
core_behavior=frame.ReportBehavior.LOG,
|
||||
)
|
||||
|
||||
return self._async_add_hass_job(hassjob, *args, background=background)
|
||||
|
@ -986,12 +986,12 @@ class HomeAssistant:
|
|||
# late import to avoid circular imports
|
||||
from .helpers import frame # pylint: disable=import-outside-toplevel
|
||||
|
||||
frame.report(
|
||||
frame.report_usage(
|
||||
"calls `async_run_job`, which is deprecated and will be removed in Home "
|
||||
"Assistant 2025.4; Please review "
|
||||
"https://developers.home-assistant.io/blog/2024/03/13/deprecate_add_run_job"
|
||||
" for replacement options",
|
||||
error_if_core=False,
|
||||
core_behavior=frame.ReportBehavior.LOG,
|
||||
)
|
||||
|
||||
if asyncio.iscoroutine(target):
|
||||
|
@ -1635,10 +1635,10 @@ class EventBus:
|
|||
# late import to avoid circular imports
|
||||
from .helpers import frame # pylint: disable=import-outside-toplevel
|
||||
|
||||
frame.report(
|
||||
frame.report_usage(
|
||||
"calls `async_listen` with run_immediately, which is"
|
||||
" deprecated and will be removed in Home Assistant 2025.5",
|
||||
error_if_core=False,
|
||||
core_behavior=frame.ReportBehavior.LOG,
|
||||
)
|
||||
|
||||
if event_filter is not None and not is_callback_check_partial(event_filter):
|
||||
|
@ -1705,10 +1705,10 @@ class EventBus:
|
|||
# late import to avoid circular imports
|
||||
from .helpers import frame # pylint: disable=import-outside-toplevel
|
||||
|
||||
frame.report(
|
||||
frame.report_usage(
|
||||
"calls `async_listen_once` with run_immediately, which is "
|
||||
"deprecated and will be removed in Home Assistant 2025.5",
|
||||
error_if_core=False,
|
||||
core_behavior=frame.ReportBehavior.LOG,
|
||||
)
|
||||
|
||||
one_time_listener: _OneTimeListener[_DataT] = _OneTimeListener(
|
||||
|
|
|
@ -60,7 +60,7 @@ from .core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
|||
from .generated.currencies import HISTORIC_CURRENCIES
|
||||
from .helpers import config_validation as cv, issue_registry as ir
|
||||
from .helpers.entity_values import EntityValues
|
||||
from .helpers.frame import report
|
||||
from .helpers.frame import ReportBehavior, report_usage
|
||||
from .helpers.storage import Store
|
||||
from .helpers.typing import UNDEFINED, UndefinedType
|
||||
from .util import dt as dt_util, location
|
||||
|
@ -695,11 +695,11 @@ class Config:
|
|||
|
||||
It will be removed in Home Assistant 2025.6.
|
||||
"""
|
||||
report(
|
||||
report_usage(
|
||||
"set the time zone using set_time_zone instead of async_set_time_zone"
|
||||
" which will stop working in Home Assistant 2025.6",
|
||||
error_if_core=True,
|
||||
error_if_integration=True,
|
||||
core_integration_behavior=ReportBehavior.ERROR,
|
||||
custom_integration_behavior=ReportBehavior.ERROR,
|
||||
)
|
||||
if time_zone := dt_util.get_time_zone(time_zone_str):
|
||||
self.time_zone = time_zone_str
|
||||
|
|
|
@ -26,7 +26,7 @@ from .helpers.deprecation import (
|
|||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
from .helpers.frame import report
|
||||
from .helpers.frame import ReportBehavior, report_usage
|
||||
from .loader import async_suggest_report_issue
|
||||
from .util import uuid as uuid_util
|
||||
|
||||
|
@ -530,12 +530,12 @@ class FlowManager(abc.ABC, Generic[_FlowContextT, _FlowResultT, _HandlerT]):
|
|||
|
||||
if not isinstance(result["type"], FlowResultType):
|
||||
result["type"] = FlowResultType(result["type"]) # type: ignore[unreachable]
|
||||
report(
|
||||
report_usage(
|
||||
(
|
||||
"does not use FlowResultType enum for data entry flow result type. "
|
||||
"This is deprecated and will stop working in Home Assistant 2025.1"
|
||||
),
|
||||
error_if_core=False,
|
||||
core_behavior=ReportBehavior.LOG,
|
||||
)
|
||||
|
||||
if (
|
||||
|
|
|
@ -5,6 +5,7 @@ from __future__ import annotations
|
|||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import enum
|
||||
import functools
|
||||
import linecache
|
||||
import logging
|
||||
|
@ -144,24 +145,72 @@ def report(
|
|||
If error_if_integration is True, raise instead of log if an integration is found
|
||||
when unwinding the stack frame.
|
||||
"""
|
||||
core_behavior = ReportBehavior.ERROR if error_if_core else ReportBehavior.LOG
|
||||
core_integration_behavior = (
|
||||
ReportBehavior.ERROR if error_if_integration else ReportBehavior.LOG
|
||||
)
|
||||
custom_integration_behavior = core_integration_behavior
|
||||
|
||||
if log_custom_component_only:
|
||||
if core_behavior is ReportBehavior.LOG:
|
||||
core_behavior = ReportBehavior.IGNORE
|
||||
if core_integration_behavior is ReportBehavior.LOG:
|
||||
core_integration_behavior = ReportBehavior.IGNORE
|
||||
|
||||
report_usage(
|
||||
what,
|
||||
core_behavior=core_behavior,
|
||||
core_integration_behavior=core_integration_behavior,
|
||||
custom_integration_behavior=custom_integration_behavior,
|
||||
exclude_integrations=exclude_integrations,
|
||||
level=level,
|
||||
)
|
||||
|
||||
|
||||
class ReportBehavior(enum.Enum):
|
||||
"""Enum for behavior on code usage."""
|
||||
|
||||
IGNORE = enum.auto()
|
||||
"""Ignore the code usage."""
|
||||
LOG = enum.auto()
|
||||
"""Log the code usage."""
|
||||
ERROR = enum.auto()
|
||||
"""Raise an error on code usage."""
|
||||
|
||||
|
||||
def report_usage(
|
||||
what: str,
|
||||
*,
|
||||
core_behavior: ReportBehavior = ReportBehavior.ERROR,
|
||||
core_integration_behavior: ReportBehavior = ReportBehavior.LOG,
|
||||
custom_integration_behavior: ReportBehavior = ReportBehavior.LOG,
|
||||
exclude_integrations: set[str] | None = None,
|
||||
level: int = logging.WARNING,
|
||||
) -> None:
|
||||
"""Report incorrect code usage.
|
||||
|
||||
Similar to `report` but allows more fine-grained reporting.
|
||||
"""
|
||||
try:
|
||||
integration_frame = get_integration_frame(
|
||||
exclude_integrations=exclude_integrations
|
||||
)
|
||||
except MissingIntegrationFrame as err:
|
||||
msg = f"Detected code that {what}. Please report this issue."
|
||||
if error_if_core:
|
||||
if core_behavior is ReportBehavior.ERROR:
|
||||
raise RuntimeError(msg) from err
|
||||
if not log_custom_component_only:
|
||||
if core_behavior is ReportBehavior.LOG:
|
||||
_LOGGER.warning(msg, stack_info=True)
|
||||
return
|
||||
|
||||
if (
|
||||
error_if_integration
|
||||
or not log_custom_component_only
|
||||
or integration_frame.custom_integration
|
||||
):
|
||||
_report_integration(what, integration_frame, level, error_if_integration)
|
||||
integration_behavior = core_integration_behavior
|
||||
if integration_frame.custom_integration:
|
||||
integration_behavior = custom_integration_behavior
|
||||
|
||||
if integration_behavior is not ReportBehavior.IGNORE:
|
||||
_report_integration(
|
||||
what, integration_frame, level, integration_behavior is ReportBehavior.ERROR
|
||||
)
|
||||
|
||||
|
||||
def _report_integration(
|
||||
|
|
|
@ -1556,16 +1556,18 @@ class Components:
|
|||
raise ImportError(f"Unable to load {comp_name}")
|
||||
|
||||
# Local import to avoid circular dependencies
|
||||
from .helpers.frame import report # pylint: disable=import-outside-toplevel
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .helpers.frame import ReportBehavior, report_usage
|
||||
|
||||
report(
|
||||
report_usage(
|
||||
(
|
||||
f"accesses hass.components.{comp_name}."
|
||||
" This is deprecated and will stop working in Home Assistant 2025.3, it"
|
||||
f" should be updated to import functions used from {comp_name} directly"
|
||||
),
|
||||
error_if_core=False,
|
||||
log_custom_component_only=True,
|
||||
core_behavior=ReportBehavior.IGNORE,
|
||||
core_integration_behavior=ReportBehavior.IGNORE,
|
||||
custom_integration_behavior=ReportBehavior.LOG,
|
||||
)
|
||||
|
||||
wrapped = ModuleWrapper(self._hass, component)
|
||||
|
@ -1585,16 +1587,18 @@ class Helpers:
|
|||
helper = importlib.import_module(f"homeassistant.helpers.{helper_name}")
|
||||
|
||||
# Local import to avoid circular dependencies
|
||||
from .helpers.frame import report # pylint: disable=import-outside-toplevel
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .helpers.frame import ReportBehavior, report_usage
|
||||
|
||||
report(
|
||||
report_usage(
|
||||
(
|
||||
f"accesses hass.helpers.{helper_name}."
|
||||
" This is deprecated and will stop working in Home Assistant 2025.5, it"
|
||||
f" should be updated to import functions used from {helper_name} directly"
|
||||
),
|
||||
error_if_core=False,
|
||||
log_custom_component_only=True,
|
||||
core_behavior=ReportBehavior.IGNORE,
|
||||
core_integration_behavior=ReportBehavior.IGNORE,
|
||||
custom_integration_behavior=ReportBehavior.LOG,
|
||||
)
|
||||
|
||||
wrapped = ModuleWrapper(self._hass, helper)
|
||||
|
|
|
@ -157,6 +157,97 @@ async def test_get_integration_logger_no_integration(
|
|||
assert logger.name == __name__
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("integration_frame_path", "keywords", "expected_error", "expected_log"),
|
||||
[
|
||||
pytest.param(
|
||||
"homeassistant/test_core",
|
||||
{},
|
||||
True,
|
||||
0,
|
||||
id="core default",
|
||||
),
|
||||
pytest.param(
|
||||
"homeassistant/components/test_core_integration",
|
||||
{},
|
||||
False,
|
||||
1,
|
||||
id="core integration default",
|
||||
),
|
||||
pytest.param(
|
||||
"custom_components/test_custom_integration",
|
||||
{},
|
||||
False,
|
||||
1,
|
||||
id="custom integration default",
|
||||
),
|
||||
pytest.param(
|
||||
"custom_components/test_custom_integration",
|
||||
{"custom_integration_behavior": frame.ReportBehavior.IGNORE},
|
||||
False,
|
||||
0,
|
||||
id="custom integration ignore",
|
||||
),
|
||||
pytest.param(
|
||||
"custom_components/test_custom_integration",
|
||||
{"custom_integration_behavior": frame.ReportBehavior.ERROR},
|
||||
True,
|
||||
1,
|
||||
id="custom integration error",
|
||||
),
|
||||
pytest.param(
|
||||
"homeassistant/components/test_integration_frame",
|
||||
{"core_integration_behavior": frame.ReportBehavior.IGNORE},
|
||||
False,
|
||||
0,
|
||||
id="core_integration_behavior ignore",
|
||||
),
|
||||
pytest.param(
|
||||
"homeassistant/components/test_integration_frame",
|
||||
{"core_integration_behavior": frame.ReportBehavior.ERROR},
|
||||
True,
|
||||
1,
|
||||
id="core_integration_behavior error",
|
||||
),
|
||||
pytest.param(
|
||||
"homeassistant/test_integration_frame",
|
||||
{"core_behavior": frame.ReportBehavior.IGNORE},
|
||||
False,
|
||||
0,
|
||||
id="core_behavior ignore",
|
||||
),
|
||||
pytest.param(
|
||||
"homeassistant/test_integration_frame",
|
||||
{"core_behavior": frame.ReportBehavior.LOG},
|
||||
False,
|
||||
1,
|
||||
id="core_behavior log",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_integration_frame")
|
||||
async def test_report_usage(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
keywords: dict[str, Any],
|
||||
expected_error: bool,
|
||||
expected_log: int,
|
||||
) -> None:
|
||||
"""Test report."""
|
||||
|
||||
what = "test_report_string"
|
||||
|
||||
errored = False
|
||||
try:
|
||||
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()):
|
||||
frame.report_usage(what, **keywords)
|
||||
except RuntimeError:
|
||||
errored = True
|
||||
|
||||
assert errored == expected_error
|
||||
|
||||
assert caplog.text.count(what) == expected_log
|
||||
|
||||
|
||||
@patch.object(frame, "_REPORTED_INTEGRATIONS", set())
|
||||
async def test_prevent_flooding(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_integration_frame: Mock
|
||||
|
|
Loading…
Add table
Reference in a new issue