Browse Source

Avoid RuntimeError while iterating over bindable properties (#4550)

This PR tries to fix #4419 by disabling the GC while copying the
dictionary of bindable properties. Calling `list()` iterates over the
dictionary, which can case a "RuntimeError: dictionary changed size
during iteration" if the garbage collector runs at the same time and
causes the finalizers to pop items from the dictionary.

The PR is still a draft because there is another iteration over
`bindable_properties` in the `_make_copyable` function. So technically
there is still a chance for the RuntimeError.

1. So we might need to disable the GC once again in `_make_copyable`.
2. Or shouldn't the finalizers pop themselves, but add to a set of
obsolete keys instead?
3. Alternatively we could try to use some kind of lock.
Falko Schindler 1 tháng trước cách đây
mục cha
commit
2fe5b2a439
1 tập tin đã thay đổi với 12 bổ sung5 xóa
  1. 12 5
      nicegui/binding.py

+ 12 - 5
nicegui/binding.py

@@ -3,6 +3,7 @@ from __future__ import annotations
 import asyncio
 import copyreg
 import dataclasses
+import threading
 import time
 import weakref
 from collections import defaultdict
@@ -35,6 +36,7 @@ MAX_PROPAGATION_TIME = 0.01
 
 bindings: DefaultDict[Tuple[int, str], List] = defaultdict(list)
 bindable_properties: Dict[Tuple[int, str], weakref.finalize] = {}
+bindable_properties_lock = threading.Lock()
 active_links: List[Tuple[Any, str, Any, str, Callable[[Any], Any]]] = []
 
 TC = TypeVar('TC', bound=type)
@@ -179,7 +181,11 @@ class BindableProperty:
             return
         setattr(owner, '___' + self.name, value)
         key = (id(owner), str(self.name))
-        bindable_properties.setdefault(key, weakref.finalize(owner, lambda: bindable_properties.pop(key, None)))
+
+        def remove_bindable_property():
+            with bindable_properties_lock:
+                bindable_properties.pop(key, None)
+        bindable_properties.setdefault(key, weakref.finalize(owner, remove_bindable_property))
         _propagate(owner, self.name)
         if value_changed and self._change_handler is not None:
             self._change_handler(owner, value)
@@ -204,10 +210,11 @@ def remove(objects: Iterable[Any]) -> None:
         ]
         if not binding_list:
             del bindings[key]
-    for (obj_id, name), finalizer in list(bindable_properties.items()):
-        if obj_id in object_ids:
-            del bindable_properties[(obj_id, name)]
-            finalizer.detach()
+    with bindable_properties_lock:
+        for (obj_id, name), finalizer in list(bindable_properties.items()):
+            if obj_id in object_ids:
+                del bindable_properties[(obj_id, name)]
+                finalizer.detach()
 
 
 def reset() -> None: