77 lines
5.5 KiB
Markdown
77 lines
5.5 KiB
Markdown
# Hooked Containers
|
|
|
|
Hooked Containers adds optional, shallow mutation callbacks to familiar Python container operations. It provides `HookedMapping[K, V]`, `HookedList[T]`, and `HookedSet[T]`, backed by owned `dict`, `list`, and `set` storage. Requires Python 3.12 or newer.
|
|
|
|
## Mixin Design
|
|
|
|
All reusable container behavior lives in `src/hooked_containers/mixins.py`. The design uses Python's [collections.abc abstract base classes](https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes) directly. Three mutable mixins compose shared reads and hook dispatch with their required primitives; standard ABC methods supply the helpers:
|
|
|
|
| Mixin | Backing Storage | Responsibility |
|
|
| --- | --- | --- |
|
|
| `ContainerMixin` | Any collection | Containment, iteration, length, and representation. |
|
|
| `HookMixin` | None | Callback dispatch after successful writes. |
|
|
| `ListMixin` | `list` | Indexed reads and writes, insertion, and native list operations. |
|
|
| `MutableMappingMixin` | `dict` | Key-based reads and writes, copying, and merging. |
|
|
| `MutableSetMixin` | `set` | Addition, discard, and named set operations. |
|
|
|
|
`HookMixin` shares callback dispatch across the three mixins. Inherited methods such as `append`, `extend`, `reverse`, `update`, `clear`, and set in-place operators reach the primitive writes automatically. A compound operation may invoke the hook more than once.
|
|
|
|
Each subclass initializes a single `_storage` attribute with the corresponding native container. There is no storage property or separate container-specific read-only hierarchy. Each of `sequence.py`, `mapping.py`, and `set.py` defines just one concrete wrapper, composing its mutable mixin and adding construction. Representation comes from `ContainerMixin`. Dict-style `popitem` intentionally removes the most recently inserted key, unlike the ABC's arbitrary-item implementation.
|
|
|
|
Custom containers can use the mixins without any concrete wrapper:
|
|
|
|
```python
|
|
from hooked_containers.mixins import MutableMappingMixin
|
|
|
|
|
|
class Settings(MutableMappingMixin[str, int]):
|
|
def __init__(self, hook=None):
|
|
self._storage = {}
|
|
self.hook = hook
|
|
```
|
|
|
|
## Change Hooks
|
|
|
|
A hook receives one immutable `Change(operation, key, value)` record after a write:
|
|
|
|
| Field | Meaning |
|
|
| --- | --- |
|
|
| `operation` | `"add"`, `"set"`, or `"remove"` |
|
|
| `key` | Mapping key, normalized list index, list slice, or `None` for sets |
|
|
| `value` | Added/new value, or the value that was removed |
|
|
|
|
```python
|
|
from hooked_containers import HookedList, HookedMapping, HookedSet
|
|
|
|
|
|
def on_change(change):
|
|
print(change.operation, change.key, change.value)
|
|
|
|
|
|
items = HookedList(["first"], hook=on_change)
|
|
items.append("second") # add 1 second
|
|
items[0] = "updated" # set 0 updated
|
|
del items[1] # remove 1 second
|
|
|
|
settings = HookedMapping({}, hook=on_change)
|
|
settings["enabled"] = True # add enabled True
|
|
|
|
members = HookedSet(hook=on_change)
|
|
members.add("ready") # add None ready
|
|
members.discard("ready") # remove None ready
|
|
```
|
|
|
|
Reads never change data or invoke the callback. Missing mapping keys raise `KeyError`, and `get` respects its default. Integer list keys are normalized against the length before the write; insertions report their clamped destination. Slice keys preserve the supplied slice in pre-write coordinates, and their values are shallow lists of new or removed items. Sorting and in-place repetition report a whole-list `"set"` using `slice(None)`.
|
|
|
|
Assignments notify even when assigning an equal value. Duplicate set additions, missing set discards, empty clears, and empty slice writes do not notify. Failed primitive writes do not notify; writes completed before a compound operation fails remain reported. A failed sort reports any resulting rearrangement. Callback exceptions propagate after the write without rolling it back. Callbacks that mutate the same container can trigger further callbacks; there is no batching or reentrancy guard.
|
|
|
|
Construction copies the outer storage without notifying. Nested values are not copied or wrapped, and mutations inside those values are not observed. Copies, slices, and non-mutating derived results are ordinary native containers without hooks. Event values are shallow references, not deep snapshots; later changes to a mutable value remain visible through that reference. Direct changes to private storage bypass callbacks.
|
|
|
|
Set comparison, algebra, and in-place methods follow the standard ABC implementations. In particular, ABC set operators accept iterables more broadly than built-in `set` operators. These wrappers implement container interfaces, not built-in subclass identity or exact built-in performance guarantees.
|
|
|
|
## Migration
|
|
|
|
Custom subclasses now initialize `_storage` directly instead of implementing `_data`. The earlier container/collection hierarchy, read-only sequence, mapping, and set mixins, along with `MutableSequenceMixin`, have been replaced by the shared `ContainerMixin` and the three mutable mixins above. Import reusable mixins from `hooked_containers.mixins` and use native backing storage.
|
|
|
|
The shallow API replaces the earlier root/path event subclasses and recursive wrapping. Use `change.operation`, `change.key`, and `change.value` instead of event types, `path`, and `item`. `HookedMapping` has independent key and value type parameters. Shared outer storage, autovivification, `suppress_hook`, `get_nested`, and the domain-state classes are no longer supported. The development notebooks are historical experiments with the previous API; the examples above describe the current API.
|