Skip to content

Commit

Permalink
Add ability to pass integration domain to report_usage (#130705)
Browse files Browse the repository at this point in the history
* Add ability to pass integration domain to report_usage

* Adjust

* Fix

* Add tests

* Update test_frame.py

* Update test_frame.py

* Update test_frame.py

* Update test_frame.py

* Update test_frame.py

* Update test_frame.py

* Finish tests

* Docstring

* Replace logger warning with report_usage

* Improve

* docstring

* Improve tests

* Adjust docstring for exclude_integrations

* Fix behavior and improve tests
  • Loading branch information
epenet authored Nov 21, 2024
1 parent 42dcfae commit 9add3a6
Show file tree
Hide file tree
Showing 4 changed files with 178 additions and 22 deletions.
18 changes: 6 additions & 12 deletions homeassistant/config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -2887,18 +2887,12 @@ def async_create_entry( # type: ignore[override]
) -> ConfigFlowResult:
"""Finish config flow and create a config entry."""
if self.source in {SOURCE_REAUTH, SOURCE_RECONFIGURE}:
report_issue = async_suggest_report_issue(
self.hass, integration_domain=self.handler
)
_LOGGER.warning(
(
"Detected %s config flow creating a new entry, "
"when it is expected to update an existing entry and abort. "
"This will stop working in %s, please %s"
),
self.source,
"2025.11",
report_issue,
report_usage(
f"creates a new entry in a '{self.source}' flow, "
"when it is expected to update an existing entry and abort",
core_behavior=ReportBehavior.LOG,
breaks_in_ha_version="2025.11",
integration_domain=self.handler,
)
result = super().async_create_entry(
title=title,
Expand Down
82 changes: 77 additions & 5 deletions homeassistant/helpers/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@

from propcache import cached_property

from homeassistant.core import async_get_hass_or_none
from homeassistant.core import HomeAssistant, async_get_hass_or_none
from homeassistant.exceptions import HomeAssistantError
from homeassistant.loader import async_suggest_report_issue
from homeassistant.loader import (
Integration,
async_get_issue_integration,
async_suggest_report_issue,
)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -186,6 +190,7 @@ def report_usage(
core_integration_behavior: ReportBehavior = ReportBehavior.LOG,
custom_integration_behavior: ReportBehavior = ReportBehavior.LOG,
exclude_integrations: set[str] | None = None,
integration_domain: str | None = None,
level: int = logging.WARNING,
) -> None:
"""Report incorrect code usage.
Expand All @@ -194,12 +199,29 @@ def report_usage(
Please create a bug report at https://..."
:param breaks_in_ha_version: if set, the report will be adjusted to specify the
breaking version
:param exclude_integrations: skip specified integration when reviewing the stack.
If no integration is found, the core behavior will be applied
:param integration_domain: fallback for identifying the integration if the
frame is not found
"""
try:
integration_frame = get_integration_frame(
exclude_integrations=exclude_integrations
)
except MissingIntegrationFrame as err:
if integration := async_get_issue_integration(
hass := async_get_hass_or_none(), integration_domain
):
_report_integration_domain(
hass,
what,
breaks_in_ha_version,
integration,
core_integration_behavior,
custom_integration_behavior,
level,
)
return
msg = f"Detected code that {what}. Please report this issue"
if core_behavior is ReportBehavior.ERROR:
raise RuntimeError(msg) from err
Expand All @@ -217,7 +239,7 @@ def report_usage(
integration_behavior = custom_integration_behavior

if integration_behavior is not ReportBehavior.IGNORE:
_report_integration(
_report_integration_frame(
what,
breaks_in_ha_version,
integration_frame,
Expand All @@ -226,14 +248,64 @@ def report_usage(
)


def _report_integration(
def _report_integration_domain(
hass: HomeAssistant | None,
what: str,
breaks_in_ha_version: str | None,
integration: Integration,
core_integration_behavior: ReportBehavior,
custom_integration_behavior: ReportBehavior,
level: int,
) -> None:
"""Report incorrect usage in an integration (identified via domain).
Async friendly.
"""
integration_behavior = core_integration_behavior
if not integration.is_built_in:
integration_behavior = custom_integration_behavior

if integration_behavior is ReportBehavior.IGNORE:
return

# Keep track of integrations already reported to prevent flooding
key = f"{integration.domain}:{what}"
if (
integration_behavior is not ReportBehavior.ERROR
and key in _REPORTED_INTEGRATIONS
):
return
_REPORTED_INTEGRATIONS.add(key)

report_issue = async_suggest_report_issue(hass, integration=integration)
integration_type = "" if integration.is_built_in else "custom "
_LOGGER.log(
level,
"Detected that %sintegration '%s' %s. %s %s",
integration_type,
integration.domain,
what,
f"This will stop working in Home Assistant {breaks_in_ha_version}, please"
if breaks_in_ha_version
else "Please",
report_issue,
)

if integration_behavior is ReportBehavior.ERROR:
raise RuntimeError(
f"Detected that {integration_type}integration "
f"'{integration.domain}' {what}. Please {report_issue}"
)


def _report_integration_frame(
what: str,
breaks_in_ha_version: str | None,
integration_frame: IntegrationFrame,
level: int = logging.WARNING,
error: bool = False,
) -> None:
"""Report incorrect usage in an integration.
"""Report incorrect usage in an integration (identified via frame).
Async friendly.
"""
Expand Down
87 changes: 87 additions & 0 deletions tests/helpers/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from homeassistant.core import HomeAssistant
from homeassistant.helpers import frame
from homeassistant.loader import async_get_integration

from tests.common import extract_stack_to_frame

Expand Down Expand Up @@ -445,3 +446,89 @@ async def test_report(
assert errored == expected_error

assert caplog.text.count(what) == expected_log


@pytest.mark.parametrize(
("behavior", "integration_domain", "source", "logs_again"),
[
pytest.param(
"core_behavior",
None,
"code that",
True,
id="core",
),
pytest.param(
"core_behavior",
"unknown_integration",
"code that",
True,
id="unknown integration",
),
pytest.param(
"core_integration_behavior",
"sensor",
"that integration 'sensor'",
False,
id="core integration",
),
pytest.param(
"custom_integration_behavior",
"test_package",
"that custom integration 'test_package'",
False,
id="custom integration",
),
],
)
@pytest.mark.usefixtures("enable_custom_integrations")
async def test_report_integration_domain(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
behavior: str,
integration_domain: str | None,
source: str,
logs_again: bool,
) -> None:
"""Test report."""
await async_get_integration(hass, "sensor")
await async_get_integration(hass, "test_package")

what = "test_report_string"
lookup_text = f"Detected {source} {what}"

caplog.clear()
frame.report_usage(
what,
**{behavior: frame.ReportBehavior.IGNORE},
integration_domain=integration_domain,
)

assert lookup_text not in caplog.text

with patch.object(frame, "_REPORTED_INTEGRATIONS", set()):
frame.report_usage(
what,
**{behavior: frame.ReportBehavior.LOG},
integration_domain=integration_domain,
)

assert lookup_text in caplog.text

# Check that it does not log again
caplog.clear()
frame.report_usage(
what,
**{behavior: frame.ReportBehavior.LOG},
integration_domain=integration_domain,
)

assert (lookup_text in caplog.text) == logs_again

# Check that it raises
with pytest.raises(RuntimeError, match=lookup_text):
frame.report_usage(
what,
**{behavior: frame.ReportBehavior.ERROR},
integration_domain=integration_domain,
)
13 changes: 8 additions & 5 deletions tests/test_config_entries.py
Original file line number Diff line number Diff line change
Expand Up @@ -7157,7 +7157,10 @@ async def _async_step_confirm(self):

assert len(hass.config_entries.async_entries("test")) == 1

with mock_config_flow("test", TestFlow):
with (
mock_config_flow("test", TestFlow),
patch.object(frame, "_REPORTED_INTEGRATIONS", set()),
):
result = await getattr(entry, f"start_{source}_flow")(hass)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.CREATE_ENTRY
Expand All @@ -7169,10 +7172,10 @@ async def _async_step_confirm(self):
assert entries[0].entry_id != entry.entry_id

assert (
f"Detected {source} config flow creating a new entry, when it is expected "
"to update an existing entry and abort. This will stop working in "
"2025.11, please create a bug report at https://github.com/home"
"-assistant/core/issues?q=is%3Aopen+is%3Aissue+"
f"Detected that integration 'test' creates a new entry in a '{source}' flow, "
"when it is expected to update an existing entry and abort. This will stop "
"working in Home Assistant 2025.11, please create a bug report at "
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+"
"label%3A%22integration%3A+test%22"
) in caplog.text

Expand Down

0 comments on commit 9add3a6

Please sign in to comment.