Skip to content

Commit d1c6d37

Browse files
committed
fix #801
accidental mutation of old parent model caused invalid JSON patches
1 parent f593ee3 commit d1c6d37

File tree

7 files changed

+182
-195
lines changed

7 files changed

+182
-195
lines changed

docs/source/about/changelog.rst

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Unreleased
2727

2828
- :issue:`789` - Conditionally rendered components cannot use contexts
2929
- :issue:`773` - Use strict equality check for text, numeric, and binary types in hooks
30+
- :issue:`801` - Accidental mutation of old model causes invalid JSON Patch
3031

3132
**Changed**
3233

src/idom/core/layout.py

+11-7
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,6 @@ def _create_layout_update(self, old_state: _ModelState) -> LayoutUpdate:
161161
new_state = _copy_component_model_state(old_state)
162162
component = new_state.life_cycle_state.component
163163

164-
with ExitStack() as exit_stack:
165-
self._render_component(exit_stack, old_state, new_state, component)
166-
167164
old_model: Optional[VdomJson]
168165
try:
169166
old_model = old_state.model.current
@@ -230,10 +227,17 @@ def _render_component(
230227
else:
231228
key, index = new_state.key, new_state.index
232229
parent.children_by_key[key] = new_state
233-
# need to do insertion in case where old_state is None and we're appending
234-
parent.model.current["children"][index : index + 1] = [
235-
new_state.model.current
236-
]
230+
# need to add this model to parent's children without mutating parent model
231+
old_parent_model = parent.model.current
232+
old_parent_children = old_parent_model["children"]
233+
parent.model.current = {
234+
**old_parent_model,
235+
"children": [
236+
*old_parent_children[:index],
237+
new_state.model.current,
238+
*old_parent_children[index + 1 :],
239+
],
240+
}
237241
finally:
238242
# avoid double render
239243
self._rendering_queue.remove_if_pending(life_cycle_state.id)

tests/test_client.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
from playwright.async_api import Browser
66

77
import idom
8+
from idom.backend.utils import find_available_port
89
from idom.testing import BackendFixture, DisplayFixture
910

1011

1112
JS_DIR = Path(__file__).parent / "js"
1213

1314

1415
async def test_automatic_reconnect(browser: Browser):
16+
port = find_available_port("localhost")
1517
page = await browser.new_page()
1618

1719
# we need to wait longer here because the automatic reconnect is not instant
@@ -22,7 +24,7 @@ def OldComponent():
2224
return idom.html.p({"id": "old-component"}, "old")
2325

2426
async with AsyncExitStack() as exit_stack:
25-
server = await exit_stack.enter_async_context(BackendFixture(port=8000))
27+
server = await exit_stack.enter_async_context(BackendFixture(port=port))
2628
display = await exit_stack.enter_async_context(
2729
DisplayFixture(server, driver=page)
2830
)
@@ -43,7 +45,7 @@ def NewComponent():
4345
return idom.html.p({"id": f"new-component-{state}"}, f"new-{state}")
4446

4547
async with AsyncExitStack() as exit_stack:
46-
server = await exit_stack.enter_async_context(BackendFixture(port=8000))
48+
server = await exit_stack.enter_async_context(BackendFixture(port=port))
4749
display = await exit_stack.enter_async_context(
4850
DisplayFixture(server, driver=page)
4951
)

tests/test_core/test_hooks.py

+28-26
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@
1111
current_hook,
1212
strictly_equal,
1313
)
14-
from idom.core.layout import Layout
15-
from idom.core.serve import render_json_patch
14+
from idom.core.layout import Layout, LayoutUpdate
1615
from idom.testing import DisplayFixture, HookCatcher, assert_idom_did_log, poll
1716
from idom.testing.logs import assert_idom_did_not_log
1817
from idom.utils import Ref
19-
from tests.tooling.asserts import assert_same_items
2018

2119

2220
async def test_must_be_rendering_in_layout_to_use_hooks():
@@ -42,31 +40,35 @@ def SimpleStatefulComponent():
4240
sse = SimpleStatefulComponent()
4341

4442
async with idom.Layout(sse) as layout:
45-
patch_1 = await render_json_patch(layout)
46-
assert patch_1.path == ""
47-
assert_same_items(
48-
patch_1.changes,
49-
[
50-
{"op": "add", "path": "/tagName", "value": ""},
51-
{
52-
"op": "add",
53-
"path": "/children",
54-
"value": [{"children": ["0"], "tagName": "div"}],
55-
},
56-
],
43+
update_1 = await layout.render()
44+
assert update_1 == LayoutUpdate(
45+
path="",
46+
old=None,
47+
new={
48+
"tagName": "",
49+
"children": [{"tagName": "div", "children": ["0"]}],
50+
},
5751
)
5852

59-
patch_2 = await render_json_patch(layout)
60-
assert patch_2.path == ""
61-
assert patch_2.changes == [
62-
{"op": "replace", "path": "/children/0/children/0", "value": "1"}
63-
]
64-
65-
patch_3 = await render_json_patch(layout)
66-
assert patch_3.path == ""
67-
assert patch_3.changes == [
68-
{"op": "replace", "path": "/children/0/children/0", "value": "2"}
69-
]
53+
update_2 = await layout.render()
54+
assert update_2 == LayoutUpdate(
55+
path="",
56+
old=update_1.new,
57+
new={
58+
"tagName": "",
59+
"children": [{"tagName": "div", "children": ["1"]}],
60+
},
61+
)
62+
63+
update_3 = await layout.render()
64+
assert update_3 == LayoutUpdate(
65+
path="",
66+
old=update_2.new,
67+
new={
68+
"tagName": "",
69+
"children": [{"tagName": "div", "children": ["2"]}],
70+
},
71+
)
7072

7173

7274
async def test_set_state_callback_identity_is_preserved():

0 commit comments

Comments
 (0)