From 723f291cf341f3f5d71ea651ec30de6397984e9b Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Wed, 14 Oct 2026 21:14:48 +0200 Subject: [PATCH 1/2] fix(abstractions): subscribe list items once and keep list sizes fresh in the backing store Each time a list was set, every BackedModel in it got one more subscription. The callback sets the list again, so each change to an item doubled its subscriptions. Items now subscribe under the store's own id plus the property name. A repeat subscription from the same store replaces the old one, and a child shared by two stores still notifies both. The is_initialization_completed setter saved the entry it read before the size check. A list that had grown was saved with its old size and showed up as changed again on the next enumerate_. It now saves the entry after the check, as the C# store does. --- .../store/in_memory_backing_store.py | 5 ++- .../store/test_in_memory_backing_store.py | 31 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/abstractions/kiota_abstractions/store/in_memory_backing_store.py b/packages/abstractions/kiota_abstractions/store/in_memory_backing_store.py index fef8c809..2ee2228e 100644 --- a/packages/abstractions/kiota_abstractions/store/in_memory_backing_store.py +++ b/packages/abstractions/kiota_abstractions/store/in_memory_backing_store.py @@ -27,6 +27,7 @@ class InMemoryBackingStore(BackingStore[T], Generic[T]): def __init__(self) -> None: + self.__id = str(uuid4()) self.__subscriptions: dict[str, Callable[[str, Any, Any], None]] = {} self.__store: dict[str, tuple[bool, T, Optional[_Collection]]] = {} self.__initialization_completed: bool = False @@ -45,6 +46,7 @@ def is_initialization_completed(self, value: bool) -> None: # forward the initialization status to nested BackedModel instances val.backing_store.is_initialization_completed = value self._ensure_collection_size_is_consistent(key, val, collection) + _, val, collection = self.__store[key] self.__store[key] = (not value, val, collection) @property @@ -110,7 +112,8 @@ def set(self, key: str, value: T) -> None: if isinstance(item, BackedModel) and item.backing_store: item.backing_store.is_initialization_completed = True item.backing_store.subscribe( - lambda prop_key, old_val, new_val: self.set(key, value) + lambda prop_key, old_val, new_val: self.set(key, value), + f"{self.__id}:{key}" ) self.__store[key] = value_to_add diff --git a/packages/abstractions/tests/store/test_in_memory_backing_store.py b/packages/abstractions/tests/store/test_in_memory_backing_store.py index dc08615a..60512002 100644 --- a/packages/abstractions/tests/store/test_in_memory_backing_store.py +++ b/packages/abstractions/tests/store/test_in_memory_backing_store.py @@ -228,3 +228,34 @@ def test_subscribers_get_previous_and_new_values(): backing_store.set("name", "Samwel") backing_store.set("name", "Bill") assert changes == [("name", None, "Samwel"), ("name", "Samwel", "Bill")] + +def test_list_items_notify_the_store_once_per_change(): + backing_store = InMemoryBackingStore() + colleague = MockEntity(id="2f8c9b8c-8ea0-46b2-9c6d-660c9b7bf6af") + backing_store.set("colleagues", [colleague]) + backing_store.set("colleagues", [colleague]) + changes = [] + backing_store.subscribe(lambda key, old, new: changes.append(key)) + for new_id in ("first", "second", "third"): + colleague.id = new_id + assert changes == ["colleagues"] * 3 + +def test_list_item_shared_by_two_stores_notifies_both(): + first, second = InMemoryBackingStore(), InMemoryBackingStore() + colleague = MockEntity(id="2f8c9b8c-8ea0-46b2-9c6d-660c9b7bf6af") + first.set("colleagues", [colleague]) + second.set("colleagues", [colleague]) + changes = [] + first.subscribe(lambda key, old, new: changes.append("first")) + second.subscribe(lambda key, old, new: changes.append("second")) + colleague.id = "2fe22fe5-1132-42cf-90f9-1dc17e325a74" + assert changes == ["first", "second"] + +def test_completing_initialization_clears_a_resized_list(): + backing_store = InMemoryBackingStore() + numbers = [1] + backing_store.set("numbers", numbers) + numbers.append(2) + backing_store.is_initialization_completed = True + backing_store.return_only_changed_values = True + assert not backing_store.enumerate_() From 0fa3b1f7b75d729f0bdc43421a5f25675c1d2907 Mon Sep 17 00:00:00 2001 From: HardMax71 Date: Thu, 15 Oct 2026 13:41:18 +0200 Subject: [PATCH 2/2] fix(abstractions): loop over the store keys in the initialization setter pylint R1733 flagged reading store[key] inside the items() loop. The size check can replace the entry, so the loop now goes over a copy of the keys and reads each entry from the store, like the C# setter. --- .../kiota_abstractions/store/in_memory_backing_store.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/abstractions/kiota_abstractions/store/in_memory_backing_store.py b/packages/abstractions/kiota_abstractions/store/in_memory_backing_store.py index 2ee2228e..34ece180 100644 --- a/packages/abstractions/kiota_abstractions/store/in_memory_backing_store.py +++ b/packages/abstractions/kiota_abstractions/store/in_memory_backing_store.py @@ -41,7 +41,8 @@ def is_initialization_completed(self) -> bool: @is_initialization_completed.setter def is_initialization_completed(self, value: bool) -> None: self.__initialization_completed = value - for key, (_, val, collection) in self.__store.items(): + for key in list(self.__store): + _, val, collection = self.__store[key] if isinstance(val, BackedModel): # forward the initialization status to nested BackedModel instances val.backing_store.is_initialization_completed = value