statepatch 0.1.0__tar.gz → 0.1.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.
- {statepatch-0.1.0 → statepatch-0.1.1}/PKG-INFO +1 -1
- {statepatch-0.1.0 → statepatch-0.1.1}/pyproject.toml +1 -1
- {statepatch-0.1.0 → statepatch-0.1.1}/src/statepatch/__init__.py +20 -5
- {statepatch-0.1.0 → statepatch-0.1.1}/tests/test_observable.py +32 -4
- {statepatch-0.1.0 → statepatch-0.1.1}/.gitignore +0 -0
- {statepatch-0.1.0 → statepatch-0.1.1}/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: statepatch
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Immer-style ops over a plain JSON tree: observables that emit every mutation as an op
|
|
5
5
|
Project-URL: Repository, https://github.com/assistant-ui/harness-sdk
|
|
6
6
|
License-Expression: MIT
|
|
@@ -320,11 +320,7 @@ class Proxy:
|
|
|
320
320
|
else container[key]
|
|
321
321
|
)
|
|
322
322
|
path = rel + (key,)
|
|
323
|
-
if isinstance(old, str) and
|
|
324
|
-
if value == old:
|
|
325
|
-
return
|
|
326
|
-
container[key] = value
|
|
327
|
-
owner._emit(Op("add", path + (len(old),), value[len(old) :]))
|
|
323
|
+
if isinstance(old, str) and value == old:
|
|
328
324
|
return
|
|
329
325
|
stored, mounts = _adopt(value, path)
|
|
330
326
|
owner._admit(mounts, path)
|
|
@@ -522,3 +518,22 @@ class Proxy:
|
|
|
522
518
|
def __repr__(self) -> str:
|
|
523
519
|
_owner, _rel, node = self._target()
|
|
524
520
|
return repr(_snapshot(node))
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def append(parent: Any, key: Segment, suffix: str) -> None:
|
|
524
|
+
"""Append ``suffix`` to the string at ``parent[key]``, recording an end-offset text-insert ``add`` op."""
|
|
525
|
+
if isinstance(parent, Observable):
|
|
526
|
+
parent = Proxy(parent, ())
|
|
527
|
+
if not isinstance(parent, Proxy):
|
|
528
|
+
raise TypeError(f"append expects a proxy or observable, got {type(parent).__name__}")
|
|
529
|
+
if not isinstance(suffix, str):
|
|
530
|
+
raise TypeError(f"append suffix must be a string, got {type(suffix).__name__}")
|
|
531
|
+
owner, rel, container = parent._target()
|
|
532
|
+
key = _index(container, key)
|
|
533
|
+
old = container[key]
|
|
534
|
+
if not isinstance(old, str):
|
|
535
|
+
raise TypeError(f"cannot append to a non-string at path {list(rel + (key,))!r}")
|
|
536
|
+
if suffix == "":
|
|
537
|
+
return
|
|
538
|
+
container[key] = old + suffix
|
|
539
|
+
owner._emit(Op("add", rel + (key, len(old)), suffix))
|
|
@@ -4,7 +4,7 @@ mounted observables forward their op streams at the mount path."""
|
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
|
-
from statepatch import Op, observable
|
|
7
|
+
from statepatch import Op, append, observable
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
def collect(obs):
|
|
@@ -27,18 +27,46 @@ def test_dict_and_list_mutations_emit_wire_shaped_ops():
|
|
|
27
27
|
state["status"] = "streaming"
|
|
28
28
|
state["messages"].append({"id": "m1", "content": ""})
|
|
29
29
|
state["messages"][0]["content"] = "hel"
|
|
30
|
-
state["messages"][0]["content"] = "hello" #
|
|
30
|
+
state["messages"][0]["content"] = "hello" # assign is always a replace
|
|
31
31
|
state["messages"].pop(0)
|
|
32
32
|
assert ops == [
|
|
33
33
|
Op("replace", ("status",), "streaming"),
|
|
34
34
|
Op("add", ("messages", 0), {"id": "m1", "content": ""}),
|
|
35
|
-
Op("
|
|
36
|
-
Op("
|
|
35
|
+
Op("replace", ("messages", 0, "content"), "hel"),
|
|
36
|
+
Op("replace", ("messages", 0, "content"), "hello"),
|
|
37
37
|
Op("remove", ("messages", 0)),
|
|
38
38
|
]
|
|
39
39
|
assert state.value == {"status": "streaming", "messages": []}
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def test_append_emits_an_end_offset_string_add():
|
|
43
|
+
state = observable({"text": "he", "parts": ["ab"]})
|
|
44
|
+
ops = collect(state)
|
|
45
|
+
append(state, "text", "llo")
|
|
46
|
+
append(state["parts"], 0, "c")
|
|
47
|
+
append(state, "text", "") # empty suffix is a no-op
|
|
48
|
+
assert state.value == {"text": "hello", "parts": ["abc"]}
|
|
49
|
+
assert ops == [
|
|
50
|
+
Op("add", ("text", 2), "llo"),
|
|
51
|
+
Op("add", ("parts", 0, 2), "c"),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_append_fails_fast_on_bad_targets():
|
|
56
|
+
state = observable({"n": 1, "text": "hi"})
|
|
57
|
+
ops = collect(state)
|
|
58
|
+
with pytest.raises(TypeError, match="non-string"):
|
|
59
|
+
append(state, "n", "x")
|
|
60
|
+
with pytest.raises(TypeError, match="suffix must be a string"):
|
|
61
|
+
append(state, "text", 1)
|
|
62
|
+
with pytest.raises(KeyError):
|
|
63
|
+
append(state, "missing", "x")
|
|
64
|
+
with pytest.raises(TypeError, match="proxy or observable"):
|
|
65
|
+
append({"text": "hi"}, "text", "x")
|
|
66
|
+
assert state.value == {"n": 1, "text": "hi"}
|
|
67
|
+
assert ops == []
|
|
68
|
+
|
|
69
|
+
|
|
42
70
|
def test_values_are_dealised_on_write():
|
|
43
71
|
state = observable({})
|
|
44
72
|
held = {"a": [1]}
|
|
File without changes
|
|
File without changes
|