Metadata-Version: 2.4
Name: mtdt
Version: 1.1.0
Summary: Type-safe mapping-like for arbitrary metadata annotations.
Project-URL: Homepage, https://github.com/mmoein2005/mtdt
Project-URL: Repository, https://github.com/mmoein2005/mtdt
Project-URL: Issues, https://github.com/mmoein2005/mtdt/issues
Author-email: Moein <mmfatemi2005@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# mtdt

Type-safe metadata mappings for Python. Two classes, zero dependencies.

**v1.1.0** · `pip install mtdt` · 190/190 tests passing · Python 3.11+ · MIT

---

## The idea

Metadata usually ends up in a plain `dict[str, Any]`. That works until two libraries both pick `"source"`, until someone stores a `list` where a `str` was expected, or until a cached object stays alive because a metadata dict is still holding it.

`mtdt` moves the policy into the key:

```python
from mtdt import MetadataStore, key
from mtdt.exceptions import InvalidValue

def must_be_str(value):
    if not isinstance(value, str):
        raise InvalidValue(what="author", expected="a str", got=value)

AUTHOR  = key("author", type=str, validator=must_be_str)
VERSION = key("version", type=int, immutable=True)

store = MetadataStore()
store[AUTHOR] = "ada"
store[VERSION] = 1

store[AUTHOR] = 42    # InvalidValue: expected a str for author; got 42.
store[VERSION] = 2    # InvalidUsage: immutable key already set
del store[VERSION]    # InvalidUsage: immutable keys cannot be removed
store["author"]       # InvalidType: keys must be Key instances (strict mode)
```

Keys are identity-based by default, so two independently created `key("author")` objects are *different keys*. Collisions between unrelated packages stop being possible.

---

## Keys

`Key` is a frozen, slotted, keyword-only dataclass. While you can instantiate it directly, the recommended entry points are the `key` and `key_factory` factories.

| Field | Default | Meaning |
| --- | --- | --- |
| `value` | `None` | Arbitrary label or payload. Not used for lookup unless `use_identity=False`. |
| `expected_type` | `Any` | Static-analysis hint. Does **not** enforce anything at runtime by itself. |
| `use_identity` | `True` | Equality/hash by object identity. `False` switches to structural comparison. |
| `weakref` | `False` | Route the value into a `WeakValueDictionary`. |
| `immutable` | `False` | Write-once. Blocks overwrite *and* deletion. |
| `validator` | `None` | `Callable[[T], None]`, raises `Invalid` on failure. |

The `key` factory is the normal entry point:

```python
from mtdt import key

K = key("label", type=int)                    # positional value, keyword config
K = key("label", type=int, weakref=True)
K = key()                                     # anonymous, identity-only key
```

`expected_type` exists for your type checker. `store[K]` is typed as `T` when `K: Key[T]`, which is most of the practical value. Runtime enforcement only happens if a validator is attached.

### Structural keys

With `use_identity=False`, keys compare by a structural tuple of their fields. Note that structural equality requires the *same validator object*, not an equivalent one. Two structurally identical keys built with separately defined validator functions will not match.

### Custom factories

`key_factory` bakes in defaults and can namespace keys:

```python
from mtdt import key_factory

mykey = key_factory(immutable=True, extra_value="myapp")

REGION = mykey("region", type=str)
REGION.value      # ('myapp', 'region')
REGION.immutable  # True
```

`extra_value` is mostly for debugging and `repr` clarity — identity already guarantees uniqueness.

---

## Weak values

If a key is marked `weakref=True`, its values are held weakly. When the value is garbage collected, the entry is automatically removed from the store.

```python
CACHE = key("cache", weakref=True)

store[CACHE] = expensive_object
del expensive_object
CACHE in store    # False — entry vanished with its last strong reference
```

Weak keys read and write through a separate `WeakValueDictionary`. Storing a value that cannot be weakly referenced (`int`, `str`, `tuple`, most builtins) raises `InvalidUsage` rather than failing silently. `len()`, iteration, and the `keys()` / `values()` / `items()` views span both backing stores: strong entries first, then weak. The views are live.

---

## Validation

Three ways to attach a validator, in order of directness:

**1. Pass it in.** Always works, no magic:

```python
K = key("port", type=int, validator=check_port)
K = key("anything", type=int, validator=None)   # explicitly opt out
```

**2. Attach it to your class.** `set_instancecheck_validator` installs a `__metadata_key_validator__` attribute that factories pick up automatically:

```python
from mtdt import set_instancecheck_validator

@set_instancecheck_validator
class Config: ...

K = key("config", type=Config)   # isinstance validator found automatically
store[K] = "not a Config"        # InvalidValue
```

Only works on classes you can set attributes on — not builtins, not most C extension types.

