From fc3c384e0ac136de733e0dae355e6430777fb10b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 May 2024 15:15:30 -0500 Subject: [PATCH] Move thread safety in label_registry sooner (#117026) --- homeassistant/helpers/label_registry.py | 9 ++++-- tests/helpers/test_label_registry.py | 43 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/label_registry.py b/homeassistant/helpers/label_registry.py index 5c9b1eb066e..aaf45fa3aad 100644 --- a/homeassistant/helpers/label_registry.py +++ b/homeassistant/helpers/label_registry.py @@ -121,6 +121,7 @@ class LabelRegistry(BaseRegistry[LabelRegistryStoreData]): description: str | None = None, ) -> LabelEntry: """Create a new label.""" + self.hass.verify_event_loop_thread("async_create") if label := self.async_get_label_by_name(name): raise ValueError( f"The name {name} ({label.normalized_name}) is already in use" @@ -139,7 +140,7 @@ class LabelRegistry(BaseRegistry[LabelRegistryStoreData]): label_id = label.label_id self.labels[label_id] = label self.async_schedule_save() - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_LABEL_REGISTRY_UPDATED, EventLabelRegistryUpdatedData( action="create", @@ -151,8 +152,9 @@ class LabelRegistry(BaseRegistry[LabelRegistryStoreData]): @callback def async_delete(self, label_id: str) -> None: """Delete label.""" + self.hass.verify_event_loop_thread("async_delete") del self.labels[label_id] - self.hass.bus.async_fire( + self.hass.bus.async_fire_internal( EVENT_LABEL_REGISTRY_UPDATED, EventLabelRegistryUpdatedData( action="remove", @@ -190,10 +192,11 @@ class LabelRegistry(BaseRegistry[LabelRegistryStoreData]): if not changes: return old + self.hass.verify_event_loop_thread("async_update") new = self.labels[label_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_LABEL_REGISTRY_UPDATED, EventLabelRegistryUpdatedData( action="update", diff --git a/tests/helpers/test_label_registry.py b/tests/helpers/test_label_registry.py index 785919b25c0..033bff9e174 100644 --- a/tests/helpers/test_label_registry.py +++ b/tests/helpers/test_label_registry.py @@ -1,5 +1,6 @@ """Tests for the Label Registry.""" +from functools import partial import re from typing import Any @@ -454,3 +455,45 @@ async def test_labels_removed_from_entities( assert len(entries) == 0 entries = er.async_entries_for_label(entity_registry, label2.label_id) assert len(entries) == 0 + + +async def test_async_create_thread_safety( + hass: HomeAssistant, + label_registry: lr.LabelRegistry, +) -> 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(label_registry.async_create, "any") + + +async def test_async_delete_thread_safety( + hass: HomeAssistant, + label_registry: lr.LabelRegistry, +) -> None: + """Test async_delete raises when called from wrong thread.""" + any_label = label_registry.async_create("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(label_registry.async_delete, any_label) + + +async def test_async_update_thread_safety( + hass: HomeAssistant, + label_registry: lr.LabelRegistry, +) -> None: + """Test async_update raises when called from wrong thread.""" + any_label = label_registry.async_create("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(label_registry.async_update, any_label.label_id, name="new name") + )