reacton 1.10.0__tar.gz → 1.10.2__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.2}/PKG-INFO +1 -1
- reacton-1.10.2/reacton/_version.py +1 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/core.py +97 -19
- {reacton-1.10.0 → reacton-1.10.2}/reacton/core_test.py +84 -0
- reacton-1.10.0/reacton/_version.py +0 -1
- {reacton-1.10.0 → reacton-1.10.2}/.gitignore +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/LICENSE +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/pyproject.toml +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/react_ipywidgets/__init__.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/react_ipywidgets/core.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/__init__.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/bqplot.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/deprecated/__init__.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/deprecated/ipyvuetify.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/find.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/find_test.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/generate.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/generate_test.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/ipycanvas.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/ipyvue.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/ipyvue_test.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/ipyvuetify.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/ipywidgets.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/logging.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/patch.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/patch_display.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/py.typed +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/rx.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/test.vue +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/test_rx.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/utils.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/utils_test.py +0 -0
- {reacton-1.10.0 → reacton-1.10.2}/reacton/work.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.10.2"
|
|
@@ -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,20 +1307,48 @@ 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.")
|
|
1273
1324
|
assert self.context is self.context_root
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1325
|
+
# everything below used to run outside the lock: a render() that was
|
|
1326
|
+
# blocked on the lock could then interleave with this teardown and
|
|
1327
|
+
# find self.context None mid-render (AssertionError in
|
|
1328
|
+
# Element.__exit__, seen in production behind a prompt kernel close)
|
|
1329
|
+
if self.container:
|
|
1330
|
+
self.container.close()
|
|
1331
|
+
if isinstance(self.container, widgets.DOMWidget) and self.container.layout is not None:
|
|
1332
|
+
self.container.layout.close()
|
|
1333
|
+
if self._shared_elements:
|
|
1334
|
+
raise RuntimeError(f"Element not cleaned up: {self._shared_elements}")
|
|
1335
|
+
if self._orphans:
|
|
1336
|
+
orphan_widgets = set([_get_widgets_dict()[k] for k in self._orphans])
|
|
1337
|
+
raise RuntimeError(f"Orphan widgets not cleaned up for widgets: {orphan_widgets}")
|
|
1338
|
+
exceptions = [*self.context.exceptions_children, *self.context_root.exceptions_self]
|
|
1339
|
+
# break the reference cycles through the tree (see _teardown_component_context);
|
|
1340
|
+
# _closing stays True, making stray setters and event handlers no-ops
|
|
1341
|
+
for context in all_contexts:
|
|
1342
|
+
_teardown_component_context(context)
|
|
1343
|
+
self.context = None
|
|
1344
|
+
self.context_root = None # type: ignore
|
|
1345
|
+
# the root element, container and root widget keep the widget tree alive, and
|
|
1346
|
+
# widgets reference their elements, whose kwargs hold user callbacks - which
|
|
1347
|
+
# capture use_state setters and therefore this render context
|
|
1348
|
+
self.element = None # type: ignore
|
|
1349
|
+
self.container = None
|
|
1350
|
+
self.last_root_widget = None
|
|
1351
|
+
self._old_element_ids.clear()
|
|
1284
1352
|
if exceptions:
|
|
1285
1353
|
raise exceptions[0]
|
|
1286
1354
|
|
|
@@ -1346,12 +1414,18 @@ class _RenderContext:
|
|
|
1346
1414
|
if DEBUG:
|
|
1347
1415
|
created_stack = traceback.format_stack()
|
|
1348
1416
|
|
|
1349
|
-
#
|
|
1350
|
-
#
|
|
1351
|
-
#
|
|
1352
|
-
#
|
|
1417
|
+
# NOTE: set_ captures self and context strongly, and that is a requirement:
|
|
1418
|
+
# a setter may be the ONLY reference keeping its component context alive
|
|
1419
|
+
# (see cf61378, where weak references lost live state). The resulting
|
|
1420
|
+
# reference cycles through the closed tree are broken by close() emptying
|
|
1421
|
+
# the component contexts instead.
|
|
1353
1422
|
|
|
1354
1423
|
def set_(value):
|
|
1424
|
+
if self._closing:
|
|
1425
|
+
# the render context is closed (or closing) and the tree is (being)
|
|
1426
|
+
# torn down: there is nothing to update. This check must come first:
|
|
1427
|
+
# after close, context.state is empty and reading it would fail.
|
|
1428
|
+
return
|
|
1355
1429
|
if callable(value):
|
|
1356
1430
|
value = value(context.state[key])
|
|
1357
1431
|
logger.info("Set state = %r for key %r (previous value was %r) (%r)", value, key, context.state[key], id(self.context))
|
|
@@ -1384,11 +1458,6 @@ class _RenderContext:
|
|
|
1384
1458
|
equals = eq or utils.equals
|
|
1385
1459
|
should_update = not equals(context.state[key], value) or should_update
|
|
1386
1460
|
|
|
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
1461
|
if should_update:
|
|
1393
1462
|
prev_value = context.state[key]
|
|
1394
1463
|
context.state[key] = value
|
|
@@ -1421,6 +1490,9 @@ class _RenderContext:
|
|
|
1421
1490
|
return set_
|
|
1422
1491
|
|
|
1423
1492
|
def force_update(self):
|
|
1493
|
+
if self._closing:
|
|
1494
|
+
# e.g. an event handler on a closed tree routing an exception to us
|
|
1495
|
+
return
|
|
1424
1496
|
# a forced update re-walks the whole tree, no subtree skipping
|
|
1425
1497
|
self._walk_all = True
|
|
1426
1498
|
if not self._is_rendering:
|
|
@@ -1479,6 +1551,12 @@ class _RenderContext:
|
|
|
1479
1551
|
self._lock_thread = threading.current_thread()
|
|
1480
1552
|
if was_locked:
|
|
1481
1553
|
logger.info("Mutex released, continuing render phase")
|
|
1554
|
+
if self._closing or self.context is None:
|
|
1555
|
+
# close() won the race for the lock (a disconnect can close the
|
|
1556
|
+
# kernel while an update was waiting to render): the tree is
|
|
1557
|
+
# torn down, there is nothing to render into anymore
|
|
1558
|
+
logger.info("Render requested on a closing/closed render context, ignoring")
|
|
1559
|
+
return container
|
|
1482
1560
|
prev_rc = getattr(local, "rc", None)
|
|
1483
1561
|
try:
|
|
1484
1562
|
local.rc = self
|
|
@@ -2963,6 +2963,66 @@ def test_close_when_overridden():
|
|
|
2963
2963
|
rc.close()
|
|
2964
2964
|
|
|
2965
2965
|
|
|
2966
|
+
def test_render_after_close_is_noop():
|
|
2967
|
+
# A disconnect can close the kernel while an update is waiting on the render
|
|
2968
|
+
# lock: the late render used to proceed into the torn-down tree and hit
|
|
2969
|
+
# "assert rc.context is not None" in Element.__exit__ (seen in production).
|
|
2970
|
+
# A render on a closed/closing context must be a no-op instead.
|
|
2971
|
+
@react.component
|
|
2972
|
+
def App():
|
|
2973
|
+
value, _set_value = react.use_state(1)
|
|
2974
|
+
return w.Label(value=str(value))
|
|
2975
|
+
|
|
2976
|
+
box, rc = react.render(App(), handle_error=False)
|
|
2977
|
+
rc.close()
|
|
2978
|
+
rc.render(App(), box) # must not raise, must not render
|
|
2979
|
+
|
|
2980
|
+
|
|
2981
|
+
def test_close_render_race():
|
|
2982
|
+
# close() in one thread while renders queue behind the lock in another:
|
|
2983
|
+
# neither thread may raise (the close teardown must not interleave with a
|
|
2984
|
+
# render that was blocked on the lock).
|
|
2985
|
+
import threading
|
|
2986
|
+
|
|
2987
|
+
for _ in range(20):
|
|
2988
|
+
|
|
2989
|
+
@react.component
|
|
2990
|
+
def App():
|
|
2991
|
+
value, _set_value = react.use_state(1)
|
|
2992
|
+
return w.Label(value=str(value))
|
|
2993
|
+
|
|
2994
|
+
box, rc = react.render(App(), handle_error=False)
|
|
2995
|
+
el = App()
|
|
2996
|
+
errors: List[BaseException] = []
|
|
2997
|
+
barrier = threading.Barrier(2)
|
|
2998
|
+
|
|
2999
|
+
def do_render(rc=rc, el=el, box=box, errors=errors, barrier=barrier):
|
|
3000
|
+
try:
|
|
3001
|
+
barrier.wait()
|
|
3002
|
+
rc.render(el, box)
|
|
3003
|
+
except BaseException as e: # noqa
|
|
3004
|
+
errors.append(e)
|
|
3005
|
+
|
|
3006
|
+
def do_close(rc=rc, errors=errors, barrier=barrier):
|
|
3007
|
+
try:
|
|
3008
|
+
barrier.wait()
|
|
3009
|
+
rc.close()
|
|
3010
|
+
except BaseException as e: # noqa
|
|
3011
|
+
errors.append(e)
|
|
3012
|
+
|
|
3013
|
+
t1 = threading.Thread(target=do_render)
|
|
3014
|
+
t2 = threading.Thread(target=do_close)
|
|
3015
|
+
t1.start()
|
|
3016
|
+
t2.start()
|
|
3017
|
+
t1.join()
|
|
3018
|
+
t2.join()
|
|
3019
|
+
# whichever won the lock, the loser must not blow up; if render won,
|
|
3020
|
+
# close ran after and the context must still close cleanly
|
|
3021
|
+
if not rc._closing:
|
|
3022
|
+
rc.close()
|
|
3023
|
+
assert not errors, errors
|
|
3024
|
+
|
|
3025
|
+
|
|
2966
3026
|
def test_render_perf_child_only():
|
|
2967
3027
|
render_child = unittest.mock.Mock()
|
|
2968
3028
|
render_main = unittest.mock.Mock()
|
|
@@ -3162,6 +3222,30 @@ def test_memory_leak(component):
|
|
|
3162
3222
|
assert weak_rc() is None
|
|
3163
3223
|
|
|
3164
3224
|
|
|
3225
|
+
@reacton.component
|
|
3226
|
+
def UseStateComponent():
|
|
3227
|
+
value, set_value = reacton.use_state(0)
|
|
3228
|
+
return w.Button(description=str(value), on_click=lambda: set_value(value + 1))
|
|
3229
|
+
|
|
3230
|
+
|
|
3231
|
+
@pytest.mark.parametrize("component", [w.Button, ButtonComponentFunction, UseMemoComponent, UseStateComponent])
|
|
3232
|
+
def test_no_reference_cycles_after_close(component):
|
|
3233
|
+
# Stricter than test_memory_leak: the closed render tree must be freed by plain
|
|
3234
|
+
# reference counting, without a garbage collection. If this fails, a closure (a
|
|
3235
|
+
# use_state setter or an event handler) holds the render context strongly and the
|
|
3236
|
+
# tree became one big reference cycle that only a gen-2 gc can free - on a server
|
|
3237
|
+
# that shows up as a memory sawtooth that scales with per-kernel state.
|
|
3238
|
+
box, rc = react.render(component(), handle_error=False)
|
|
3239
|
+
rc.close()
|
|
3240
|
+
weak_rc = weakref.ref(rc)
|
|
3241
|
+
gc.disable()
|
|
3242
|
+
try:
|
|
3243
|
+
del rc
|
|
3244
|
+
assert weak_rc() is None
|
|
3245
|
+
finally:
|
|
3246
|
+
gc.enable()
|
|
3247
|
+
|
|
3248
|
+
|
|
3165
3249
|
def test_fragment():
|
|
3166
3250
|
@reacton.component
|
|
3167
3251
|
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
|