Skip to content

Commit

Permalink
Add new frame helper to better distinguish custom and core integratio…
Browse files Browse the repository at this point in the history
…ns (#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 <[email protected]>

* Use enum.auto()

---------

Co-authored-by: J. Nick Koston <[email protected]>
  • Loading branch information
epenet and bdraco authored Nov 7, 2024
1 parent ee30520 commit a3b0909
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 33 deletions.
20 changes: 10 additions & 10 deletions homeassistant/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,12 +656,12 @@ def async_add_job[_R, *_Ts](
# 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:
Expand Down Expand Up @@ -712,12 +712,12 @@ def async_add_hass_job[_R](
# 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)
Expand Down Expand Up @@ -986,12 +986,12 @@ def async_run_job[_R, *_Ts](
# 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):
Expand Down Expand Up @@ -1635,10 +1635,10 @@ def async_listen(
# 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):
Expand Down Expand Up @@ -1705,10 +1705,10 @@ def async_listen_once(
# 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(
Expand Down
8 changes: 4 additions & 4 deletions homeassistant/core_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
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
Expand Down Expand Up @@ -695,11 +695,11 @@ def set_time_zone(self, time_zone_str: str) -> None:
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
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/data_entry_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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

Expand Down Expand Up @@ -530,12 +530,12 @@ async def _async_handle_step(

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 (
Expand Down
65 changes: 57 additions & 8 deletions homeassistant/helpers/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import asyncio
from collections.abc import Callable
from dataclasses import dataclass
import enum
import functools
import linecache
import logging
Expand Down Expand Up @@ -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(
Expand Down
20 changes: 12 additions & 8 deletions homeassistant/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -1556,16 +1556,18 @@ def __getattr__(self, comp_name: str) -> ModuleWrapper:
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)
Expand All @@ -1585,16 +1587,18 @@ def __getattr__(self, helper_name: str) -> ModuleWrapper:
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)
Expand Down
91 changes: 91 additions & 0 deletions tests/helpers/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit a3b0909

Please sign in to comment.