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:
epenet 2024-11-07 18:23:35 +01:00 committed by GitHub
parent ee30520b57
commit a3b0909e3f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 177 additions and 33 deletions

View file

@ -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(

View file

@ -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

View file

@ -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 (

View file

@ -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(

View file

@ -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)

View file

@ -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