Browse Source

Make reloader re entrant

jean-robin medori 1 tuần trước cách đây
mục cha
commit
83eb0a973b
2 tập tin đã thay đổi với 95 bổ sung5 xóa
  1. 8 5
      taipy/core/_entity/_reload.py
  2. 87 0
      tests/core/_entity/test_reload.py

+ 8 - 5
taipy/core/_entity/_reload.py

@@ -11,6 +11,7 @@
 
 import functools
 from typing import Dict, Type
+import threading
 
 from ...common._check_dependencies import EnterpriseEditionUtils
 from .._manager._manager import _Manager
@@ -22,13 +23,12 @@ class _Reloader:
     """The _Reloader singleton class"""
 
     _instance = None
-    _no_reload_context = False
-
-    _managers: Dict[str, Type[_Manager]] = {}
 
     def __new__(cls, *args, **kwargs):
         if not isinstance(cls._instance, cls):
-            cls._instance = object.__new__(cls, *args, **kwargs)
+            cls._instance = super().__new__(cls *args, **kwargs)
+            cls._instance._no_reload_context = False  # Initialize once
+            cls._instance._context_depth = 0  # Track nested `with` usage
             cls._managers = cls._build_managers()
         return cls._instance
 
@@ -46,11 +46,14 @@ class _Reloader:
         return entity
 
     def __enter__(self):
+        self._context_depth += 1
         self._no_reload_context = True
         return self
 
     def __exit__(self, exc_type, exc_value, exc_traceback):
-        self._no_reload_context = False
+        self._context_depth -= 1
+        if self._context_depth == 0:
+            self._no_reload_context = False
 
     @classmethod
     @functools.lru_cache

+ 87 - 0
tests/core/_entity/test_reload.py

@@ -0,0 +1,87 @@
+# Copyright 2021-2025 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+import pytest
+
+import taipy as tp
+from taipy import Config
+from taipy.core._entity._reload import _Reloader
+
+
+@pytest.fixture(scope="function", autouse=True)
+def reset_reloader():
+    """Reset the _Reloader singleton between tests."""
+    _Reloader._instance = None
+    _Reloader._no_reload_context = False
+    _Reloader._context_depth = 0
+    yield
+    _Reloader._instance = None
+    _Reloader._no_reload_context = False
+    _Reloader._context_depth = 0
+
+
+class TestReloader:
+    def test_initial_state(self):
+        reloader = _Reloader()
+        assert reloader._context_depth == 0
+        assert not reloader._no_reload_context
+
+    def test_single_context(self):
+        dn = tp.create_global_data_node(Config.configure_data_node("dn1", scope=tp.Scope.GLOBAL))
+        assert len(dn.edits) == 0
+        dn.track_edit(comments="inside") # creates a new edit in memory without saving the data node
+        reloader = _Reloader()
+        with reloader:
+            assert reloader._context_depth == 1
+            assert reloader._no_reload_context
+            assert len(dn.edits) == 1 # The dn is not reloaded so the edit is still there
+        assert reloader._context_depth == 0
+        assert not reloader._no_reload_context
+        assert len(dn.edits) == 0 # The dn is reloaded so the edit is removed
+
+    def test_nested_contexts(self):
+        dn = tp.create_global_data_node(Config.configure_data_node("dn1", scope=tp.Scope.GLOBAL))
+        assert len(dn.edits) == 0
+        dn.track_edit(comments="inside") # creates a new edit in memory without saving the data node
+        reloader = _Reloader()
+        with reloader:
+            assert reloader._context_depth == 1
+            assert reloader._no_reload_context
+            assert len(dn.edits) == 1 # The dn is not reloaded so the edit is still there
+            with reloader:
+                assert reloader._context_depth == 2
+                assert reloader._no_reload_context
+                assert len(dn.edits) == 1 # The dn is not reloaded so the edit is still there
+                with reloader:
+                    assert reloader._context_depth == 3
+                    assert reloader._no_reload_context
+                    assert len(dn.edits) == 1 # The dn is not reloaded so the edit is still there
+                assert reloader._context_depth == 2
+                assert reloader._no_reload_context
+                assert len(dn.edits) == 1 # The dn is not reloaded so the edit is still there
+            assert reloader._context_depth == 1
+            assert reloader._no_reload_context
+            assert len(dn.edits) == 1 # The dn is not reloaded so the edit is still there
+        assert reloader._context_depth == 0
+        assert not reloader._no_reload_context
+        assert len(dn.edits) == 0 # The dn is reloaded so the edit is removed
+
+    def test_exception_handling(self):
+        reloader = _Reloader()
+        try:
+            with reloader:
+                assert reloader._context_depth == 1
+                assert reloader._no_reload_context
+                raise ValueError("Test exception")
+        except ValueError:
+            pass
+
+        assert reloader._context_depth == 0
+        assert not reloader._no_reload_context