From 1159347ccaee582d2da97021550cab5bd88dbfab Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Thu, 8 Oct 2026 20:50:03 -0500 Subject: [PATCH] typing updates --- src/hooked_containers/container.py | 51 +++++++----------------------- src/hooked_containers/events.py | 30 +++++++++--------- src/hooked_containers/mapping.py | 39 ++++++++++------------- src/hooked_containers/sequence.py | 16 ++-------- 4 files changed, 46 insertions(+), 90 deletions(-) diff --git a/src/hooked_containers/container.py b/src/hooked_containers/container.py index d25cf7f..5d15b0b 100644 --- a/src/hooked_containers/container.py +++ b/src/hooked_containers/container.py @@ -1,58 +1,31 @@ -from abc import ABC, abstractmethod -from collections.abc import Container, Iterable, MutableMapping, MutableSequence, Sized -from typing import TypeVar +from abc import ABC +from abc import abstractmethod +from collections.abc import MutableMapping +from collections.abc import MutableSequence from . import events as e -from .events import HookFunction +from .mixins import CollectionMixin +from .mixins import HookMixin from .types import MutableNesting -T = TypeVar("T") - -class HookedContainer(ABC, Sized, Iterable[T], Container[T]): - _data: MutableNesting[T] - _path: MutableSequence[int] - _root: MutableMapping[T] | MutableSequence[T] | None = None - hook: HookFunction +class HookedContainer[T](HookMixin, CollectionMixin[T], ABC): + _data: MutableMapping[T, MutableNesting[T]] | MutableSequence[T] def __repr__(self): return f"{self.__class__.__name__}({self._data!r})" - def __len__(self): - return len(self._data) - - def __iter__(self): - return iter(self._data) - - def __contains__(self, x): - return x in self._data - - # Sequence Methods - # __contains__, __iter__, __reversed__, index, and count - @abstractmethod def __getitem__(self, key): ... - # MutableSequence Methods - # append, reverse, extend, pop, remove, and __iadd__ - def __setitem__(self, s, value): self._data[s] = value - if self.hook: - self.hook(e.SetItemEvent(self._root, self.new_path(s), value)) + self._notify(e.SetItemEvent, s, value) def __delitem__(self, s): - item = self._data.pop(s) - if self.hook: - self.hook(e.RemoveItemEvent(self._root, self.new_path(s), item)) - - # @abstractmethod - # def insert(self, index, value): ... - - # Custom Methods - - def new_path(self, key): - return (*self._path, key) + item = self._data[s] + del self._data[s] + self._notify(e.RemoveItemEvent, s, item) def get_nested(self, keys): """Recursively call __getitem__ with each key in the iterable.""" diff --git a/src/hooked_containers/events.py b/src/hooked_containers/events.py index 88d0975..0b1396e 100644 --- a/src/hooked_containers/events.py +++ b/src/hooked_containers/events.py @@ -1,33 +1,33 @@ from __future__ import annotations -from collections.abc import MutableSequence -from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Generic, Protocol, TypeVar - -if TYPE_CHECKING: - from .mapping import HookedMapping - -T = TypeVar("T") +from collections.abc import Collection +from collections.abc import Hashable +from collections.abc import Sequence +from dataclasses import dataclass +from dataclasses import field +from typing import Protocol @dataclass(frozen=True) -class ChangeEvent(Generic[T]): - root: HookedMapping[T] = field(repr=False) - path: MutableSequence[int] +class ChangeEvent[T]: + """Describe a mutation at an index or key path within a root collection.""" + + root: Collection[object] = field(repr=False) + path: Sequence[Hashable | slice] item: T @dataclass(frozen=True) -class AddItemEvent(ChangeEvent[T]): ... +class AddItemEvent[T](ChangeEvent[T]): ... @dataclass(frozen=True) -class SetItemEvent(ChangeEvent[T]): ... +class SetItemEvent[T](ChangeEvent[T]): ... @dataclass(frozen=True) -class RemoveItemEvent(ChangeEvent[T]): ... +class RemoveItemEvent[T](ChangeEvent[T]): ... class HookFunction(Protocol): - def __call__(self, event: ChangeEvent[T]) -> None: ... + def __call__[T](self, event: ChangeEvent[T]) -> None: ... diff --git a/src/hooked_containers/mapping.py b/src/hooked_containers/mapping.py index 63878e4..8a60d25 100644 --- a/src/hooked_containers/mapping.py +++ b/src/hooked_containers/mapping.py @@ -1,14 +1,14 @@ -from collections.abc import MutableMapping, MutableSequence -from typing import TypeVar +from collections.abc import MutableMapping +from collections.abc import MutableSequence from . import events as e -from .container import HookedContainer, MutableNesting +from .container import HookedContainer +from .container import MutableNesting from .events import HookFunction - -T = TypeVar("T") +from .mixins import MutableMappingMixin -class HookedMapping(HookedContainer[T], MutableMapping[T, MutableNesting[T]]): +class HookedMapping[T](MutableMappingMixin[T, MutableNesting[T]], HookedContainer[T]): _data: MutableMapping[T, MutableNesting[T]] def __init__( @@ -26,7 +26,6 @@ class HookedMapping(HookedContainer[T], MutableMapping[T, MutableNesting[T]]): case _: raise TypeError(f"Expected a mapping, got {type(existing)}") # self._data = dict(it) - self._data = existing self.hook = hook self._root = root if root is not None else self self._path = list(path) if path is not None else [] @@ -51,22 +50,16 @@ class HookedMapping(HookedContainer[T], MutableMapping[T, MutableNesting[T]]): *, suppress_hook: bool = False, ) -> MutableNesting[T] | None: - new_path = self.new_path(key) - event = ( - e.SetItemEvent(self._root, new_path, value) - if key in self._data - else e.AddItemEvent(self._root, new_path, value) - ) match value: - case HookedMapping(_data=value): - self._data[key] = value + case HookedMapping(_data=data): + event_type = e.SetItemEvent if key in self._data else e.AddItemEvent + self._data[key] = data + if not suppress_hook: + self._notify(event_type, key, value) + return data case _: - self._data[key] = value - if self.hook and not suppress_hook: - self.hook(event) + if suppress_hook: + self._data[key] = value + else: + super().__setitem__(key, value) return value - - def __delitem__(self, key: T) -> None: - item = self._data.pop(key) - if self.hook: - self.hook(e.RemoveItemEvent(self._root, self.new_path(key), item)) diff --git a/src/hooked_containers/sequence.py b/src/hooked_containers/sequence.py index 74c994f..1b55e03 100644 --- a/src/hooked_containers/sequence.py +++ b/src/hooked_containers/sequence.py @@ -1,16 +1,12 @@ from collections.abc import MutableSequence -from copy import copy -from typing import TypeVar -from . import events as e from .container import HookedContainer from .events import HookFunction +from .mixins import MutableSequenceMixin from .types import MutableNesting -T = TypeVar("T") - -class HookedList(HookedContainer[T], MutableSequence[T]): +class HookedList[T](MutableSequenceMixin[T], HookedContainer[T]): _data: MutableSequence[T] path: MutableSequence[int] @@ -33,8 +29,7 @@ class HookedList(HookedContainer[T], MutableSequence[T]): self._path = list(path) if path is not None else [] def __getitem__(self, s): - new_path = copy(self._path) - new_path.append(s) + new_path = self.new_path(s) match self._data[s]: case HookedContainer(_data=seq): return type(self)(seq, self.hook, root=self._root, path=new_path) @@ -42,8 +37,3 @@ class HookedList(HookedContainer[T], MutableSequence[T]): return HookedList(seq, self.hook, root=self._root, path=new_path) case _ as item: return item - - def insert(self, index, value): - self._data.insert(index, value) - if self.hook: - self.hook(e.AddItemEvent(self._root, self.new_path(index), value))