Compare commits

...

8 Commits

Author SHA1 Message Date
John Lancaster
07bdbe0b46 changed events 2026-02-22 18:19:48 -06:00
John Lancaster
62204abdd0 default-ish behavior 2026-02-22 18:02:32 -06:00
John Lancaster
dcc8a37627 started mapping tests 2026-02-22 18:02:22 -06:00
John Lancaster
85bf9a431e import ergonomics 2026-02-22 17:48:52 -06:00
John Lancaster
70fa9ede75 working mapping? 2026-02-22 17:48:27 -06:00
John Lancaster
7f4ed3a4b2 update 2026-02-22 17:21:05 -06:00
John Lancaster
4efd57dfb7 rename 2026-02-22 17:21:01 -06:00
John Lancaster
7576bfb719 new_path method 2026-02-22 17:20:11 -06:00
7 changed files with 99 additions and 23 deletions

View File

@@ -1,2 +1,12 @@
def hello() -> str: from .container import HookedContainer, HookFunction
return "Hello from hooked-containers!" from .mapping import HookedMapping
from .sequence import HookedList
from .state import NameSpaceState
__all__ = [
"HookedContainer",
"HookFunction",
"HookedMapping",
"HookedList",
"NameSpaceState",
]

View File

@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Container, Iterable, MutableMapping, MutableSequence, Sized from collections.abc import Container, Iterable, MutableMapping, MutableSequence, Sized
from copy import copy
from typing import Protocol, TypeVar from typing import Protocol, TypeVar
from . import events as e from . import events as e
@@ -15,6 +16,7 @@ class HookFunction(Protocol):
class HookedContainer(ABC, Sized, Iterable[T], Container[T]): class HookedContainer(ABC, Sized, Iterable[T], Container[T]):
_path: MutableSequence[int] _path: MutableSequence[int]
_root: MutableMapping[T] | MutableSequence[T] | None = None
hook: HookFunction hook: HookFunction
def __repr__(self): def __repr__(self):
@@ -35,18 +37,23 @@ class HookedContainer(ABC, Sized, Iterable[T], Container[T]):
def __setitem__(self, s, value): def __setitem__(self, s, value):
self._data[s] = value self._data[s] = value
if self.hook: if self.hook:
self.hook(e.SetItemEvent(index=s, item=value, path=self._path)) self.hook(e.SetItemEvent(self.new_path(s), value))
def __delitem__(self, s): def __delitem__(self, s):
item = self._data.pop(s) item = self._data.pop(s)
if self.hook: if self.hook:
self.hook(e.RemoveItemEvent(index=s, item=item, path=self._path)) self.hook(e.RemoveItemEvent(self.new_path(s), item))
@abstractmethod @abstractmethod
def insert(self, index, value): ... def insert(self, index, value): ...
# Custom Methods # Custom Methods
def new_path(self, key):
new_path = copy(self._path)
new_path.append(key)
return new_path
def get_nested(self, keys): def get_nested(self, keys):
"""Recursively call __getitem__ with each key in the iterable.""" """Recursively call __getitem__ with each key in the iterable."""
result = self result = self

View File

@@ -7,9 +7,8 @@ T = TypeVar("T")
@dataclass(frozen=True) @dataclass(frozen=True)
class ChangeEvent(Generic[T]): class ChangeEvent(Generic[T]):
index: int path: MutableSequence[int]
item: T item: T
path: MutableSequence[int] | None = None
@dataclass(frozen=True) @dataclass(frozen=True)

View File

