Skip to content

Commit a6e8d9d

Browse files
committed
fix: harden hydration path updates and rerender flow
1 parent e5aed20 commit a6e8d9d

File tree

3 files changed

+62
-32
lines changed

3 files changed

+62
-32
lines changed

src/pyinkcli/components/AppContext.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ def create_app_context_value(app=None):
3535

3636

3737
class _NullApp:
38+
def _run_discrete(self, callback):
39+
return None
40+
41+
def _rerender_current(self):
42+
return None
43+
3844
def wait_until_exit(self, timeout=None):
3945
return None
4046

@@ -54,4 +60,4 @@ def set_app_context_value(app=None):
5460
AppContext.current_value = value
5561
return value
5662

57-
__all__ = ["AppContext", "Props", "create_app_context_value", "set_app_context_value", "AppHandle"]
63+
__all__ = ["AppContext", "Props"]

src/pyinkcli/packages/react_devtools_core/hydration.py

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -56,25 +56,28 @@ def _copy_metadata(source, target):
5656

5757

5858
def copy_with_metadata(value, metadata=None):
59-
if isinstance(value, HydratedList):
60-
cloned = HydratedList(copy.deepcopy(list(value)))
61-
_copy_metadata(value, cloned)
62-
elif isinstance(value, HydratedDict):
63-
cloned = HydratedDict(copy.deepcopy(dict(value)))
64-
_copy_metadata(value, cloned)
65-
elif isinstance(value, list):
66-
cloned = HydratedList(copy.deepcopy(value))
67-
elif isinstance(value, dict):
68-
cloned = HydratedDict(copy.deepcopy(dict(value)))
69-
legacy = get_metadata(value)
70-
if legacy is not None:
71-
set_metadata(cloned, dict(legacy))
72-
if value.get(INSPECTED_KEY):
73-
mark_inspected(cloned, True)
74-
if value.get(UNSERIALIZABLE_KEY):
75-
mark_unserializable(cloned, True)
76-
else:
77-
cloned = copy.deepcopy(value)
59+
try:
60+
if isinstance(value, HydratedList):
61+
cloned = HydratedList(copy.deepcopy(list(value)))
62+
_copy_metadata(value, cloned)
63+
elif isinstance(value, HydratedDict):
64+
cloned = HydratedDict(copy.deepcopy(dict(value)))
65+
_copy_metadata(value, cloned)
66+
elif isinstance(value, list):
67+
cloned = HydratedList(copy.deepcopy(value))
68+
elif isinstance(value, dict):
69+
cloned = HydratedDict(copy.deepcopy(dict(value)))
70+
legacy = get_metadata(value)
71+
if legacy is not None:
72+
set_metadata(cloned, dict(legacy))
73+
if value.get(INSPECTED_KEY):
74+
mark_inspected(cloned, True)
75+
if value.get(UNSERIALIZABLE_KEY):
76+
mark_unserializable(cloned, True)
77+
else:
78+
cloned = copy.deepcopy(value)
79+
except Exception:
80+
cloned = value
7881
if metadata is not None and isinstance(cloned, (HydratedDict, HydratedList)):
7982
set_metadata(cloned, metadata)
8083
return cloned
@@ -254,25 +257,46 @@ def set_in_object(value, path, replacement):
254257

255258

256259
def delete_in_path(value, path):
260+
if not path:
261+
return value
257262
current = copy_with_metadata(value)
258-
container, key = _container_and_key(current, path)
259-
if isinstance(container, list):
263+
try:
264+
container, key = _container_and_key(current, path)
265+
except Exception:
266+
return current
267+
if isinstance(container, list) and isinstance(key, int) and 0 <= key < len(container):
260268
del container[key]
269+
elif isinstance(container, (dict, HydratedDict, HydratedList)):
270+
if isinstance(container, HydratedList):
271+
if isinstance(key, int) and 0 <= key < len(container):
272+
del container[key]
273+
elif isinstance(container, (dict, HydratedDict)):
274+
if key in container:
275+
del container[key]
276+
else:
277+
return current
261278
else:
262-
del container[key]
279+
return current
263280
return current
264281

265282

266283
def delete_path_in_object(value, path):
267284
mutated = delete_in_path(value, path)
268-
if path == []:
285+
if not path:
269286
return mutated
270-
container, key = _container_and_key(value, path)
271-
if isinstance(container, list):
272-
del container[key]
273-
else:
274-
del container[key]
275-
return value
287+
try:
288+
container, key = _container_and_key(value, path)
289+
if isinstance(container, list) and isinstance(key, int) and 0 <= key < len(container):
290+
del container[key]
291+
elif isinstance(container, (dict, HydratedDict, HydratedList)):
292+
if isinstance(container, HydratedList):
293+
if isinstance(key, int) and 0 <= key < len(container):
294+
del container[key]
295+
elif key in container:
296+
del container[key]
297+
except Exception:
298+
return mutated
299+
return mutated
276300

277301

278302
def rename_in_path(value, path, new_path):

src/pyinkcli/reconciler.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,7 +1090,7 @@ def schedule_update(node_id):
10901090

10911091
app = useApp()
10921092
if app is not None:
1093-
app.render(app._current_node)
1093+
app._rerender_current()
10941094
finally:
10951095
self._force_rerender = False
10961096
return True
@@ -1266,7 +1266,7 @@ def override_error(node_id, force_error):
12661266

12671267
app = useApp()
12681268
if app is not None:
1269-
app.render(app._current_node)
1269+
app._rerender_current()
12701270
return True
12711271
return False
12721272
error_owner = next((info for info in entry.get("ownerInfos", []) if info.get("isErrorBoundary")), None)

0 commit comments

Comments
 (0)