Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new frame helper to better distinguish custom and core integrations #130025

Merged
merged 7 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
from enum import Enum
epenet marked this conversation as resolved.
Show resolved Hide resolved
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 == ReportBehavior.LOG:
epenet marked this conversation as resolved.
Show resolved Hide resolved
core_behavior = ReportBehavior.IGNORE
if core_integration_behavior == ReportBehavior.LOG:
epenet marked this conversation as resolved.
Show resolved Hide resolved
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):
epenet marked this conversation as resolved.
Show resolved Hide resolved
"""Enum for behavior on code usage."""

IGNORE = 1
"""Ignore the code usage."""
LOG = 2
"""Log the code usage."""
ERROR = 3
"""Raise an error on code usage."""
epenet marked this conversation as resolved.
Show resolved Hide resolved

epenet marked this conversation as resolved.
Show resolved Hide resolved

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 == ReportBehavior.ERROR:
epenet marked this conversation as resolved.
Show resolved Hide resolved
raise RuntimeError(msg) from err
if not log_custom_component_only:
if core_behavior == ReportBehavior.LOG:
epenet marked this conversation as resolved.
Show resolved Hide resolved
_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 != ReportBehavior.IGNORE:
epenet marked this conversation as resolved.
Show resolved Hide resolved
_report_integration(
what, integration_frame, level, integration_behavior == ReportBehavior.ERROR
epenet marked this conversation as resolved.
Show resolved Hide resolved
)


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
Loading