mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-25 08:04:57 +08:00
core/tools: serialize domain objects on PersistentDataView commit
This commit is contained in:
+55
-26
@@ -94,23 +94,6 @@ class PersistentDataView[T](AbstractAsyncContextManager["PersistentDataView[T]"]
|
|||||||
return None
|
return None
|
||||||
return current
|
return current
|
||||||
|
|
||||||
def _ensure_parent(self, root: object) -> object:
|
|
||||||
"""Ensure dict parents along path exist; return parent container for last key."""
|
|
||||||
if not self._path:
|
|
||||||
return root
|
|
||||||
current: object = root
|
|
||||||
for key in self._path[:-1]:
|
|
||||||
if not isinstance(current, dict):
|
|
||||||
msg = f"cannot navigate non-dict parent at {key!r}"
|
|
||||||
raise TypeError(msg)
|
|
||||||
mapping = cast("dict[object, object]", current)
|
|
||||||
child: object | None = mapping.get(key)
|
|
||||||
if child is None:
|
|
||||||
child = cast("object", {})
|
|
||||||
mapping[key] = child
|
|
||||||
current = child
|
|
||||||
return current
|
|
||||||
|
|
||||||
def __getitem__(self, key: str | int) -> PersistentDataView[Any]:
|
def __getitem__(self, key: str | int) -> PersistentDataView[Any]:
|
||||||
child_path = (*self._path, key)
|
child_path = (*self._path, key)
|
||||||
cached = self._child_cache.get(child_path)
|
cached = self._child_cache.get(child_path)
|
||||||
@@ -142,11 +125,16 @@ class PersistentDataView[T](AbstractAsyncContextManager["PersistentDataView[T]"]
|
|||||||
def store(self, value: T) -> None:
|
def store(self, value: T) -> None:
|
||||||
txn = self._require_txn()
|
txn = self._require_txn()
|
||||||
self._domain[self._path] = value
|
self._domain[self._path] = value
|
||||||
if not self._path:
|
self._write_at_path(txn, self._path, value)
|
||||||
|
txn.dirty = True
|
||||||
|
|
||||||
|
def _write_at_path(self, txn: _TxnState, path: tuple[str | int, ...], value: object) -> None:
|
||||||
|
"""Write *value* into the txn buffer at *path* (may be a live domain object)."""
|
||||||
|
if not path:
|
||||||
txn.root = value
|
txn.root = value
|
||||||
else:
|
return
|
||||||
parent = self._ensure_parent(txn.root)
|
parent = self._ensure_parent_for(txn.root, path)
|
||||||
last = self._path[-1]
|
last = path[-1]
|
||||||
if isinstance(parent, dict):
|
if isinstance(parent, dict):
|
||||||
cast("dict[object, object]", parent)[last] = value
|
cast("dict[object, object]", parent)[last] = value
|
||||||
elif isinstance(parent, list) and isinstance(last, int):
|
elif isinstance(parent, list) and isinstance(last, int):
|
||||||
@@ -155,11 +143,52 @@ class PersistentDataView[T](AbstractAsyncContextManager["PersistentDataView[T]"]
|
|||||||
lst.append(None)
|
lst.append(None)
|
||||||
lst[last] = value
|
lst[last] = value
|
||||||
else:
|
else:
|
||||||
msg = f"cannot store at path {self._path!r}"
|
msg = f"cannot store at path {path!r}"
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
txn.dirty = True
|
|
||||||
|
def _ensure_parent_for(self, root: object, path: tuple[str | int, ...]) -> object:
|
||||||
|
"""Ensure dict parents along *path* exist; return parent container for last key."""
|
||||||
|
if not path:
|
||||||
|
return root
|
||||||
|
current: object = root
|
||||||
|
for key in path[:-1]:
|
||||||
|
if not isinstance(current, dict):
|
||||||
|
msg = f"cannot navigate non-dict parent at {key!r}"
|
||||||
|
raise TypeError(msg)
|
||||||
|
mapping = cast("dict[object, object]", current)
|
||||||
|
child: object | None = mapping.get(key)
|
||||||
|
if child is None:
|
||||||
|
child = cast("object", {})
|
||||||
|
mapping[key] = child
|
||||||
|
current = child
|
||||||
|
return current
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _serialize_value(value: object) -> object:
|
||||||
|
"""Prefer domain ``to_raw()`` so stores receive JSON-ish trees, not live objects."""
|
||||||
|
to_raw = getattr(value, "to_raw", None)
|
||||||
|
if callable(to_raw):
|
||||||
|
return to_raw()
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _flush_domain(self, txn: _TxnState) -> None:
|
||||||
|
"""Rewrite domain objects into the buffer in serializable form before store."""
|
||||||
|
for path, value in self._domain.items():
|
||||||
|
self._write_at_path(txn, path, self._serialize_value(value))
|
||||||
|
|
||||||
def _materialize_domain(self, typ: type[Any], current: object) -> object:
|
def _materialize_domain(self, typ: type[Any], current: object) -> object:
|
||||||
|
if current is not None and isinstance(current, typ):
|
||||||
|
self._domain[self._path] = current
|
||||||
|
return current
|
||||||
|
|
||||||
|
# Hybrid domain objects (e.g. TodoStack): reconstruct from durable raw.
|
||||||
|
from_raw = getattr(typ, "from_raw", None)
|
||||||
|
if current is not None and callable(from_raw):
|
||||||
|
converted: object = from_raw(current)
|
||||||
|
self._domain[self._path] = converted
|
||||||
|
self.store(cast("T", converted))
|
||||||
|
return converted
|
||||||
|
|
||||||
ctor = cast("Any", typ)
|
ctor = cast("Any", typ)
|
||||||
if current is None:
|
if current is None:
|
||||||
try:
|
try:
|
||||||
@@ -170,15 +199,13 @@ class PersistentDataView[T](AbstractAsyncContextManager["PersistentDataView[T]"]
|
|||||||
if created is not None:
|
if created is not None:
|
||||||
self.store(cast("T", created))
|
self.store(cast("T", created))
|
||||||
return created
|
return created
|
||||||
if not isinstance(current, typ):
|
|
||||||
try:
|
try:
|
||||||
converted: object = ctor(current)
|
converted = ctor(current)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
converted = current
|
converted = current
|
||||||
self._domain[self._path] = converted
|
self._domain[self._path] = converted
|
||||||
self.store(cast("T", converted))
|
self.store(cast("T", converted))
|
||||||
return converted
|
return converted
|
||||||
return current
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def typed(self, typ: None = None) -> T: ...
|
def typed(self, typ: None = None) -> T: ...
|
||||||
@@ -211,6 +238,7 @@ class PersistentDataView[T](AbstractAsyncContextManager["PersistentDataView[T]"]
|
|||||||
"""Flush the root buffer to the store without closing the txn."""
|
"""Flush the root buffer to the store without closing the txn."""
|
||||||
txn = self._require_txn()
|
txn = self._require_txn()
|
||||||
if txn.dirty:
|
if txn.dirty:
|
||||||
|
self._flush_domain(txn)
|
||||||
await self._store.store(txn.root)
|
await self._store.store(txn.root)
|
||||||
txn.dirty = False
|
txn.dirty = False
|
||||||
|
|
||||||
@@ -249,6 +277,7 @@ class PersistentDataView[T](AbstractAsyncContextManager["PersistentDataView[T]"]
|
|||||||
# Root exit
|
# Root exit
|
||||||
try:
|
try:
|
||||||
if exc_type is None and txn.dirty:
|
if exc_type is None and txn.dirty:
|
||||||
|
self._flush_domain(txn)
|
||||||
await self._store.store(txn.root)
|
await self._store.store(txn.root)
|
||||||
finally:
|
finally:
|
||||||
self._txn = None
|
self._txn = None
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ async def test_view_rollback_on_error() -> None:
|
|||||||
assert await store.load() == {"keep": 1}
|
assert await store.load() == {"keep": 1}
|
||||||
|
|
||||||
|
|
||||||
async def test_typed_todo_stack() -> None:
|
async def test_typed_todo_stack_commits_raw() -> None:
|
||||||
store = MemoryViewStore({})
|
store = MemoryViewStore({})
|
||||||
root: PersistentDataView[dict[str, object]] = PersistentDataView(store, bound_type=dict)
|
root: PersistentDataView[dict[str, object]] = PersistentDataView(store, bound_type=dict)
|
||||||
async with root as data:
|
async with root as data:
|
||||||
@@ -38,7 +38,24 @@ async def test_typed_todo_stack() -> None:
|
|||||||
assert todo.depth == 1
|
assert todo.depth == 1
|
||||||
loaded = await store.load()
|
loaded = await store.load()
|
||||||
assert isinstance(loaded, dict)
|
assert isinstance(loaded, dict)
|
||||||
assert isinstance(loaded.get("todo"), TodoStack)
|
raw_todo = loaded.get("todo")
|
||||||
|
assert isinstance(raw_todo, dict)
|
||||||
|
assert "groups" in raw_todo
|
||||||
|
restored = TodoStack.from_raw(raw_todo)
|
||||||
|
assert restored.depth == 1
|
||||||
|
assert [i.title for i in restored.groups[0].items] == ["A", "B"]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_typed_todo_stack_roundtrip_from_raw() -> None:
|
||||||
|
store = MemoryViewStore({"todo": {"groups": [], "next_id": 1}})
|
||||||
|
root: PersistentDataView[dict[str, object]] = PersistentDataView(store, bound_type=dict)
|
||||||
|
async with root as data:
|
||||||
|
todo = data["todo"].typed(TodoStack)
|
||||||
|
_ = todo.push_group(["only"])
|
||||||
|
loaded = await store.load()
|
||||||
|
assert isinstance(loaded, dict)
|
||||||
|
again = TodoStack.from_raw(loaded["todo"])
|
||||||
|
assert [i.title for i in again.all_items()] == ["only"]
|
||||||
|
|
||||||
|
|
||||||
async def test_mutate_outside_txn_errors() -> None:
|
async def test_mutate_outside_txn_errors() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user