reacton 1.10.0__tar.gz → 1.10.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reacton-1.10.0 → reacton-1.10.1}/PKG-INFO +1 -1
- reacton-1.10.1/reacton/_version.py +1 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/core.py +77 -9
- {reacton-1.10.0 → reacton-1.10.1}/reacton/core_test.py +24 -0
- reacton-1.10.0/reacton/_version.py +0 -1
- {reacton-1.10.0 → reacton-1.10.1}/.gitignore +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/LICENSE +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/pyproject.toml +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/react_ipywidgets/__init__.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/react_ipywidgets/core.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/__init__.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/bqplot.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/deprecated/__init__.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/deprecated/ipyvuetify.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/find.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/find_test.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/generate.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/generate_test.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/ipycanvas.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/ipyvue.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/ipyvue_test.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/ipyvuetify.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/ipywidgets.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/logging.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/patch.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/patch_display.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/py.typed +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/rx.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/test.vue +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/test_rx.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/utils.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/utils_test.py +0 -0
- {reacton-1.10.0 → reacton-1.10.1}/reacton/work.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.10.1"
|
|
@@ -1166,6 +1166,46 @@ class ComponentContext:
|
|
|
1166
1166
|
TEffect = TypeVar("TEffect", bound="Effect")
|
|
1167
1167
|
|
|
1168
1168
|
|
|
1169
|
+
def _teardown_component_context(context: ComponentContext):
|
|
1170
|
+
"""Empty a component context so a closed render context can be freed by refcounting.
|
|
1171
|
+
|
|
1172
|
+
Closures created during rendering (use_state setters, event handlers) capture their
|
|
1173
|
+
component context and the render context strongly - they must, a setter can be the
|
|
1174
|
+
only reference keeping its component context alive (cf61378). Since those closures
|
|
1175
|
+
are also stored in the hook state and on widget callback dispatchers, a closed tree
|
|
1176
|
+
is one big reference cycle that refcounting cannot free: it waits for a gen-2
|
|
1177
|
+
garbage collection, which on a server shows up as a memory sawtooth scaling with
|
|
1178
|
+
per-kernel state. Emptying the contexts breaks the cycles: whoever still holds a
|
|
1179
|
+
setter or handler keeps only a small hollow context alive, and set_/force_update
|
|
1180
|
+
are no-ops once the render context is closing.
|
|
1181
|
+
"""
|
|
1182
|
+
# replace the containers instead of clearing them: state_get() hands out the live
|
|
1183
|
+
# state dicts (test_state_get closes and re-renders with them), and in general we
|
|
1184
|
+
# only want to drop OUR references, not destroy objects someone else captured
|
|
1185
|
+
context.parent = None
|
|
1186
|
+
context.invoke_element = None
|
|
1187
|
+
context.root_element = None
|
|
1188
|
+
context.root_element_next = None
|
|
1189
|
+
context.elements = {}
|
|
1190
|
+
context.elements_next = {}
|
|
1191
|
+
context.children = {}
|
|
1192
|
+
context.children_next = {}
|
|
1193
|
+
context.widgets = {}
|
|
1194
|
+
context.element_to_widget = {}
|
|
1195
|
+
context.state = {}
|
|
1196
|
+
context.state_metadata = {}
|
|
1197
|
+
context.effects = []
|
|
1198
|
+
context.memo = []
|
|
1199
|
+
context.user_contexts = {}
|
|
1200
|
+
context.user_contexts_prev = {}
|
|
1201
|
+
context.context_listeners = defaultdict(set)
|
|
1202
|
+
context.used_keys = set()
|
|
1203
|
+
context.owns = set()
|
|
1204
|
+
context.exceptions_self = []
|
|
1205
|
+
context.exceptions_children = []
|
|
1206
|
+
context.context_managers = []
|
|
1207
|
+
|
|
1208
|
+
|
|
1169
1209
|
@dataclass
|
|
1170
1210
|
class RerenderReason:
|
|
1171
1211
|
reason: str
|
|
@@ -1267,6 +1307,17 @@ class _RenderContext:
|
|
|
1267
1307
|
def close(self):
|
|
1268
1308
|
with self.thread_lock:
|
|
1269
1309
|
self._closing = True
|
|
1310
|
+
# snapshot the component contexts before _remove_element detaches them from
|
|
1311
|
+
# their parents: detached contexts would escape the teardown below while the
|
|
1312
|
+
# setter/handler closures in their state still reference them and us
|
|
1313
|
+
all_contexts: List[ComponentContext] = []
|
|
1314
|
+
|
|
1315
|
+
def collect(context: ComponentContext):
|
|
1316
|
+
all_contexts.append(context)
|
|
1317
|
+
for child in list(context.children.values()) + list(context.children_next.values()):
|
|
1318
|
+
collect(child)
|
|
1319
|
+
|
|
1320
|
+
collect(self.context_root)
|
|
1270
1321
|
logger.info("Removing elements...")
|
|
1271
1322
|
self._remove_element(self.element, default_key="/", parent_key=ROOT_KEY)
|
|
1272
1323
|
logger.info("Removing elements done.")
|
|
@@ -1281,6 +1332,19 @@ class _RenderContext:
|
|
|
1281
1332
|
orphan_widgets = set([_get_widgets_dict()[k] for k in self._orphans])
|
|
1282
1333
|
raise RuntimeError(f"Orphan widgets not cleaned up for widgets: {orphan_widgets}")
|
|
1283
1334
|
exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
|
|
1335
|
+
# break the reference cycles through the tree (see _teardown_component_context);
|
|
1336
|
+
# _closing stays True, making stray setters and event handlers no-ops
|
|
1337
|
+
for context in all_contexts:
|
|
1338
|
+
_teardown_component_context(context)
|
|
1339
|
+
self.context = None
|
|
1340
|
+
self.context_root = None # type: ignore
|
|
1341
|
+
# the root element, container and root widget keep the widget tree alive, and
|
|
1342
|
+
# widgets reference their elements, whose kwargs hold user callbacks - which
|
|
1343
|
+
# capture use_state setters and therefore this render context
|
|
1344
|
+
self.element = None # type: ignore
|
|
1345
|
+
self.container = None
|
|
1346
|
+
self.last_root_widget = None
|
|
1347
|
+
self._old_element_ids.clear()
|
|
1284
1348
|
if exceptions:
|
|
1285
1349
|
raise exceptions[0]
|
|
1286
1350
|
|
|
@@ -1346,12 +1410,18 @@ class _RenderContext:
|
|
|
1346
1410
|
if DEBUG:
|
|
1347
1411
|
created_stack = traceback.format_stack()
|
|
1348
1412
|
|
|
1349
|
-
#
|
|
1350
|
-
#
|
|
1351
|
-
#
|
|
1352
|
-
#
|
|
1413
|
+
# NOTE: set_ captures self and context strongly, and that is a requirement:
|
|
1414
|
+
# a setter may be the ONLY reference keeping its component context alive
|
|
1415
|
+
# (see cf61378, where weak references lost live state). The resulting
|
|
1416
|
+
# reference cycles through the closed tree are broken by close() emptying
|
|
1417
|
+
# the component contexts instead.
|
|
1353
1418
|
|
|
1354
1419
|
def set_(value):
|
|
1420
|
+
if self._closing:
|
|
1421
|
+
# the render context is closed (or closing) and the tree is (being)
|
|
1422
|
+
# torn down: there is nothing to update. This check must come first:
|
|
1423
|
+
# after close, context.state is empty and reading it would fail.
|
|
1424
|
+
return
|
|
1355
1425
|
if callable(value):
|
|
1356
1426
|
value = value(context.state[key])
|
|
1357
1427
|
logger.info("Set state = %r for key %r (previous value was %r) (%r)", value, key, context.state[key], id(self.context))
|
|
@@ -1384,11 +1454,6 @@ class _RenderContext:
|
|
|
1384
1454
|
equals = eq or utils.equals
|
|
1385
1455
|
should_update = not equals(context.state[key], value) or should_update
|
|
1386
1456
|
|
|
1387
|
-
# if a cleanup during close trigger a state update in a different component, we want to ignore that
|
|
1388
|
-
# otherwise we get a deadlock
|
|
1389
|
-
if self._closing:
|
|
1390
|
-
should_update = False
|
|
1391
|
-
|
|
1392
1457
|
if should_update:
|
|
1393
1458
|
prev_value = context.state[key]
|
|
1394
1459
|
context.state[key] = value
|
|
@@ -1421,6 +1486,9 @@ class _RenderContext:
|
|
|
1421
1486
|
return set_
|
|
1422
1487
|
|
|
1423
1488
|
def force_update(self):
|
|
1489
|
+
if self._closing:
|
|
1490
|
+
# e.g. an event handler on a closed tree routing an exception to us
|
|
1491
|
+
return
|
|
1424
1492
|
# a forced update re-walks the whole tree, no subtree skipping
|
|
1425
1493
|
self._walk_all = True
|
|
1426
1494
|
if not self._is_rendering:
|
|
@@ -3162,6 +3162,30 @@ def test_memory_leak(component):
|
|
|
3162
3162
|
assert weak_rc() is None
|
|
3163
3163
|
|
|
3164
3164
|
|
|
3165
|
+
@reacton.component
|
|
3166
|
+
def UseStateComponent():
|
|
3167
|
+
value, set_value = reacton.use_state(0)
|
|
3168
|
+
return w.Button(description=str(value), on_click=lambda: set_value(value + 1))
|
|
3169
|
+
|
|
3170
|
+
|
|
3171
|
+
@pytest.mark.parametrize("component", [w.Button, ButtonComponentFunction, UseMemoComponent, UseStateComponent])
|
|
3172
|
+
def test_no_reference_cycles_after_close(component):
|
|
3173
|
+
# Stricter than test_memory_leak: the closed render tree must be freed by plain
|
|
3174
|
+
# reference counting, without a garbage collection. If this fails, a closure (a
|
|
3175
|
+
# use_state setter or an event handler) holds the render context strongly and the
|
|
3176
|
+
# tree became one big reference cycle that only a gen-2 gc can free - on a server
|
|
3177
|
+
# that shows up as a memory sawtooth that scales with per-kernel state.
|
|
3178
|
+
box, rc = react.render(component(), handle_error=False)
|
|
3179
|
+
rc.close()
|
|
3180
|
+
weak_rc = weakref.ref(rc)
|
|
3181
|
+
gc.disable()
|
|
3182
|
+
try:
|
|
3183
|
+
del rc
|
|
3184
|
+
assert weak_rc() is None
|
|
3185
|
+
finally:
|
|
3186
|
+
gc.enable()
|
|
3187
|
+
|
|
3188
|
+
|
|
3165
3189
|
def test_fragment():
|
|
3166
3190
|
@reacton.component
|
|
3167
3191
|
def Children():
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.10.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|