@@ -1,9 +1,8 @@
from collections.abc import MutableMapping, Sequence from collections.abc import MutableMapping, MutableSequence
from copy import copy
from typing import TypeVar from typing import TypeVar
from . import events as e from . import events as e
from .container import HookedContainer, MutableNesting from .container import HookedContainer, HookFunction, MutableNesting
T = TypeVar("T") T = TypeVar("T")
@@ -13,33 +12,56 @@ class HookedMapping(HookedContainer[T], MutableMapping[T, MutableNesting[T]]):
def __init__( def __init__(
self, self,
hook,
existing: MutableMapping[T, MutableNesting[T]], existing: MutableMapping[T, MutableNesting[T]],
path: Sequence[int] | None = None, hook: HookFunction | None = None,
path: MutableSequence[int] | None = None,
): ):
self.hook = hook self.hook = hook
match existing:
case HookedMapping(_data=seq):
self._data = seq
case MutableMapping() as seq:
self._data = seq
case _ as it:
self._data = dict(it)
self._data = existing self._data = existing
self._path = list(path) if path is not None else [] self._path = list(path) if path is not None else []
# MutableMapping Methods
def __iter__(self):
return iter(self._data)
def insert(self, key: T, value: MutableNesting[T]) -> None:
self._data[key] = value
if self.hook:
self.hook(e.AddItemEvent(value, path=self.new_path(key)))
# HookedContainer methods
def __getitem__(self, key: T) -> MutableNesting[T]: def __getitem__(self, key: T) -> MutableNesting[T]:
if key not in self._data:
self.insert(key, {})
return HookedMapping(self._data[key], hook=self.hook, path=self.new_path(key))
value = self._data[key] value = self._data[key]
new_path = copy(self._path)
new_path.append(key)
match value: match value:
case HookedMapping(_data=seq):
return type(self)(seq, hook=self.hook, path=self.new_path(key))
case MutableMapping() as mapping: case MutableMapping() as mapping:
return HookedMapping(mapping, self.hook, path=new_path) return HookedMapping(mapping, hook=self.hook, path=self.new_path(key))
case HookedContainer() as seq:
return type(self)(seq, self.hook, path=new_path)
case _ as item: case _ as item:
return item return item
def __setitem__(self, key: T, value: MutableNesting[T]) -> None: def __setitem__(self, key: T, value: MutableNesting[T]) -> None:
if key not in self._data:
self.insert(key, value)
else:
self._data[key] = value self._data[key] = value
if self.hook: if self.hook:
self.hook(e.SetItemEvent(index=key, item=value)) self.hook(e.SetItemEvent(value, path=self.new_path(key)))
def __delitem__(self, key: T) -> None: def __delitem__(self, key: T) -> None:
item = self._data[key] item = self._data[key]
del self._data[key] del self._data[key]
if self.hook: if self.hook:
self.hook(e.RemoveItemEvent(index=key, item=item, path=self._path)) self.hook(e.RemoveItemEvent(item, path=self.new_path(key)))

View File

@@ -3,7 +3,7 @@ from copy import copy
from typing import TypeVar from typing import TypeVar
from . import events as e from . import events as e
from .common import HookedContainer, HookFunction from .container import HookedContainer, HookFunction
T = TypeVar("T") T = TypeVar("T")
@@ -42,4 +42,4 @@ class HookedList(HookedContainer[T], MutableSequence[T]):
def insert(self, index, value): def insert(self, index, value):
self._data.insert(index, value) self._data.insert(index, value)
if self.hook: if self.hook:
self.hook(e.AddItemEvent(index=index, item=value, path=self._path)) self.hook(e.AddItemEvent(self.new_path(index), value))

View File

@@ -28,4 +28,4 @@ class NameSpaceState(HookedMapping[str]):
def __getitem__(self, key): def __getitem__(self, key):
val = super().__getitem__(key) val = super().__getitem__(key)
# print("ns GetItem") # print("ns GetItem")
return DomainState(self.hook, existing=val, path=self._path + [key]) return DomainState(val, hook=self.hook, path=self.new_path(key))

38
tests/test_mapping.py Normal file
View File

@@ -0,0 +1,38 @@
from hooked_containers.mapping import HookedMapping
class TestHookedMapping:
class TestConstruction:
def test_empty(self):
m = HookedMapping({})
assert m._data == {}
assert dict(m) == {}
def test_with_existing(self):
existing = {"a": 1, "b": 2}
og_id = id(existing)
m = HookedMapping(existing)
assert id(m._data) == og_id
assert dict(m) == existing
def test_nesting(self):
existing = {"a": {"x": 1}, "b": {"y": 2}}
m = HookedMapping(existing)
assert dict(m) == existing
assert dict(m["a"]) == {"x": 1}
assert dict(m["b"]) == {"y": 2}
class TestMappingOps:
def test_setitem(self):
m = HookedMapping({})
m["a"] = 1
assert m._data == {"a": 1}
def test_getitem(self):
m = HookedMapping({"a": 1})
assert m["a"] == 1
def test_delitem(self):
m = HookedMapping({"a": 1})
del m["a"]
assert not m["a"]