diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 7b49a6b1b0d..86982cb428f 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -27,6 +27,7 @@ from homeassistant.core import ( SupportsResponse, ) from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass @@ -249,6 +250,16 @@ def execute(hass, filename, source, data=None, return_response=False): if return_response: raise ServiceValidationError(f"Error executing script: {err}") from err logger.error("Error executing script: %s", err) + ir.create_issue( + hass, + DOMAIN, + filename, + breaks_in_ha_version="2024.4.0", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="script_continue_on_error", + translation_placeholders={"script_name": filename.replace(".py", "")}, + ) return None except Exception as err: # pylint: disable=broad-except if return_response: @@ -256,6 +267,16 @@ def execute(hass, filename, source, data=None, return_response=False): f"Error executing script ({type(err).__name__}): {err}" ) from err logger.exception("Error executing script: %s", err) + ir.create_issue( + hass, + DOMAIN, + filename, + breaks_in_ha_version="2024.4.0", + is_fixable=False, + severity=ir.IssueSeverity.ERROR, + translation_key="script_continue_on_error", + translation_placeholders={"script_name": filename.replace(".py", "")}, + ) return None return restricted_globals["output"] diff --git a/homeassistant/components/python_script/strings.json b/homeassistant/components/python_script/strings.json index ccf1b33c767..06217784be3 100644 --- a/homeassistant/components/python_script/strings.json +++ b/homeassistant/components/python_script/strings.json @@ -4,5 +4,11 @@ "name": "[%key:common::action::reload%]", "description": "Reloads all available Python scripts." } + }, + "issues": { + "script_continue_on_error": { + "title": "'python_script.{script_name}' logs errors and continues", + "description": "The `python_script` integration will change to raise script errors instead of logging them.\n\nThis will **stop** automations/scripts if `continue_on_error` is not set ([docs](https://www.home-assistant.io/docs/scripts/#continuing-on-error)).\n\nTo enable the new behavior, add a `response_variable` ([docs](https://www.home-assistant.io/docs/scripts/service-calls/#use-templates-to-handle-response-data)) to your service call." + } } } diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index ee7fedee0d5..5237a4fcfc4 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -7,6 +7,7 @@ import pytest from homeassistant.components.python_script import DOMAIN, FOLDER, execute from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component @@ -123,7 +124,9 @@ this is not valid Python async def test_execute_runtime_error( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + issue_registry: ir.IssueRegistry, ) -> None: """Test compile error logs error.""" caplog.set_level(logging.ERROR) @@ -135,9 +138,12 @@ raise Exception('boom') await hass.async_block_till_done() assert "Error executing script: boom" in caplog.text + assert len(issue_registry.issues) == 1 -async def test_execute_runtime_error_with_response(hass: HomeAssistant) -> None: +async def test_execute_runtime_error_with_response( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: """Test compile error logs error.""" source = """ raise Exception('boom') @@ -148,10 +154,13 @@ raise Exception('boom') assert type(task.exception()) == HomeAssistantError assert "Error executing script (Exception): boom" in str(task.exception()) + assert len(issue_registry.issues) == 0 async def test_accessing_async_methods( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + issue_registry: ir.IssueRegistry, ) -> None: """Test compile error logs error.""" caplog.set_level(logging.ERROR) @@ -163,9 +172,12 @@ hass.async_stop() await hass.async_block_till_done() assert "Not allowed to access async methods" in caplog.text + assert len(issue_registry.issues) == 1 -async def test_accessing_async_methods_with_response(hass: HomeAssistant) -> None: +async def test_accessing_async_methods_with_response( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: """Test compile error logs error.""" source = """ hass.async_stop() @@ -176,6 +188,7 @@ hass.async_stop() assert type(task.exception()) == ServiceValidationError assert "Not allowed to access async methods" in str(task.exception()) + assert len(issue_registry.issues) == 0 async def test_using_complex_structures( @@ -196,7 +209,9 @@ logger.info('Logging from inside script: %s %s' % (mydict["a"], mylist[2])) async def test_accessing_forbidden_methods( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + issue_registry: ir.IssueRegistry, ) -> None: """Test compile error logs error.""" caplog.set_level(logging.ERROR) @@ -208,12 +223,16 @@ async def test_accessing_forbidden_methods( "time.tzset()": "TimeWrapper.tzset", }.items(): caplog.records.clear() + issue_registry.async_delete("python_script", "test.py") hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert f"Not allowed to access {name}" in caplog.text + assert len(issue_registry.issues) == 1 -async def test_accessing_forbidden_methods_with_response(hass: HomeAssistant) -> None: +async def test_accessing_forbidden_methods_with_response( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: """Test compile error logs error.""" for source, name in { "hass.stop()": "HomeAssistant.stop", @@ -226,6 +245,7 @@ async def test_accessing_forbidden_methods_with_response(hass: HomeAssistant) -> assert type(task.exception()) == ServiceValidationError assert f"Not allowed to access {name}" in str(task.exception()) + assert len(issue_registry.issues) == 0 async def test_iterating(hass: HomeAssistant) -> None: