mnemoreg 0.3.11
A tiny thread-safe registry mapping for simple plugin/registry use-cases.
Requires Python: >=3.8

mnemoreg
========

A tiny, dependency-free, thread-safe registry mapping useful for registering
callables and values by string keys. Designed to be embedded in other projects
or used as a small standalone utility.

Key points
- Small, stdlib-only implementation.
- Thread-safe (uses RLock for mutations).
- Simple decorator-based registration for callables.
- Optional descriptions for stored values: each stored entry can carry an
  optional human-readable description (string) that is accessible via
  `Registry.snapshot()`.

Quick facts
- Package exports: `Registry`, `AlreadyRegisteredError`, `NotRegisteredError`, `StorageProtocol`, and `__version__`.
- Main implementation: `mnemoreg/core.py`.
- Storage helpers live in `mnemoreg/_storage` (default in-memory backend is exported as `MemoryStorage` — note: the name contains a historical typo).

Install
-------
From PyPI:

    pip install mnemoreg

From source (editable/development):

    git clone https://github.com/i3iorn/mnemoreg.git
    cd mnemoreg
    python -m venv .venv
    # Windows cmd.exe:
    .venv\Scripts\activate
    # PowerShell:
    .venv\Scripts\Activate.ps1
    # Unix/macOS:
    source .venv/bin/activate

    pip install -e .
    # dev extras:
    pip install -e .[dev]

Usage (quick)
-------------
The `Registry` behaves like a mapping from string keys to values. Examples:

```python
from mnemoreg import Registry, OverwritePolicy

r = Registry[str, int]()
r['one'] = 1
assert r['one'] == 1

# decorator registration (explicit key) with optional description
@r.register('plus', description='adds one')
def plus(x: int) -> int:
    return x + 1

assert r['plus'](4) == 5

# decorator registration (uses function.__name__ when key omitted)
@r.register()
def multiply(x: int, y: int) -> int:
    return x * y

assert r['multiply'](3, 4) == 12

# bulk operations (acquires the same lock for the block)
with r.bulk():
    r['a'] = 1
    r['b'] = 2

# removal
del r['a']

# snapshot returns a mapping of StoredItem objects that wrap the value and expose an optional description
snap = r.snapshot()      # shallow dict copy; values are StoredItem objects
item = snap['plus']
assert item.value(2) == 3
assert item.description == 'adds one'

# to_dict / to_json produce a shallow mapping of *values* (descriptions are not included by default)
# if you need to persist descriptions you can use from_dict/update with the Stored tuple form described below.
s = r.to_json()          # JSON string of values only
new_r = Registry.from_json(s)
```

Storing descriptions and the Stored tuple
----------------------------------------
Internally, the storage backend stores entries as a 2-tuple (value, description).
This is exposed via a convenient type alias in the library (see `mnemoreg._types.Stored`).
The tuple form is:

    (value_or_None, Optional[str])

Examples:

- Register via decorator with description:

    @r.register('f', description='does something')
    def f(x):
        ...

- Update or create entries using the Stored form (keeps description):

    # Stored tuple (value, description)
    r.update({'a': (1, 'one')})

- Create a Registry from a dict that already contains descriptions:

    new = Registry.from_dict({'k': (42, 'answer')})

Notes about serialization and round-tripping
-------------------------------------------
- `Registry.to_dict()` and `Registry.to_json()` produce mappings of values only
  (they call `snapshot()` and extract `.value`). Descriptions are intentionally
  omitted from that shallow JSON representation to keep `to_json()` focused on
  JSON-serializable payloads.
- If you want to persist descriptions you must use the Stored tuple form when
  calling `Registry.from_dict()` or `Registry.update()` so the description is
  kept in the backend. Example:

    saved = registry._store.to_dict()  # low-level store representation: {k: (value, description)}
    Registry.from_dict(saved)

API / behavior summary
----------------------
- class `Registry(Generic[K, V])`
  - Mapping-like: `__getitem__`, `__setitem__`, `__delitem__`, `__iter__`, `__len__`, `__contains__`.
  - `register(key: Optional[str] = None, description: Optional[str] = None)` — decorator to register functions/values with optional description.
  - `get(key, default=None)`, `snapshot()` — read helpers. `snapshot()` returns a dict of `StoredItem` objects which provide `.value` and `.description`.
  - `from_dict(mapping)`, `from_json(s)` — classmethods to build a Registry from serialized data; `from_dict` accepts either a plain mapping of values or the internal Stored tuple mapping `(value, description)`.
  - `to_json(**kwargs)` — serialize shallowly to JSON (values only).
  - `bulk()` — context manager that acquires the registry lock for batched operations.
  - `update(mapping)`, `clear()`.

- Overwrite behavior: controlled by `OverwritePolicy` (FORBID = 0, ALLOW = 1, WARN = 2). The default is FORBID.

- Exceptions: `AlreadyRegisteredError`, `NotRegisteredError`.

Notes and caveats (important)
- Keys must be strings (type parameter `K` is bound to `str`). The registry will raise on invalid keys (empty string, contains whitespace, or wrong type).
- `to_dict()` / `to_json()` intentionally drop descriptions. Use `from_dict()`/`update()` with Stored tuples if you need to preserve descriptions.
- The `Stored` alias (Tuple[Optional[V], Optional[str]]) is available at `mnemoreg._types` for advanced uses.

Development
-----------
This project uses pytest, ruff, black, isort, mypy, and pre-commit hooks.

Typical local developer setup:

```bash
python -m venv .venv
# activate the venv
pip install -e .[dev]
```

Run tests:

```bash
python -m pytest -q
```

Format / lint / checks (examples):

```bash
ruff check --fix .
isort --profile black .
black .
pre-commit run --all-files
python -m mypy mnemoreg --ignore-missing-imports
```


CI & publishing
----------------
There are GitHub Actions workflows under `.github/workflows/`.

The `publish.yml` workflow (current behavior):
- Triggers on: push to `main` or `master`, tag pushes matching `v*.*.*`, pull requests to `main`/`master`, and `workflow_dispatch`.
- The workflow runs tests, then attempts to determine/create a semver tag and push it, builds distributions, and (optionally) uploads to PyPI if the `MNEMOREG_PY_PI_TOKEN` secret is configured.

If you don't want to trigger the publish flow or accidental publishes:
- Work from a fork and open PRs from the fork — forks cannot push tags to the upstream repo and won't have the PyPI secret.
- Work on non-main branches; pushes to non-main branches do not trigger the `push` trigger (but PRs to `master` do trigger `pull_request`).
- Repository maintainers can tighten the workflow triggers (recommended): e.g. remove `pull_request` trigger, only run publish on explicit tag push or manual `workflow_dispatch`.
- Ensure `MNEMOREG_PY_PI_TOKEN` is never added unless you intend to publish.

Contributing
------------
Please follow the project's code style (black/ruff/isort) and add tests for new behavior.
Suggested flow for external contributors:
- Fork the repo.
- Create a feature branch in your fork and open a PR to `master`.
- Add tests and run the test suite locally.

License
-------
MIT — see the `LICENSE` file.

Maintainers
-----------
i3iorn