diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index d907b7759dd..661515758de 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -1955,7 +1955,7 @@ class ConfigEntries: if entry.entry_id not in self._entries: raise UnknownEntry(entry.entry_id) - self.hass.verify_event_loop_thread("async_update_entry") + self.hass.verify_event_loop_thread("hass.config_entries.async_update_entry") changed = False _setter = object.__setattr__ diff --git a/homeassistant/core.py b/homeassistant/core.py index 0aa5026d670..f6b0b977fa5 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -439,7 +439,10 @@ class HomeAssistant: # frame is a circular import, so we import it here frame.report( - f"calls {what} from a thread", + f"calls {what} from a thread. " + "For more information, see " + "https://developers.home-assistant.io/docs/asyncio_thread_safety/" + f"#{what.replace('.', '')}", error_if_core=True, error_if_integration=True, ) @@ -802,7 +805,7 @@ class HomeAssistant: # check with a check for the `hass.config.debug` flag being set as # long term we don't want to be checking this in production # environments since it is a performance hit. - self.verify_event_loop_thread("async_create_task") + self.verify_event_loop_thread("hass.async_create_task") return self.async_create_task_internal(target, name, eager_start) @callback @@ -1493,7 +1496,7 @@ class EventBus: This method must be run in the event loop. """ _verify_event_type_length_or_raise(event_type) - self._hass.verify_event_loop_thread("async_fire") + self._hass.verify_event_loop_thread("hass.bus.async_fire") return self.async_fire_internal( event_type, event_data, origin, context, time_fired ) @@ -2506,7 +2509,7 @@ class ServiceRegistry: This method must be run in the event loop. """ - self._hass.verify_event_loop_thread("async_register") + self._hass.verify_event_loop_thread("hass.services.async_register") self._async_register( domain, service, service_func, schema, supports_response, job_type ) @@ -2565,7 +2568,7 @@ class ServiceRegistry: This method must be run in the event loop. """ - self._hass.verify_event_loop_thread("async_remove") + self._hass.verify_event_loop_thread("hass.services.async_remove") self._async_remove(domain, service) @callback diff --git a/homeassistant/helpers/area_registry.py b/homeassistant/helpers/area_registry.py index 56d6b8be224..db208990219 100644 --- a/homeassistant/helpers/area_registry.py +++ b/homeassistant/helpers/area_registry.py @@ -204,7 +204,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): picture: str | None = None, ) -> AreaEntry: """Create a new area.""" - self.hass.verify_event_loop_thread("async_create") + self.hass.verify_event_loop_thread("area_registry.async_create") normalized_name = normalize_name(name) if self.async_get_area_by_name(name): @@ -233,7 +233,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): @callback def async_delete(self, area_id: str) -> None: """Delete area.""" - self.hass.verify_event_loop_thread("async_delete") + self.hass.verify_event_loop_thread("area_registry.async_delete") device_registry = dr.async_get(self.hass) entity_registry = er.async_get(self.hass) device_registry.async_clear_area_id(area_id) @@ -314,7 +314,7 @@ class AreaRegistry(BaseRegistry[AreasRegistryStoreData]): if not new_values: return old - self.hass.verify_event_loop_thread("_async_update") + self.hass.verify_event_loop_thread("area_registry.async_update") new = self.areas[area_id] = dataclasses.replace(old, **new_values) # type: ignore[arg-type] self.async_schedule_save() diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py index 62e9e8339e8..5b22b6d8051 100644 --- a/homeassistant/helpers/category_registry.py +++ b/homeassistant/helpers/category_registry.py @@ -98,7 +98,7 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): icon: str | None = None, ) -> CategoryEntry: """Create a new category.""" - self.hass.verify_event_loop_thread("async_create") + self.hass.verify_event_loop_thread("category_registry.async_create") self._async_ensure_name_is_available(scope, name) category = CategoryEntry( icon=icon, @@ -122,7 +122,7 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): @callback def async_delete(self, *, scope: str, category_id: str) -> None: """Delete category.""" - self.hass.verify_event_loop_thread("async_delete") + self.hass.verify_event_loop_thread("category_registry.async_delete") del self.categories[scope][category_id] self.hass.bus.async_fire_internal( EVENT_CATEGORY_REGISTRY_UPDATED, @@ -157,7 +157,7 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): if not changes: return old - self.hass.verify_event_loop_thread("async_update") + self.hass.verify_event_loop_thread("category_registry.async_update") new = self.categories[scope][category_id] = dataclasses.replace(old, **changes) # type: ignore[arg-type] self.async_schedule_save() diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 2ff80e7c6af..a0bfc751a12 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -906,7 +906,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): if not new_values: return old - self.hass.verify_event_loop_thread("async_update_device") + self.hass.verify_event_loop_thread("device_registry.async_update_device") new = attr.evolve(old, **new_values) self.devices[device_id] = new @@ -933,7 +933,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]): @callback def async_remove_device(self, device_id: str) -> None: """Remove a device from the device registry.""" - self.hass.verify_event_loop_thread("async_remove_device") + self.hass.verify_event_loop_thread("device_registry.async_remove_device") device = self.devices.pop(device_id) self.deleted_devices[device_id] = DeletedDeviceEntry( config_entries=device.config_entries, diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index ac2307feea5..81454db57a7 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -821,7 +821,7 @@ class EntityRegistry(BaseRegistry): unit_of_measurement=unit_of_measurement, ) - self.hass.verify_event_loop_thread("async_get_or_create") + self.hass.verify_event_loop_thread("entity_registry.async_get_or_create") _validate_item( self.hass, domain, @@ -894,7 +894,7 @@ class EntityRegistry(BaseRegistry): @callback def async_remove(self, entity_id: str) -> None: """Remove an entity from registry.""" - self.hass.verify_event_loop_thread("async_remove") + self.hass.verify_event_loop_thread("entity_registry.async_remove") entity = self.entities.pop(entity_id) config_entry_id = entity.config_entry_id key = (entity.domain, entity.platform, entity.unique_id) @@ -1089,7 +1089,7 @@ class EntityRegistry(BaseRegistry): if not new_values: return old - self.hass.verify_event_loop_thread("_async_update_entity") + self.hass.verify_event_loop_thread("entity_registry.async_update_entity") new = self.entities[entity_id] = attr.evolve(old, **new_values) diff --git a/homeassistant/helpers/floor_registry.py b/homeassistant/helpers/floor_registry.py index 4d2faba41b9..6980fdc98c0 100644 --- a/homeassistant/helpers/floor_registry.py +++ b/homeassistant/helpers/floor_registry.py @@ -121,7 +121,7 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]): level: int | None = None, ) -> FloorEntry: """Create a new floor.""" - self.hass.verify_event_loop_thread("async_create") + self.hass.verify_event_loop_thread("floor_registry.async_create") if floor := self.async_get_floor_by_name(name): raise ValueError( f"The name {name} ({floor.normalized_name}) is already in use" @@ -152,7 +152,7 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]): @callback def async_delete(self, floor_id: str) -> None: """Delete floor.""" - self.hass.verify_event_loop_thread("async_delete") + self.hass.verify_event_loop_thread("floor_registry.async_delete") del self.floors[floor_id] self.hass.bus.async_fire_internal( EVENT_FLOOR_REGISTRY_UPDATED, @@ -191,7 +191,7 @@ class FloorRegistry(BaseRegistry[FloorRegistryStoreData]): if not changes: return old - self.hass.verify_event_loop_thread("async_update") + self.hass.verify_event_loop_thread("floor_registry.async_update") new = self.floors[floor_id] = dataclasses.replace(old, **changes) # type: ignore[arg-type] self.async_schedule_save() diff --git a/homeassistant/helpers/issue_registry.py b/homeassistant/helpers/issue_registry.py index 771edf7610d..9b54a3f761f 100644 --- a/homeassistant/helpers/issue_registry.py +++ b/homeassistant/helpers/issue_registry.py @@ -144,7 +144,7 @@ class IssueRegistry(BaseRegistry): translation_placeholders: dict[str, str] | None = None, ) -> IssueEntry: """Get issue. Create if it doesn't exist.""" - self.hass.verify_event_loop_thread("async_get_or_create") + self.hass.verify_event_loop_thread("issue_registry.async_get_or_create") if (issue := self.async_get_issue(domain, issue_id)) is None: issue = IssueEntry( active=True, @@ -204,7 +204,7 @@ class IssueRegistry(BaseRegistry): @callback def async_delete(self, domain: str, issue_id: str) -> None: """Delete issue.""" - self.hass.verify_event_loop_thread("async_delete") + self.hass.verify_event_loop_thread("issue_registry.async_delete") if self.issues.pop((domain, issue_id), None) is None: return @@ -221,7 +221,7 @@ class IssueRegistry(BaseRegistry): @callback def async_ignore(self, domain: str, issue_id: str, ignore: bool) -> IssueEntry: """Ignore issue.""" - self.hass.verify_event_loop_thread("async_ignore") + self.hass.verify_event_loop_thread("issue_registry.async_ignore") old = self.issues[(domain, issue_id)] dismissed_version = ha_version if ignore else None if old.dismissed_version == dismissed_version: diff --git a/homeassistant/helpers/label_registry.py b/homeassistant/helpers/label_registry.py index aaf45fa3aad..d4150f0a3bb 100644 --- a/homeassistant/helpers/label_registry.py +++ b/homeassistant/helpers/label_registry.py @@ -121,7 +121,7 @@ class LabelRegistry(BaseRegistry[LabelRegistryStoreData]): description: str | None = None, ) -> LabelEntry: """Create a new label.""" - self.hass.verify_event_loop_thread("async_create") + self.hass.verify_event_loop_thread("label_registry.async_create") if label := self.async_get_label_by_name(name): raise ValueError( f"The name {name} ({label.normalized_name}) is already in use" @@ -152,7 +152,7 @@ class LabelRegistry(BaseRegistry[LabelRegistryStoreData]): @callback def async_delete(self, label_id: str) -> None: """Delete label.""" - self.hass.verify_event_loop_thread("async_delete") + self.hass.verify_event_loop_thread("label_registry.async_delete") del self.labels[label_id] self.hass.bus.async_fire_internal( EVENT_LABEL_REGISTRY_UPDATED, @@ -192,7 +192,7 @@ class LabelRegistry(BaseRegistry[LabelRegistryStoreData]): if not changes: return old - self.hass.verify_event_loop_thread("async_update") + self.hass.verify_event_loop_thread("label_registry.async_update") new = self.labels[label_id] = dataclasses.replace(old, **changes) # type: ignore[arg-type] self.async_schedule_save() diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 22f1dc8e534..3824442c86e 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -500,7 +500,7 @@ async def test_async_get_or_create_thread_checks( """We raise when trying to create in the wrong thread.""" with pytest.raises( RuntimeError, - match="Detected code that calls async_create from a thread. Please report this issue.", + match="Detected code that calls area_registry.async_create from a thread.", ): await hass.async_add_executor_job(area_registry.async_create, "Mock1") @@ -512,7 +512,7 @@ async def test_async_update_thread_checks( area = area_registry.async_create("Mock1") with pytest.raises( RuntimeError, - match="Detected code that calls _async_update from a thread. Please report this issue.", + match="Detected code that calls area_registry.async_update from a thread.", ): await hass.async_add_executor_job( partial(area_registry.async_update, area.id, name="Mock2") @@ -526,6 +526,6 @@ async def test_async_delete_thread_checks( area = area_registry.async_create("Mock1") with pytest.raises( RuntimeError, - match="Detected code that calls async_delete from a thread. Please report this issue.", + match="Detected code that calls area_registry.async_delete from a thread.", ): await hass.async_add_executor_job(area_registry.async_delete, area.id) diff --git a/tests/helpers/test_category_registry.py b/tests/helpers/test_category_registry.py index 7e02d5c5d78..1800b3babe9 100644 --- a/tests/helpers/test_category_registry.py +++ b/tests/helpers/test_category_registry.py @@ -403,7 +403,7 @@ async def test_async_create_thread_safety( """Test async_create raises when called from wrong thread.""" with pytest.raises( RuntimeError, - match="Detected code that calls async_create from a thread. Please report this issue.", + match="Detected code that calls category_registry.async_create from a thread.", ): await hass.async_add_executor_job( partial(category_registry.async_create, name="any", scope="any") @@ -418,7 +418,7 @@ async def test_async_delete_thread_safety( with pytest.raises( RuntimeError, - match="Detected code that calls async_delete from a thread. Please report this issue.", + match="Detected code that calls category_registry.async_delete from a thread.", ): await hass.async_add_executor_job( partial( @@ -437,7 +437,7 @@ async def test_async_update_thread_safety( with pytest.raises( RuntimeError, - match="Detected code that calls async_update from a thread. Please report this issue.", + match="Detected code that calls category_registry.async_update from a thread.", ): await hass.async_add_executor_job( partial( diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 6b167f8ee49..e40b3ca0356 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -2485,7 +2485,7 @@ async def test_async_get_or_create_thread_safety( with pytest.raises( RuntimeError, - match="Detected code that calls async_update_device from a thread. Please report this issue.", + match="Detected code that calls device_registry.async_update_device from a thread.", ): await hass.async_add_executor_job( partial( @@ -2515,7 +2515,7 @@ async def test_async_remove_device_thread_safety( with pytest.raises( RuntimeError, - match="Detected code that calls async_remove_device from a thread. Please report this issue.", + match="Detected code that calls device_registry.async_remove_device from a thread.", ): await hass.async_add_executor_job( device_registry.async_remove_device, device.id diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index bb0b98c247e..f158dc5b0de 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1984,7 +1984,7 @@ async def test_get_or_create_thread_safety( """Test call async_get_or_create_from a thread.""" with pytest.raises( RuntimeError, - match="Detected code that calls async_get_or_create from a thread. Please report this issue.", + match="Detected code that calls entity_registry.async_get_or_create from a thread.", ): await hass.async_add_executor_job( entity_registry.async_get_or_create, "light", "hue", "1234" @@ -1998,7 +1998,7 @@ async def test_async_update_entity_thread_safety( entry = entity_registry.async_get_or_create("light", "hue", "1234") with pytest.raises( RuntimeError, - match="Detected code that calls _async_update_entity from a thread. Please report this issue.", + match="Detected code that calls entity_registry.async_update_entity from a thread.", ): await hass.async_add_executor_job( partial( @@ -2016,6 +2016,6 @@ async def test_async_remove_thread_safety( entry = entity_registry.async_get_or_create("light", "hue", "1234") with pytest.raises( RuntimeError, - match="Detected code that calls async_remove from a thread. Please report this issue.", + match="Detected code that calls entity_registry.async_remove from a thread.", ): await hass.async_add_executor_job(entity_registry.async_remove, entry.entity_id) diff --git a/tests/helpers/test_floor_registry.py b/tests/helpers/test_floor_registry.py index 80734d11561..95381e82389 100644 --- a/tests/helpers/test_floor_registry.py +++ b/tests/helpers/test_floor_registry.py @@ -367,7 +367,7 @@ async def test_async_create_thread_safety( """Test async_create raises when called from wrong thread.""" with pytest.raises( RuntimeError, - match="Detected code that calls async_create from a thread. Please report this issue.", + match="Detected code that calls floor_registry.async_create from a thread.", ): await hass.async_add_executor_job(floor_registry.async_create, "any") @@ -381,7 +381,7 @@ async def test_async_delete_thread_safety( with pytest.raises( RuntimeError, - match="Detected code that calls async_delete from a thread. Please report this issue.", + match="Detected code that calls floor_registry.async_delete from a thread.", ): await hass.async_add_executor_job(floor_registry.async_delete, any_floor) @@ -395,7 +395,7 @@ async def test_async_update_thread_safety( with pytest.raises( RuntimeError, - match="Detected code that calls async_update from a thread. Please report this issue.", + match="Detected code that calls floor_registry.async_update from a thread.", ): await hass.async_add_executor_job( partial(floor_registry.async_update, any_floor.floor_id, name="new name") diff --git a/tests/helpers/test_issue_registry.py b/tests/helpers/test_issue_registry.py index 19644de8baf..252fb8389d3 100644 --- a/tests/helpers/test_issue_registry.py +++ b/tests/helpers/test_issue_registry.py @@ -367,7 +367,7 @@ async def test_get_or_create_thread_safety( """Test call async_get_or_create_from a thread.""" with pytest.raises( RuntimeError, - match="Detected code that calls async_get_or_create from a thread. Please report this issue.", + match="Detected code that calls issue_registry.async_get_or_create from a thread.", ): await hass.async_add_executor_job( partial( @@ -397,7 +397,7 @@ async def test_async_delete_issue_thread_safety( with pytest.raises( RuntimeError, - match="Detected code that calls async_delete from a thread. Please report this issue.", + match="Detected code that calls issue_registry.async_delete from a thread.", ): await hass.async_add_executor_job( ir.async_delete_issue, @@ -422,7 +422,7 @@ async def test_async_ignore_issue_thread_safety( with pytest.raises( RuntimeError, - match="Detected code that calls async_ignore from a thread. Please report this issue.", + match="Detected code that calls issue_registry.async_ignore from a thread.", ): await hass.async_add_executor_job( ir.async_ignore_issue, hass, "any", "any", True diff --git a/tests/helpers/test_label_registry.py b/tests/helpers/test_label_registry.py index 033bff9e174..af53ef51f98 100644 --- a/tests/helpers/test_label_registry.py +++ b/tests/helpers/test_label_registry.py @@ -464,7 +464,7 @@ async def test_async_create_thread_safety( """Test async_create raises when called from wrong thread.""" with pytest.raises( RuntimeError, - match="Detected code that calls async_create from a thread. Please report this issue.", + match="Detected code that calls label_registry.async_create from a thread.", ): await hass.async_add_executor_job(label_registry.async_create, "any") @@ -478,7 +478,7 @@ async def test_async_delete_thread_safety( with pytest.raises( RuntimeError, - match="Detected code that calls async_delete from a thread. Please report this issue.", + match="Detected code that calls label_registry.async_delete from a thread.", ): await hass.async_add_executor_job(label_registry.async_delete, any_label) @@ -492,7 +492,7 @@ async def test_async_update_thread_safety( with pytest.raises( RuntimeError, - match="Detected code that calls async_update from a thread. Please report this issue.", + match="Detected code that calls label_registry.async_update from a thread.", ): await hass.async_add_executor_job( partial(label_registry.async_update, any_label.label_id, name="new name") diff --git a/tests/test_core.py b/tests/test_core.py index 2dcd23db9a6..0c0f92fa14b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -3442,7 +3442,8 @@ async def test_async_fire_thread_safety(hass: HomeAssistant) -> None: events = async_capture_events(hass, "test_event") hass.bus.async_fire("test_event") with pytest.raises( - RuntimeError, match="Detected code that calls async_fire from a thread." + RuntimeError, + match="Detected code that calls hass.bus.async_fire from a thread.", ): await hass.async_add_executor_job(hass.bus.async_fire, "test_event") @@ -3452,7 +3453,8 @@ async def test_async_fire_thread_safety(hass: HomeAssistant) -> None: async def test_async_register_thread_safety(hass: HomeAssistant) -> None: """Test async_register thread safety.""" with pytest.raises( - RuntimeError, match="Detected code that calls async_register from a thread." + RuntimeError, + match="Detected code that calls hass.services.async_register from a thread.", ): await hass.async_add_executor_job( hass.services.async_register, @@ -3465,7 +3467,8 @@ async def test_async_register_thread_safety(hass: HomeAssistant) -> None: async def test_async_remove_thread_safety(hass: HomeAssistant) -> None: """Test async_remove thread safety.""" with pytest.raises( - RuntimeError, match="Detected code that calls async_remove from a thread." + RuntimeError, + match="Detected code that calls hass.services.async_remove from a thread.", ): await hass.async_add_executor_job( hass.services.async_remove, "test_domain", "test_service" @@ -3479,6 +3482,7 @@ async def test_async_create_task_thread_safety(hass: HomeAssistant) -> None: pass with pytest.raises( - RuntimeError, match="Detected code that calls async_create_task from a thread." + RuntimeError, + match="Detected code that calls hass.async_create_task from a thread.", ): await hass.async_add_executor_job(hass.async_create_task, _any_coro)