**3. Embed it in the annotation.** For cases where the type itself is shared:

```python
from typing import Annotated
from mtdt import EmbeddedValidator

Port = Annotated[int, EmbeddedValidator(check_port)]
K = key("port", type=Port)
```

Validators run on `__setitem__`, `setdefault` (only when inserting), and `update`. They do not run on `unchecked_update`.

---

## Strict and non-strict stores

`MetadataStore` operates in two modes, determining what can be used as a key:

- **Strict Mode (Default):** Only `Key` instances are accepted. This unlocks the full power of `mtdt`, including runtime validation, immutability enforcement, weak reference routing, and namespacing. Use this when you want type-safety and robust metadata management.
- **Non-Strict Mode (`strict=False`):** Any hashable Python object can be used as a key. In this mode, `mtdt` acts exactly like a standard Python `dict`, completely bypassing all `mtdt`-specific features. Use this as an escape hatch for migrating an existing string-keyed dict where you need a drop-in dictionary replacement.

```python
store = MetadataStore()               # strict: Key instances only
loose = MetadataStore(strict=False)   # any hashable key, kwargs allowed
```

---

## Bulk updates

`update()` validates everything *before* writing anything, so a rejected entry leaves the store untouched:

```python
store.update({AUTHOR: "ada", VERSION: 1})
store.update([(AUTHOR, "ada")])
```

It rejects another `MetadataStore` as its argument — merging two stores would silently re-run validators against already-validated data and mix the strong and weak partitions. Use `unchecked_update()` when that is what you actually want:

```python
store.unchecked_update(other_store)   # no type checks, no validators, no immutability
```

`unchecked_update` is deliberately unsafe. Reach for it for trusted internal copies and fast paths, nothing else.

---

## Errors

All exceptions derive from `Invalid`, a dataclass exception carrying `what` / `expected` / `got` / `note`. The `note` is attached via `add_note()`, so it shows up in tracebacks.

| Exception | Also a | Raised when |
| --- | --- | --- |
| `InvalidType` | `TypeError` | Key is not a `Key` in strict mode, or not hashable in non-strict mode. Also for a non-callable `validator` argument. |
| `InvalidValue` | `RuntimeError` | A validator rejected the value. |
| `InvalidUsage` | — | Immutable key overwritten or deleted; non-weakref-able value under a weak key; `MetadataStore` passed to `update()`; kwargs passed to `update()` in strict mode. |
| `KeyError` | — | Standard mapping misses on `__getitem__`, `pop`, `popitem`. |

Because `InvalidType` is a `TypeError` and `InvalidValue` is a `RuntimeError`, existing `except TypeError` handlers keep working.

---

## When this is the right tool

- Attaching metadata to objects you do not own, where you cannot add attributes.
- Plugin or extension systems where several independent parties annotate the same object and must not collide.
- Metadata with mixed lifetimes, where some entries should not keep their values alive.
- Configuration or registry entries that must be written exactly once.
- Anywhere a `dict[str, Any]` has already caused a collision or a type bug.

## When it is not

- **As a general-purpose dict.** Every operation adds validation overhead and key routing. If you just need a mapping, use a `dict`.
- **When keys must survive serialization.** Identity-based keys do not round-trip through `pickle` or JSON: an unpickled key is a new object and will not match the original. `use_identity=False` helps, but the validator is compared by `id()`, so it is still fragile. Design around this rather than fighting it.
- **When you want real runtime type enforcement.** `expected_type` is a hint, not a check. Generics, protocols, and nested containers are out of scope. Use `pydantic`, `attrs`, or `beartype` for that.
- **When the shape is known and fixed.** A dataclass or `TypedDict` is clearer, faster, and better supported by tooling.
- **When key definitions cannot be shared.** Identity keys must be importable from a single module by every consumer. If your producers and consumers only agree on strings, this model does not fit.

---

## Known limitations

- `typing.Union` and `typing.Optional` are not supported by validator inference. PEP 604 unions (`int | str`) are.
- `infer_instancecheck_validator` correctly returns `None` for types that are not `isinstance`-safe (e.g., `list[int]`), meaning you must provide an explicit `validator=` or `EmbeddedValidator` for parameterized generics.
- The `repr` of a store shows the weak store by address; CPython deliberately suppresses `WeakValueDictionary` contents during formatting to avoid triggering collection.

---

## API surface

```python
from mtdt import (
    Key, MetadataStore,
    key, key_factory,
    EmbeddedValidator,
    get_embedded_validator,
    set_instancecheck_validator,
    infer_instancecheck_validator,
)
from mtdt.exceptions import Invalid, InvalidType, InvalidUsage, InvalidValue
```

## License

MIT.
