From 8401b05d40acdef49d318c57201bfb49f348d6f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 May 2024 19:55:43 -0500 Subject: [PATCH] Move thread safety check in category_registry sooner (#117050) --- homeassistant/helpers/category_registry.py | 9 ++-- tests/helpers/test_category_registry.py | 53 ++++++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/category_registry.py b/homeassistant/helpers/category_registry.py index b0a465314f7..62e9e8339e8 100644 --- a/homeassistant/helpers/category_registry.py +++ b/homeassistant/helpers/category_registry.py @@ -98,6 +98,7 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): icon: str | None = None, ) -> CategoryEntry: """Create a new category.""" + self.hass.verify_event_loop_thread("async_create") self._async_ensure_name_is_available(scope, name) category = CategoryEntry( icon=icon, @@ -110,7 +111,7 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): self.categories[scope][category.category_id] = category self.async_schedule_save() - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_CATEGORY_REGISTRY_UPDATED, EventCategoryRegistryUpdatedData( action="create", scope=scope, category_id=category.category_id @@ -121,8 +122,9 @@ 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") del self.categories[scope][category_id] - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_CATEGORY_REGISTRY_UPDATED, EventCategoryRegistryUpdatedData( action="remove", @@ -155,10 +157,11 @@ class CategoryRegistry(BaseRegistry[CategoryRegistryStoreData]): if not changes: return old + self.hass.verify_event_loop_thread("async_update") new = self.categories[scope][category_id] = dataclasses.replace(old, **changes) # type: ignore[arg-type] self.async_schedule_save() - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_CATEGORY_REGISTRY_UPDATED, EventCategoryRegistryUpdatedData( action="update", scope=scope, category_id=category_id diff --git a/tests/helpers/test_category_registry.py b/tests/helpers/test_category_registry.py index a6a36940a68..7e02d5c5d78 100644 --- a/tests/helpers/test_category_registry.py +++ b/tests/helpers/test_category_registry.py @@ -1,5 +1,6 @@ """Tests for the category registry.""" +from functools import partial import re from typing import Any @@ -394,3 +395,55 @@ async def test_loading_categories_from_storage( assert category3.category_id == "uuid3" assert category3.name == "Grocery stores" assert category3.icon == "mdi:store" + + +async def test_async_create_thread_safety( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """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.", + ): + await hass.async_add_executor_job( + partial(category_registry.async_create, name="any", scope="any") + ) + + +async def test_async_delete_thread_safety( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Test async_delete raises when called from wrong thread.""" + any_category = category_registry.async_create(name="any", scope="any") + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_delete from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial( + category_registry.async_delete, + scope="any", + category_id=any_category.category_id, + ) + ) + + +async def test_async_update_thread_safety( + hass: HomeAssistant, category_registry: cr.CategoryRegistry +) -> None: + """Test async_update raises when called from wrong thread.""" + any_category = category_registry.async_create(name="any", scope="any") + + with pytest.raises( + RuntimeError, + match="Detected code that calls async_update from a thread. Please report this issue.", + ): + await hass.async_add_executor_job( + partial( + category_registry.async_update, + scope="any", + category_id=any_category.category_id, + name="new name", + ) + )