import threading
from types import TracebackType
from typing import TYPE_CHECKING, NoReturn, Optional, Type, Union, ClassVar
from melder.aether.conduit.creations.creations import Creations
from melder.utilities.general_base.cleanable import Cleanable
from melder.utilities.helpers.id_builder import IDBuilder
from melder.aether.conduit.meld.spellspace_meld import SpellSpaceMeld
from melder.utilities.custom_exceptions.spell_space_scope_error import SpellSpaceScopeError
if TYPE_CHECKING:
from melder.aether.conduit.creations.conduit_creations import (
ConduitCreations,
)
from melder.aether.conduit.meld.conduit_meld import ConduitMeld
from melder.aether.conduit.spell_space.spell_space_pool import SpellSpacePool
from melder.aether.conduit.spell_space.spell_space_thread_state import (
SpellSpaceThreadState,
)
[docs]
class SpellSpace(Cleanable):
"""
Explicit scope handle for `Existence.unique_per_spell_space`.
Purpose:
Represent one spellspace-bound resolution window without storing a live
conduit back-reference. The conduit remains the factory for spellspaces,
but the runtime scope object carries only the explicit collaborators it
needs to execute, reset, and unregister itself.
Contract:
- Owns one stable spellspace id and one stable owner conduit id.
- Delegates runtime execution through one owned `SpellSpaceMeld`.
- Clears spellspace-scoped instances through the injected `Creations`.
- Unregisters itself from the injected spellspace registry on cleanup.
- Does not own conduit-wide resolution caches or control-plane state.
- Acts as its own context manager for the managed
`with conduit.enter_spellspace() as space:` lane:
`enter_spellspace()` acquires and activates the space (pool acquire
plus thread-stack push) before the `with` statement runs, so
`__enter__` is a trivial self-return and `__exit__` performs the
LIFO pop-validated recycle. No per-cycle wrapper object exists.
- Nested managed scopes are first-class: each `enter_spellspace()`
call pushes one new independent scope onto the per-thread stack
(A -> B -> C -> D to any depth), each scope owns its own
spellspace-local storage, and exits must unwind in LIFO order.
- One managed activation per acquisition: after `__exit__` recycles
the space back to the pool, re-entering the same object is a caller
contract violation (the trusted-private-caller posture documented
on `SpellSpacePool.release`); LIFO validation in `pop_expected`
fails fast on the common misuse shapes.
- Carries one lease flag, `_released`: the pool sets it on release and
clears it on acquisition, and a released space refuses `meld` and
`purge` with SpellSpaceScopeError. After permanent cleanup it stays
True as a documented tombstone.
- Every exit finishes its pool return (or destroy) when a disposal
method fails - the store is emptied before any method runs - and then
raises the disposal ExceptionGroup.
Threading:
- Uses an internal `RLock` for cleanup/reset idempotence.
- The managed enter/exit lane itself is thread-confined by
construction (pool deque hand-off plus per-thread stack), matching
the `recycle_from_managed_context` confinement contract.
Lifecycle:
- Created by `Conduit.create_spellspace(...)` or `Conduit.enter_spellspace(...)`.
- Normal cleanup returns the spellspace to its conduit-local pool.
- Permanent cleanup drops all injected collaborators.
Registration:
MELDER KERNEL - guarded, access=public. Users DRIVE it as a context
manager (`with conduit.enter_spellspace() as space:`) and meld against
the active space, but never construct or `bind()` it - the conduit is the
factory and the guard refuses binding.
Subsystem Context:
The scope handle of the conduit `spell_space` subsystem, backing
`Existence.unique_per_spell_space`. The conduit is the factory; this
object carries only the explicit collaborators it needs (an owned
`SpellSpaceMeld` for execution, an injected `Creations` for its scoped
instances, and the registry/pool it unregisters and recycles into). It
holds NO live conduit back-reference. Managed entry lives on a per-thread
stack, so nested `enter_spellspace()` scopes (A -> B -> C) each own their
storage and must unwind LIFO.
System Context:
This is how the DGR gives a caller an EXPLICIT, nestable resolution
window without leaking conduit-wide state into it: instances resolved as
`unique_per_spell_space` live and die with the space - every exit clears
them before the space goes back to its pool, and a released space
refuses `meld`, so a recycled shell cannot serve an object built through
a stale handle. Making the space its own context manager (trivial `__enter__`,
LIFO-validated `__exit__` recycle) and confining the managed lane to one
thread is what lets the pool hand spaces back and forth without per-cycle
wrapper objects or cross-thread synchronization.
AGENT_ACCESS: public
AGENT_PURPOSE:
access: public. Explicit request scope for Existence.unique_per_spell_space. Enter via
conduit.enter_spellspace(); the block exit clears spellspace-scoped instances and returns
the space to its pool; a released space refuses meld and purge.
"""
__slots__ = Cleanable.__slots__ + [
"_lock",
"_id",
"_owner_conduit_id",
"_meld",
"_creations",
"_owner_conduit_creations",
"_registry_tracked",
"_spellspace_registry",
"_spellspace_pool",
"_spellspace_stack_state",
"_permanent_cleanup_requested",
"_released",
]
def __init__(
self,
*,
owner_conduit_id: str,
conduit_meld: ConduitMeld,
owner_conduit_creations: ConduitCreations,
spellspace_registry: set["SpellSpace"],
spellspace_pool: SpellSpacePool,
spellspace_stack_state: SpellSpaceThreadState,
) -> None:
"""
Create one explicit spellspace scope.
Args:
owner_conduit_id:
Stable id of the conduit that created this spellspace.
conduit_meld:
Conduit-facing meld runtime used as the shared-core source for
constructing this spellspace's dedicated front door.
owner_conduit_creations:
Conduit-owned creations manager used for conduit-scoped
existences (`unique_per_conduit`, current `many`) beneath the
spellspace front door.
spellspace_registry:
Conduit-owned registry set used for spellspace lifecycle
bookkeeping and self-unregistration.
spellspace_pool:
Conduit-local pool that should receive this spellspace on
normal cleanup.
spellspace_stack_state:
Conduit-owned per-thread active-scope stack holder. Injected
(not owned) so `__exit__` can perform the LIFO pop-validated
managed exit without a per-cycle wrapper object. The conduit
owns its lifecycle; this spellspace only references it.
Contract:
- Builds a POOLED, REUSABLE scope object. It starts leased (`_released`
False); the pool sets the flag on release and clears it on
acquisition, and a released space refuses `meld` and `purge`, so a
handle held across a recycle boundary fails loudly instead of
melding into an idle shell.
- Constructs its own `Creations` store keyed to this space, so
spellspace-scoped instances are isolated from conduit-scoped ones.
Owned State:
Owns its lock, id and creations store. Borrows the owning conduit's
registry, meld runtime and thread-state holder.
Threading:
Creates the lock guarding later scope operations.
Lifecycle / Cleanup:
Recycling clears its store and returns it to the pool; permanent cleanup
is what actually destroys it.
Returns:
None.
"""
super().__init__()
self._lock: threading.RLock = threading.RLock()
self._id: str = IDBuilder.create_id()
self._owner_conduit_id: str = owner_conduit_id
self._creations: Creations = Creations(
owner_conduit_id=owner_conduit_id,
id=self._id,
)
self._owner_conduit_creations: ConduitCreations = owner_conduit_creations
# A spellspace is not a lineage root: a `unique_per_conduit_lineage`
# spell resolved from within it stores in the owner conduit's lineage
# root, so the spellspace meld is handed the owner conduit's lineage-root
# store (`conduit_meld._root_creations`) and uses it for those melds.
# Likewise a spellspace is not a cluster member: a
# `unique_per_conduit_cluster` spell resolved from within it stores in
# the owner conduit's cluster team store, so the spellspace meld
# references the owner conduit's cluster facade
# (`conduit_meld._cluster_creations`) rather than owning its own. The
# facade is filled on election by ConduitCluster, so a spellspace built
# after election still resolves into the same team store.
self._meld: SpellSpaceMeld = SpellSpaceMeld(
spellspace=self,
spellspace_creations=self._creations,
conduit_creations=self._owner_conduit_creations,
root_creations=conduit_meld._root_creations,
cluster_creations=conduit_meld._cluster_creations,
spellbook=conduit_meld._spellbook,
conduit_id=conduit_meld._conduit_id,
resolution_conduit_id=conduit_meld._resolution_conduit_id,
dynamic_environment=conduit_meld._dynamic_environment,
meld_hooks=conduit_meld._meld_hooks,
)
# A temporary owner map must be reset on return even when this Space
# never registers a callback itself.
self._meld._inherit_meld_hooks(conduit_meld)
self._registry_tracked: bool = False
self._spellspace_registry: set[SpellSpace] = spellspace_registry
self._spellspace_pool: SpellSpacePool = spellspace_pool
self._spellspace_stack_state: SpellSpaceThreadState = (
spellspace_stack_state
)
self._permanent_cleanup_requested: bool = False
self._released: bool = False
def __enter__(self) -> "SpellSpace":
"""
Return this already-activated managed spellspace.
Contract:
- `Conduit.enter_spellspace()` performs the real activation work
(pool acquisition plus per-thread stack push) before the `with`
statement begins, so this method is a trivial self-return on
the hot path.
- Valid only for spaces handed out by `enter_spellspace()`; using
a manually created (`create_spellspace()`) space as a context
manager raises `SpellSpaceScopeError` on exit because it was
never pushed onto the per-thread stack.
Returns:
SpellSpace: This spellspace, active as the current top-of-stack
scope for the calling thread.
"""
return self
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
"""
Pop this scope off the per-thread stack and recycle it.
Contract:
- Validates LIFO integrity: this space must be the current
top-of-stack scope for the calling thread (nested scopes must
unwind innermost-first).
- Recycles through the managed pooled fast lane, which clears
spellspace-local creations before pool return so scope teardown
stays deterministic and owner-driven. The return finishes even
when a disposal method fails; the disposal ExceptionGroup then
propagates, with the block's exception as its context when the
block raised.
- A space already released or destroyed inside the block (an
explicit `cleanup()`, or its conduit's pool return or teardown)
only leaves the stack and returns: its store was already
disposed.
- The common lane of `recycle_from_managed_context` (untracked, no
permanent teardown requested) runs inline here, saving a call per
scope; every other lane goes through that method.
- Runs on exceptions too, exactly like the former wrapper.
- After exit this object belongs to the pool again; re-entering
it without a fresh `enter_spellspace()` acquisition is a caller
contract violation.
Raises:
SpellSpaceScopeError:
If this space is not the calling thread's active top-of-stack
scope.
ExceptionGroup:
Disposal failures, raised after the space is back in its pool.
Returns:
None.
"""
if self._released:
self._leave_after_early_release()
return None
self._spellspace_stack_state.pop_expected(self)
# Hot path (one exit per managed scope): the common lane of
# `recycle_from_managed_context`, inlined to save a call frame and a
# second read of the lease flag. Keep the two in step.
if self._permanent_cleanup_requested or self._registry_tracked:
self.cleanup()
return None
try:
self._creations.reset_for_pool_unlocked()
finally:
# Finish the return even when a disposal method failed: the store
# was swapped empty before any method ran, so the shell is clean.
if self._meld._meld_hooks_modified:
self._meld._reset_pooled_meld_hooks()
self._spellspace_pool.release(self)
return None
[docs]
def cleanup(self) -> None:
"""
Cleanup this spellspace through either the reusable or permanent lane.
Contract:
- Normal cleanup returns this spellspace to the conduit-local pool
after reusable cleanup. A space that is already released is left
as it is, so a second cleanup never pools it twice.
- `permanent_cleanup()` forces the destructive lane even when a
pool is attached.
- Either lane finishes when a disposal method fails (the store is
emptied before any method runs), then the disposal ExceptionGroup
propagates.
Raises:
ExceptionGroup: Disposal failures, after the space is pooled or destroyed.
Returns:
None.
"""
if self._cleaned:
return
with self._lock:
if self._cleaned:
return
if self._permanent_cleanup_requested:
self._cleanup_for_destroy()
return
if self._released:
return
try:
self._cleanup_for_pool_reuse()
finally:
self._spellspace_pool.release(self)
[docs]
def permanent_cleanup(self) -> None:
"""
Permanently destroy this spellspace instead of returning it to a pool.
Contract:
- Flips the permanent cleanup flag immediately.
- Reuses the normal cleanup entrypoint so all public teardown still
flows through one surface.
Returns:
None.
"""
self._permanent_cleanup_requested = True
self.cleanup()
[docs]
def recycle_from_managed_context(self) -> None:
"""
Recycle one managed pooled spellspace through the fast common lane.
Purpose:
Avoid the generic cleanup branch work on the common
`enter_spellspace()` managed exit path, where the spellspace is
untracked in the registry and is expected to return directly to the
conduit-local pool after clearing only spellspace-local state.
Contract:
- Valid only for live managed spellspaces acquired through the
untracked pool path; a space that is already released (or
destroyed) returns at once.
- Falls back to the generic cleanup entrypoint when permanent
teardown was requested or the spellspace is registry-tracked.
- Clears spellspace-local creations before returning this
spellspace to the pool.
- Keeps collaborator references intact for later reuse, restoring
temporary hooks after disposal and before idle publication.
- Finishes the hook reset and pool release when a disposal method
fails (the store was swapped empty first), then the disposal
ExceptionGroup propagates.
Threading / Concurrency:
- This lane runs without the spellspace `RLock` because managed
spellspaces are thread-confined by construction: the pool's
deque pop hands the object to exactly one thread, the object
lives only on that thread's `SpellSpaceThreadState` stack, and
`pop_expected(...)` validates LIFO ownership before this method
runs. The pool's deque append on release is the hand-off point
to the next acquiring thread.
- The spellspace-local clear uses
`Creations.reset_for_pool_unlocked()`: the same thread
confinement that justifies skipping the spellspace lock also
covers the store's internal lock on this lane. The clear remains
explicit and immediate, not deferred; disposal-bearing stores
still fall back to the fully locked teardown flow inside that
method.
- Concurrent external `cleanup()` / `permanent_cleanup()` against
an in-flight managed spellspace is a caller contract violation
(the object is not idle in the pool and not registry-tracked),
matching the trusted-private-caller posture documented on
`SpellSpacePool.release(...)`.
Raises:
ExceptionGroup: Disposal failures, after the space is back in its pool.
Returns:
None.
"""
if self._released:
return
if self._permanent_cleanup_requested or self._registry_tracked:
self.cleanup()
return
# Common unchanged path is lock-free by the confinement contract above;
# a temporary hook map uses the existing Meld lock only while resetting.
# The unlocked variant is valid here precisely because this lane is
# the confinement-guaranteed managed exit; the explicit
# spellspace-local clear still happens before pool return so scope
# teardown stays deterministic and owner-driven.
try:
self._creations.reset_for_pool_unlocked()
finally:
# Finish the return even when a disposal method failed: the store
# was swapped empty before any method ran, so the shell is clean.
if self._meld._meld_hooks_modified:
self._meld._reset_pooled_meld_hooks()
self._spellspace_pool.release(self)
def _cleanup_for_pool_reuse(self) -> None:
"""
Clear spellspace-scoped runtime state so this object can be retained.
Contract:
- Clears spellspace-scoped creations for this spellspace id.
- Removes this spellspace from the active registry only when the
current lifecycle path registered it there.
- Still tolerates direct/manual registry insertion paths by
discarding the spellspace when it is currently present.
- Keeps collaborator references intact for later reuse.
- Restores temporary Meld hooks after disposal, before idle publication.
- Finishes the registry discard and hook reset when a disposal method
fails; the disposal ExceptionGroup then propagates.
"""
try:
self._creations.reset_for_pool()
finally:
if self._registry_tracked or self in self._spellspace_registry:
self._spellspace_registry.discard(self)
self._registry_tracked = False
self._permanent_cleanup_requested = False
if self._meld._meld_hooks_modified:
self._meld._reset_pooled_meld_hooks()
def _cleanup_for_destroy(self) -> None:
"""
Permanently destroy this spellspace and release collaborator references.
Contract:
- Clears spellspace-scoped creations before dropping references.
- Removes this spellspace from the current registry when tracked.
- Still tolerates direct/manual registry insertion paths by
discarding the spellspace when it is currently present.
- Deletes the pool reference as part of final teardown.
- Cleans the owned Meld before dropping it, releasing both hook references.
- Every step runs when a disposal method fails; the disposal
ExceptionGroup then propagates.
- `_released` is set and kept as a documented tombstone (True), so a
later `meld`, `purge` or block exit reads it instead of hitting a
deleted attribute; `_id` and `_lock` are kept as before.
- Removes this space from the top of the destroying thread's
spellspace stack when it is there (destroyed inside its own
managed block), before the thread-state reference is deleted, so
later scopes on that thread keep a consistent stack.
"""
self._released = True
try:
self._creations.cleanup()
finally:
self._meld.cleanup()
if self._registry_tracked or self in self._spellspace_registry:
self._spellspace_registry.discard(self)
self._spellspace_stack_state.discard_expected(self)
self._cleaned = True
del self._registry_tracked
del self._spellspace_registry
del self._owner_conduit_id
del self._meld
del self._creations
del self._owner_conduit_creations
del self._spellspace_pool
del self._spellspace_stack_state
del self._permanent_cleanup_requested
def _leave_after_early_release(self) -> None:
"""
Internal
End a managed block whose space was released or destroyed inside it.
Contract:
- A destroyed space already left the stack top in
`_cleanup_for_destroy`; there is nothing to do.
- A released space drops itself from the top of the calling thread's
stack when it is still there (an explicit cleanup inside the
block); its conduit's pool return has already drained the stack
otherwise. Nothing is disposed again and nothing is pooled twice.
Returns:
None.
"""
if self._cleaned:
return
self._spellspace_stack_state.discard_expected(self)
def _refuse_released(self) -> NoReturn:
"""
Internal
Refuse use of a space that is not leased.
Contract:
- A destroyed space raises the standard cleaned RuntimeError.
- A space released to its pool raises SpellSpaceScopeError naming the
space and how to get a live one.
Raises:
RuntimeError: If this space was permanently cleaned.
SpellSpaceScopeError: If this space was released to its pool.
"""
self.check_cleaned()
raise SpellSpaceScopeError(
f"SpellSpace {self._id} was released to its pool and cannot meld or purge. "
"Use a space only inside its `with conduit.enter_spellspace()` block, or before "
"cleanup() for a create_spellspace() space; acquire a new one to continue."
)
@property
def id(self) -> str:
"""
Return the stable identifier for this spellspace.
Contract:
- The space's versioned identity, assigned at construction.
- NOT `check_cleaned()` guarded, so it stays readable on a recycled or
cleaned space - useful for logging a space you no longer hold.
Threading:
Unsynchronized read; safe from any thread.
Lifecycle / Cleanup:
Readable after cleanup, by design.
Returns:
str: Unique id assigned at construction.
"""
return self._id
@property
def owner_conduit_id(self) -> str:
"""
Return the stable owner conduit id for this spellspace.
Contract:
- The conduit this space belongs to, fixed at construction - a pooled space
is never re-homed to a different conduit.
- NOT `check_cleaned()` guarded, matching `id`.
Threading:
Unsynchronized read; safe from any thread.
Lifecycle / Cleanup:
Readable after cleanup, by design.
Returns:
str: Owner conduit id injected at construction time.
"""
return self._owner_conduit_id
[docs]
def meld(
self,
spell: Optional[Union[str, object]] = None,
*,
spell_id: Optional[str] = None,
spellframe: Optional[Union[str, object]] = None,
binding_name: Optional[str] = None,
override: Optional[Union[dict, list, tuple]] = None,
) -> object:
"""
Delegate one meld call through the injected Meld runtime.
Call shape:
Positional strings are human SpellNames. Machine callers use
keyword-only `spell_id=...`. A warm id meld is served here from the
spellspace door's fast-door entry; any other id meld is forwarded
positionally to the door's existing ID fast lane.
Contract:
- Delegates resolution and lifecycle behavior to the shared
conduit meld runtime through its spellspace front door.
- Keeps human `spell` and machine `spell_id` identities mutually
exclusive.
- Warm id lane (2026-09-26): a `spell_id=...` meld with no
`spell`, `spellframe` or `binding_name` reads the door's
fast-door entry and applies the same guard ladder and arms as
`SpellSpaceMeld.meld` (plain, non-empty dict override, bound
existing object). A hit returns without entering the door; a miss
or any failed guard continues in the door, so results, errors and
the cache-emit check are identical to calling the door.
- Warm name/class lane (2026-09-27): a `meld("Name")` or
`meld(spell=Cls)` with no `spellframe` or `binding_name` reads the
door's name/class registry with the same guards and arms; the
door mints that entry after one successful meld of the shape, so
a hit and the door agree. An unhashable `spell`, an instance or a
callable as `spell` are misses and continue in the door.
- Propagates runtime failures from the meld pipeline unchanged.
- A space released to its pool raises SpellSpaceScopeError before any
lookup; a permanently cleaned one raises RuntimeError.
Returns:
object: The resolved runtime object returned by the shared meld runtime.
Args:
spell:
Human SpellName string or concrete spell/frame object.
spell_id:
Optional canonical SHA256 machine identity.
override:
Optional positional or keyword override payload.
"""
if self._released:
self._refuse_released()
# Warm lanes (id 2026-09-26, name/class 2026-09-27): the scoped calls `spell_id=...` alone,
# `meld("Name")` and `meld(spell=Cls)` read the spellspace door's success-only entry here
# (ids from `_fast_meld_doors`, names and classes from `_fast_input_doors`), saving the door
# frame, the name/class -> id lookup and keyword marshaling on a hit. The guard ladder and
# both arms mirror `SpellSpaceMeld.meld` and `Conduit.meld` exactly (`Meld._fast_meld_doors`
# lists every reader); only guard reads sit inside the AttributeError try, so an executor's
# own AttributeError is never swallowed. An id miss continues in the door's positional id
# lane; a name/class miss and every other call shape keep the path below.
if spellframe is None and binding_name is None:
meld_door = self._meld
fast_entry = None
if spell is None:
if type(spell_id) is str:
fast_entry = meld_door._fast_meld_doors.get(spell_id)
elif spell_id is None:
try:
fast_entry = meld_door._fast_input_doors.get(spell)
except TypeError:
# Unhashable `spell` (never minted): the door resolves it uncached.
fast_entry = None
if fast_entry is not None:
(
door_spell,
captured_context,
captured_epoch,
existing_object_entry,
) = fast_entry
fast_executor = None
try:
if (
not meld_door._meld_hooks
and door_spell._door_epoch == captured_epoch
and door_spell._creation_context is captured_context
and not meld_door._spellbook._spellbook_validation_required
):
# Slots are read per hit: hydration swaps them in place.
if override is None:
fast_executor = captured_context._no_overrides_instance_executor
elif type(override) is dict and override:
fast_executor = captured_context._overrides_executor
except AttributeError:
# Cleaned spell/context: guard miss; the door decides.
fast_executor = None
if fast_executor is not None:
if override is None:
if existing_object_entry:
# Existing object: the door returns this same bound slot.
instance = door_spell.user_created_object
else:
instance = fast_executor(meld_door)
else:
instance = fast_executor(meld_door, override)[0]
spellbook = meld_door._spellbook
if spellbook._cache_emit_required:
spellbook._emit_cache_file_if_required()
return instance
if spell is None and type(spell_id) is str:
if override is None:
return meld_door.meld(spell_id)
return meld_door.meld(spell_id, spell_override=override)
if spell is not None and spell_id is not None:
raise ValueError("meld accepts either `spell` or `spell_id`, not both.")
internal_spell = spell
internal_spell_name = None
if spell_id is not None:
internal_spell = spell_id
elif isinstance(spell, str):
internal_spell = None
internal_spell_name = spell
return self._meld.meld(
internal_spell,
spell_name=internal_spell_name,
spellframe=spellframe,
binding_name=binding_name,
spell_override=override,
)
[docs]
def purge(
self,
spell: Optional[Union[str, object]] = None,
*,
spell_id: Optional[str] = None,
spellframe: Optional[Union[str, object]] = None,
binding_name: Optional[str] = None,
purge_all: bool = True,
) -> int:
"""
Purge a registered target's retained creations from this SpellSpace.
Purpose:
Expose request-local retirement through the owned SpellSpaceMeld
door while keeping this explicit scope usable.
Contract:
- SpellSpaceMeld authorizes only many and unique_per_spell_space.
- The target store is always this space's own Creations. No conduit,
Spell owner, lineage-root or cluster team store can be selected.
- Identity normalization follows meld's name/type/frame/id rules.
- Purge does not exit the scope, pop its stack or return it to a pool.
Args:
spell:
Logical name, class/function reference or application instance.
An instance is inspected for its class before ordinary lookup.
The original reference is retained for single-object removal.
spell_id:
Explicit machine identity, mutually exclusive with spell.
spellframe:
Optional frame/type used by ordinary binding discovery.
binding_name:
Optional binding name within that frame.
purge_all:
True removes every retained local entry for the target. False
requires an instance as spell and removes only that object.
Returns:
int:
Removed local creation count; zero when none are retained.
Raises:
ValueError:
If selectors conflict, no target is supplied, or single-object
removal is requested without an instance.
KeyError:
If normal meld discovery cannot find the binding.
RuntimeError:
If permanently cleaned or the target has a broader lifetime.
SpellSpaceScopeError:
If this space was released to its pool.
TypeError:
If purge_all is not a bool.
ExceptionGroup:
Disposal failures after selected local entries have been removed.
Threading / Lifecycle:
Creations uses this space's store lock and releases it before
disposal. Direct manual use needs no active managed stack entry.
Managed thread confinement and pool ownership rules remain unchanged;
a returned/recycled space refuses purge until it is acquired again.
"""
if self._released:
self._refuse_released()
if spell is not None and spell_id is not None:
raise ValueError("purge accepts either `spell` or `spell_id`, not both.")
internal_spell = spell
internal_spell_name = None
if spell_id is not None:
internal_spell = spell_id
elif isinstance(spell, str):
internal_spell = None
internal_spell_name = spell
return self._meld.purge(
internal_spell,
spell_name=internal_spell_name,
spellframe=spellframe,
binding_name=binding_name,
purge_all=purge_all,
)
# Machine identity remains explicit at the public boundary via `spell_id=`;
# this scope forwards it positionally into the internal ID fast lane.