Skip to content

Commit c8e3f74

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

File tree

7 files changed

+182
-192
lines changed

7 files changed

+182
-192
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-4
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,17 @@ def _render_component(
230230
else:
231231
key, index = new_state.key, new_state.index
232232
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-
]
233+
# need to add this model to parent's children without mutating parent model
234+
old_parent_model = parent.model.current
235+
old_parent_children = old_parent_model["children"]
236+
parent.model.current = {
237+
**old_parent_model,
238+
"children": [
239+
*old_parent_children[:index],
240+
new_state.model.current,
241+
*old_parent_children[index + 1 :],
242+
],
243+
}
237244
finally:
238245
# avoid double render
239246
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)