restructured mixins
This commit is contained in:
@@ -1,49 +1,76 @@
|
||||
# Hooked Containers
|
||||
|
||||
Hooked Containers provides an abstract container foundation that calls a function hook whenever its contents are changed through its API. The aim is to retain familiar Python container operations while letting application code react to additions, replacements, and removals.
|
||||
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
|
||||
|
||||
The design follows Python's [collections.abc abstract base classes](https://docs.python.org/3/library/collections.abc.html#collections-abstract-base-classes). Separate mixin classes should build up functionality in layers, so a custom container can compose the behavior it needs:
|
||||
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:
|
||||
|
||||
| Layer | Responsibility |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------- |
|
||||
| Container | Membership testing through `__contains__`. |
|
||||
| Collection | Iteration and size through `__iter__` and `__len__`, alongside membership. |
|
||||
| Hook support | A shared callback and change-event reporting for mutation methods. |
|
||||
| Sequence | Indexed access through `__getitem__`, with sequence helpers such as `index` and `count`. |
|
||||
| Mutable sequence | Assignment, deletion, and insertion, supporting helpers such as `append`, `extend`, and `pop`. |
|
||||
| Mapping | Key-based access, iteration over keys, and helpers such as `keys`, `items`, and `values`. |
|
||||
| Mutable mapping | Assignment and deletion, supporting helpers such as `update`, `setdefault`, and `pop`. |
|
||||
| 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. |
|
||||
|
||||
Sequences and mappings are separate branches built on the common container foundation, not successive steps in a single inheritance chain. Mutation hooks belong in the primitive operations so inherited ABC mixin methods also report changes. A compound operation may therefore invoke the hook more than once.
|
||||
`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.
|
||||
|
||||
This layered separation is the intended architecture. Currently, `HookedContainer` combines shared container and hook behavior; `HookedList` and `HookedMapping` extend it with the standard library's `MutableSequence` and `MutableMapping` mixins, respectively.
|
||||
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 change-event object after a mutation. The event identifies the root container (`root`), the index or key path (`path`), and the affected value (`item`). The current event types are `AddItemEvent`, `SetItemEvent`, and `RemoveItemEvent`.
|
||||
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.mapping import HookedMapping
|
||||
from hooked_containers.sequence import HookedList
|
||||
from hooked_containers import HookedList, HookedMapping, HookedSet
|
||||
|
||||
|
||||
def on_change(event):
|
||||
print(type(event).__name__, event.path, event.item)
|
||||
def on_change(change):
|
||||
print(change.operation, change.key, change.value)
|
||||
|
||||
|
||||
items = HookedList(["first"], hook=on_change)
|
||||
items.append("second") # AddItemEvent (1,) second
|
||||
items[0] = "updated" # SetItemEvent (0,) updated
|
||||
del items[1] # RemoveItemEvent (1,) second
|
||||
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 # AddItemEvent ('enabled',) True
|
||||
settings["enabled"] = True # add enabled True
|
||||
|
||||
members = HookedSet(hook=on_change)
|
||||
members.add("ready") # add None ready
|
||||
members.discard("ready") # remove None ready
|
||||
```
|
||||
|
||||
The wrappers can share existing mutable storage. Mutating that storage directly bypasses the hook; make changes through the hooked container to receive events. Hooks are optional, and nested wrappers retain the root and path for supported nested container types.
|
||||
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)`.
|
||||
|
||||
## Status
|
||||
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.
|
||||
|
||||
Requires Python 3.12 or newer. This repository is an early implementation of the container and mixin design; the API and separation of responsibilities may evolve.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user