Source code for melder.aether.conduit.meld.contracts.spell_map

from typing import Any, Optional, Union, Tuple, ClassVar



# Melder Imports
from melder.utilities.general_base.cleanable import Cleanable
from melder.utilities.helpers.general_helpers import SpellInputUtils


[docs] class SpellMap(Cleanable): """ Declarative DI descriptor for normal spellbook-local resolution. `SpellMap` is the explicit descriptor form of ordinary Melder DI intent. It tells SpellCrafter and Meld which spell or frame/binding identity should be injected at a given parameter or attribute location, optionally along with an override payload that should be carried into runtime construction. Unlike `SpellContract`, this descriptor does not declare a cross-conduit hole that will be satisfied later. It stays inside the current resolution world. SpellCrafter and the runtime planning path inspect it, derive the canonical lookup identity, and then resolve a local spell through the usual Spellbook, SpellIndex, and Existence machinery. Supported shapes: - `SpellMap(MyService)` Direct type-based DI where the spell itself supplies the lookup identity. - `SpellMap(ILogic)` Frame-based DI where a Protocol / interface acts as the lookup frame. - `SpellMap(MyService, spellframe=ILogic, binding_name="primary")` Fully explicit spell + frame + binding declaration. - `SpellMap(spell=None, spellframe=ILogic, binding_name="primary")` Frame-only descriptor where the DI key is derived entirely from frame and binding. Typical usage: `python class MyService: def __init__(self, repo=SpellMap(MyRepo)): self.repo = repo class UsesLogic: def __init__(self, logic=SpellMap(ILogic, binding_name="primary")): self.logic = logic class UsesConfig: def __init__( self, cfg=SpellMap( spell=None, spellframe=IAppConfig, binding_name="primary", ), ): self.cfg = cfg ` Contract: - Pure intent object; it does not resolve providers by itself. - Used for normal in-graph DI, not late conduit linking. - Carries spell/frame/binding identity plus optional override payload. - Cleanable and invalid after cleanup. - Should not be subclassed or treated like a runtime-resolved object. Threading: Value-shaped and effectively immutable after construction: the four slots are set in `__init__` and read thereafter. No lock is taken, because a descriptor written into a constructor default is shared read-only across every resolution that reads it. Lifecycle / Cleanup: Cleanable. The descriptor lives as long as the class default that declares it, which in practice is the lifetime of the defining module. Registration: MELDER KERNEL, USER-INSTANTIATED but NOT user-bindable. A user writes `SpellMap(MyRepo)` in their own constructor default constantly, so instances are authored outside melder - but binding the SpellMap CLASS itself is refused, which would be meaningless because a descriptor is a statement of intent, not a service to resolve. Subsystem Context: One of the two declarative DI descriptors, paired with `SpellContract`. The division is scope: `SpellMap` stays INSIDE the current resolution world and resolves through the ordinary Spellbook / SpellIndex / Existence machinery, while `SpellContract` declares a hole a future linked conduit may fill. Phase 1 classifies each parameter into a `ParameterDIShape`; a SpellMap default lands as `SPELLMAP_DEFAULT`, and Phase 3 performs the actual candidate resolution. System Context: The four supported shapes exist because DI identity in Melder is a (spell, frame, binding) triple rather than a single type key, and different call sites know different parts of it. Passing a concrete class supplies the whole identity; passing a Protocol supplies a frame and asks the graph to find the implementation; the frame-only form (`spell=None`) exists for the case where the caller deliberately knows NOTHING but the contract and the binding name. Ambiguity is a build-time failure, not a runtime one: a SpellMap default that resolves to zero or to multiple candidates raises rather than silently picking. That is the whole reason the explicit `binding_name` form exists - it is the documented way to disambiguate when several spells legitimately satisfy the same frame. AGENT_ACCESS: public AGENT_PURPOSE: access: public. Declarative DI descriptor for normal in-graph resolution. Write it as a constructor default: SpellMap(MyRepo), SpellMap(ILogic, binding_name='primary'), or frame-only with spell=None. Zero or multiple matches raise at build time. """ __slots__ = Cleanable.__slots__ + [ "spell", "spellframe", "binding_name", "override", ] def __init__( self, spell: Any | None = None, *, spellframe: Optional[Any] = None, binding_name: Optional[str] = None, override: Optional[Union[dict, list, tuple]] = None, ) -> None: """ Create one `SpellMap` declarative descriptor. Args: spell: Primary lookup target, or `None` for frame-only SpellMaps. When not `None`, this is typically: - the concrete implementation class - a Protocol used as a frame-like identity - a callable spell target - or another resolver-supported object Resolution semantics: - if `spellframe` is provided, the frame becomes the primary grouping identity and `spell` acts as the concrete spell side of the declaration - if `spellframe` is omitted, `spell` itself supplies the DI key - if `spell` is `None`, `spellframe` must be present and becomes the sole DI identity spellframe: Optional logical interface, Protocol, or frame key used to group spells in Spellbook lookup space. binding_name: Optional named binding used to disambiguate multiple providers under the same frame. Kept exactly as written, the way `Bind` keeps a spell's binding name (0.2.8226; it was lowercased here before). Matching is case-insensitive: Phase 3 compares normalized names, so "ScanProfile", "scanprofile" and "SCANPROFILE" select the provider bound as "ScanProfile", as `meld` does. `None` (or "") keeps default-binding semantics. override: Optional construction payload for the provider spell this descriptor resolves to, applied when that provider is constructed for this consumer: - `dict`: keyword arguments for the provider's constructor - `list` / `tuple`: positional arguments Values may be any Python object. They are never copied into the compiled plan or the creation cache: phase 9 records a reference to this descriptor and the executor reads the live value from it at hydration, so the provider receives the object itself, by identity (2026-09-26). A caller payload on `meld(override=...)` still wins over this payload (override > contract > dependency). A payload on a non-resolvable definition is refused by phase 3. When `None`, no payload is attached. Raises: ValueError: If both `spell` and `spellframe` are omitted, because the descriptor would have no DI identity. TypeError: If `binding_name` is neither a string nor `None`. Returns: None. """ if spell is None and spellframe is None: raise ValueError( "SpellMap requires at least one of `spell` or `spellframe` " "to be provided." ) if binding_name is not None and not isinstance(binding_name, str): raise TypeError( "SpellMap binding_name must be a string or None; " f"got {type(binding_name).__name__}." ) super().__init__() self.spell = spell self.spellframe = spellframe # Kept as written, as Bind keeps a spell's binding name; `canonical_key` # and Phase 3 compare its normalized form, so case never decides a match. self.binding_name = binding_name # Preserve the caller payload by reference; None means no payload is attached. self.override = override
[docs] def cleanup(self) -> None: """ Release descriptor references and invalidate the object. Contract: - Idempotent and safe to call multiple times. - Clears mutable override payloads before dropping references. - After cleanup the descriptor should be treated as unusable and callers should rely on `check_cleaned()` before reading it. Returns: None. """ if self._cleaned: return # No internal lock needed; this is a simple intent object. self._cleaned = True # Clear override payload if it is a container. if isinstance(self.override, (list, dict)): self.override.clear() self.override = None self.spell = None self.spellframe = None self.binding_name = None
@property def lookup_triplet(self) -> tuple[Any, Optional[Any], Optional[str]]: """ Return the raw descriptor identity tuple. This is the shape SpellCrafter and the runtime planning path consume before they derive the normalized key or resolve the underlying spell. Contract: - Returns the RAW, AS-SUPPLIED triplet - the spell, spellframe and binding name exactly as the caller gave them, WITHOUT normalization. - This is NOT the registry key. Use `canonical_key` when you need the normalized identity the spellbook actually indexes by; the two can differ for the same SpellMap. Threading: Pure computation over immutable fields; safe from any thread. Lifecycle / Cleanup: Carries no cleaned-state guard. Returns: tuple[Any, Optional[Any], Optional[str]]: `(spell, spellframe, binding_name)` exactly as stored on the descriptor. Notes: - For frame-only SpellMaps (`spell is None`), only `spellframe` and `binding_name` contribute to the runtime identity. - For fully explicit SpellMaps, higher layers may use both `spell` and `spellframe` when enforcing resolution rules. - The binding name is the caller's text (0.2.8226); `canonical_key` carries the normalized form that matching uses. """ return (self.spell, self.spellframe, self.binding_name) @property def canonical_key(self) -> Tuple[str, str]: """ Return the normalized `(frame_key, binding_key)` pair. This is the canonical Spellbook-style key derived through `SpellInputUtils.normalize_spell_key(...)`. It is the stable string pair higher layers use when indexing or matching SpellMap intent. Shape rules: - when `spellframe` is present, the frame key is derived from `spellframe` - when `spellframe` is absent, the frame key is derived from `spell` - binding defaults normalize to `"__default__"` inside the helper path Contract: - NORMALIZED identity: it runs the raw triplet through the shared key normalizer, so equivalent-but-differently-spelled inputs collapse to the same key. This is what the spellbook indexes by. - Recomputed on every access rather than cached, so it always reflects the current field values. Threading: Pure computation over immutable fields; safe from any thread. Lifecycle / Cleanup: Carries no cleaned-state guard. Returns: Tuple[str, str]: Normalized `(frame_key, binding_key)` pair. """ frame_key, bind_key = SpellInputUtils.normalize_spell_key( spell=self.spell, spellframe=self.spellframe, binding_name=self.binding_name, ) return frame_key, bind_key @property def spell_key(self) -> Tuple[str, str]: """ Compatibility alias for `canonical_key`. Contract: - ALIAS for `canonical_key`, kept for call-site readability. Identical behaviour - it is the NORMALIZED key, not the raw triplet. Threading: Pure computation over immutable fields; safe from any thread. Lifecycle / Cleanup: Carries no cleaned-state guard. Returns: Tuple[str, str]: The same normalized key pair returned by `canonical_key`. """ return self.canonical_key def __repr__(self) -> str: """ Return a debug-oriented representation of the SpellMap descriptor. Contract: - Includes the override payload alongside the identity fields, so a `SpellMap` repr shows what it will DO as well as what it names. - Unguarded, unlike some other repr implementations in the codebase, so it stays safe to log. Threading: Pure computation over immutable fields; safe from any thread. Lifecycle / Cleanup: Carries no cleaned-state guard. Returns: str: Representation showing stored spell/frame/binding/override fields without performing any resolution. """ return ( f"<SpellMap spell={self.spell!r} " f"spellframe={self.spellframe!r} " f"binding_name={self.binding_name!r} " f"override={self.override!r}>" )