def scoped(
config: ScopeConfig | None = None,
*,
catalogue: ProgramCatalogue | None = None,
) -> _ScopedContext
Create a scoped context manager for narrowed execution.
Parameters
config
- Scope configuration describing allowlist and hook updates. Mutually exclusive with
catalogue.
catalogue
- Catalogue whose allowlist establishes the scope and whose program entries
sh.make resolves inside it. Mutually exclusive with config.
Returns
- _ScopedContext
- A context manager that narrows the current context.
Raises
TypeError
- If neither, or both,
config and catalogue are supplied.
class ScopeConfig(
allowlist: frozenset[Program] | None = None,
before_hooks: tuple[BeforeHook, ...] = (),
after_hooks: tuple[AfterHook, ...] = (),
observe_hooks: tuple[ExecHook, ...] = (),
timeout: float | None = None,
env_overlay: EnvOverlay | None = None,
env_mode: EnvMode = EnvMode.OVERLAY,
catalogue: ProgramCatalogue | None = None,
executable_bindings: ExecutableBindingOverlay | None = None,
)
Bases: _ExecutableBindingSource
Configuration object for scoped execution context updates.
Attributes
allowlist
- Optional allowlist for the scope. When
None, inherit the current allowlist.
before_hooks
- Hooks invoked before command execution (FIFO order).
after_hooks
- Hooks invoked after command execution (LIFO order).
observe_hooks
- Hooks invoked for structured execution events.
timeout
- Optional default timeout in seconds for calls within the scope.
env_overlay
- Optional immutable environment overlay layered over the live
os.environ at subprocess spawn time. When None, no overlay is applied within the scope.
env_mode
- Policy used to render the composed environment for child processes.
catalogue
- Catalogue this scope activates for command construction. When
None, the active catalogue is inherited. A named catalogue replaces the inherited one outright; see _resolve_narrowed_catalogue.
executable_bindings
- Optional immutable mapping from a logical program to the executable it should run within the scope. When
None, the bindings already in effect are inherited unchanged. Entries here win per program over the inherited layer, and never widen the allowlist.
10 members
field ScopeConfig.allowlist
allowlist: frozenset[Program] | None
field ScopeConfig.before_hooks
before_hooks: tuple[BeforeHook, ...]
field ScopeConfig.after_hooks
after_hooks: tuple[AfterHook, ...]
field ScopeConfig.observe_hooks
observe_hooks: tuple[ExecHook, ...]
field ScopeConfig.timeout
field ScopeConfig.env_overlay
env_overlay: EnvOverlay | None
field ScopeConfig.env_mode
field ScopeConfig.catalogue
catalogue: ProgramCatalogue | None
field ScopeConfig.executable_bindings
executable_bindings: ExecutableBindingOverlay | None
method ScopeConfig.executable_binding
def executable_binding(program: Program) -> ExecutableBinding | None
Return the binding for *program*, or None when it is unbound.
Parameters
program Program
- The logical program whose binding is wanted.
Returns
- ExecutableBinding | None
- The effective binding, or
None when the program runs under the name it was catalogued with.
class CuprumContext(
allowlist: frozenset[Program] = dc.field(default_factory=frozenset),
before_hooks: tuple[BeforeHook, ...] = (),
after_hooks: tuple[AfterHook, ...] = (),
observe_hooks: tuple[ExecHook, ...] = (),
timeout: float | None = None,
env_overlay: EnvOverlay | None = None,
_allowlist_is_restricted: bool = False,
env_mode: EnvMode = EnvMode.OVERLAY,
catalogue: ProgramCatalogue | None = dc.field(default=None, compare=False),
executable_bindings: ExecutableBindingOverlay | None = None,
)
Bases: _HookPolicy, _ExecutableBindingPolicy
Immutable execution context holding allowlist and hooks.
Attributes
allowlist
- Frozenset of programs permitted in this context.
before_hooks
- Tuple of hooks invoked before command execution (FIFO order).
after_hooks
- Tuple of hooks invoked after command execution (LIFO order).
observe_hooks
- Tuple of hooks invoked for structured execution events (FIFO order).
timeout
- Optional default timeout in seconds applied when a call does not supply an explicit timeout.
env_overlay
- Optional immutable environment overlay layered over the live
os.environ when command environments are resolved. When None, no overlay is active on this context.
_allowlist_is_restricted
- Internal marker distinguishing the permissive empty default allowlist from an empty allowlist produced by narrowing a restricted scope. It keeps its position ahead of
catalogue so the positional argument order that predates the catalogue field is unchanged.
env_mode
- Policy used to render the composed environment for child processes.
catalogue
- Catalogue the innermost active scope activated, or
None outside any catalogue scope. sh.make consults it to resolve builders that do not name a catalogue explicitly. Excluded from equality because it carries no permission semantics: two contexts that allow the same programs are equivalent for enforcement regardless of which catalogue supplied them.
executable_bindings
- Optional immutable mapping from a logical program to the executable it should run. When
None, every permitted program runs under the name it was catalogued with. A binding supplies the executable for an already-permitted program and never widens the allowlist.
25 members
field CuprumContext.allowlist
allowlist: frozenset[Program]
field CuprumContext.before_hooks
before_hooks: tuple[BeforeHook, ...]
field CuprumContext.after_hooks
after_hooks: tuple[AfterHook, ...]
field CuprumContext.observe_hooks
observe_hooks: tuple[ExecHook, ...]
field CuprumContext.timeout
field CuprumContext.env_overlay
env_overlay: EnvOverlay | None
field CuprumContext.env_mode
field CuprumContext.catalogue
catalogue: ProgramCatalogue | None
field CuprumContext.executable_bindings
executable_bindings: ExecutableBindingOverlay | None
method CuprumContext.is_allowed
def is_allowed(program: Program) -> bool
Return True when the program is in the allowlist.
This method only checks membership and returns False for empty allowlists. Use check_allowed() for enforcement; it applies the two-mode empty-allowlist policy, permitting empty unrestricted contexts and denying empty restricted contexts.
Parameters
program Program
- The program to check against the current allowlist.
Returns
- bool
True when the program is a member of the allowlist.
method CuprumContext.check_allowed
def check_allowed(program: Program) -> None
Raise ForbiddenProgramError if program is not allowed.
When the allowlist is empty and unrestricted, all programs are permitted (permissive default). When the allowlist is empty and restricted, all programs are denied.
A warning log with operation and restricted_state fields is emitted before raising ForbiddenProgramError.
Raises
ForbiddenProgramError
- If the program is not permitted by the context allowlist.
method CuprumContext.narrow
def narrow(config: ScopeConfig) -> CuprumContext
Create a derived context with narrowed allowlist and extended hooks.
Parameters
config
- Scope configuration describing allowlist and hook updates.
Returns
- CuprumContext
- A new context with narrowed permissions and extended hooks.
Notes
When the parent has an empty *unrestricted* allowlist, the provided allowlist is used directly to establish a base scope. When the parent is restricted and empty, narrowing keeps it empty. When the parent has programs, the new allowlist is intersected to enforce narrowing (can only remove, not add programs).
method CuprumContext.with_allowlist
def with_allowlist(allowlist: frozenset[Program]) -> CuprumContext
Return a context with the given allowlist replacing the current one.
Unlike narrow(), this sets the allowlist directly without intersection. Use with care; prefer narrow() for enforcing safety invariants.
Parameters
allowlist frozenset[Program]
- The allowlist to install in place of the current one.
Returns
- CuprumContext
- A new context with the supplied allowlist.
method CuprumContext.with_program
def with_program(program: Program) -> CuprumContext
Return a context with the program added to the allowlist.
Parameters
program Program
- The program to add to the allowlist.
Returns
- CuprumContext
- A new context whose allowlist includes the program.
method CuprumContext.without_program
def without_program(program: Program) -> CuprumContext
Return a context with the program removed from the allowlist.
Parameters
program Program
- The program to remove from the allowlist.
Returns
- CuprumContext
- A new context whose allowlist excludes the program.
method CuprumContext.with_env_overlay
def with_env_overlay(
overlay: EnvOverlay | None,
mode: EnvMode = EnvMode.OVERLAY,
) -> CuprumContext
Return a context whose env overlay is layered with overlay.
Values in overlay win over earlier overlay entries. A replacement policy discards earlier layers, while every other policy reads the live os.environ at subprocess spawn time. In non-replacement modes, passing None leaves the effective overlay unchanged; in replacement mode, it creates an empty replacement boundary.
Parameters
overlay collections.abc.Mapping[str, str | UnsetType] | None
- Environment variables to layer over the current overlay.
None leaves the current overlay unchanged.
mode EnvMode
- Policy contributed by this child layer.
Returns
- CuprumContext
- A new context with the merged environment overlay.
method CuprumContext.with_before_hook
def with_before_hook(hook: BeforeHook) -> typ.Self
Return a context with an additional before hook.
Parameters
hook BeforeHook
- The before-execution hook to append.
Returns
- CuprumContext
- A new context with the before hook appended.
method CuprumContext.without_before_hook
def without_before_hook(hook: BeforeHook) -> typ.Self
Return a context with the specified before hook removed.
Parameters
hook BeforeHook
- The before-execution hook to remove.
Returns
- CuprumContext
- A new context without the given before hook.
method CuprumContext.with_after_hook
def with_after_hook(hook: AfterHook) -> typ.Self
Return a context with an additional after hook (prepended for LIFO).
Parameters
hook AfterHook
- The after-execution hook to prepend.
Returns
- CuprumContext
- A new context with the after hook prepended.
method CuprumContext.without_after_hook
def without_after_hook(hook: AfterHook) -> typ.Self
Return a context with the specified after hook removed.
Parameters
hook AfterHook
- The after-execution hook to remove.
Returns
- CuprumContext
- A new context without the given after hook.
method CuprumContext.with_observe_hook
def with_observe_hook(hook: ExecHook) -> typ.Self
Return a context with an additional observe hook.
Parameters
hook ExecHook
- The structured-event observe hook to append.
Returns
- CuprumContext
- A new context with the observe hook appended.
method CuprumContext.without_observe_hook
def without_observe_hook(hook: ExecHook) -> typ.Self
Return a context with the specified observe hook removed.
Parameters
hook ExecHook
- The structured-event observe hook to remove.
Returns
- CuprumContext
- A new context without the given observe hook.
method CuprumContext.with_executable_binding
def with_executable_binding(
program: Program,
binding: ExecutableBinding,
) -> typ.Self
Return a context whose binding for *program* is *binding*.
Rebinding a program replaces the executable for the derived context and its descendants; the receiver is unchanged.
Parameters
program Program
- The logical program to bind.
binding ExecutableBinding
- The executable the program should run.
Returns
- CuprumContext
- A new context carrying the binding.
method CuprumContext.resolve_executable
def resolve_executable(
program: Program,
*,
cwd: str | None,
) -> str | None
Return the executable to run for *program*, or None.
Resolution is deliberately independent of allowlist enforcement: the allowlist decides whether a program may run, and this method decides only what a permitted program runs. Keeping the two apart means an unpermitted program cannot become runnable by acquiring a binding, and a refusal still names the logical program rather than a path.
Parameters
program Program
- The logical program to resolve.
cwd str | None
- The working directory the execution will run in. A relative bound path is anchored at it;
None leaves the platform to resolve the path's own spelling.
Returns
- str | None
- The executable string, or
None when the program is unbound and should run under its catalogued name.
Raises
ExecutableResolutionError
- A bound program's resolver failed. The error names *program* and chains the resolver's own exception as
__cause__.
method CuprumContext.executable_binding
def executable_binding(program: Program) -> ExecutableBinding | None
Return the binding for *program*, or None when it is unbound.
Parameters
program Program
- The logical program whose binding is wanted.
Returns
- ExecutableBinding | None
- The effective binding, or
None when the program runs under the name it was catalogued with.
def current_context() -> CuprumContext
Return the current execution context.
Returns
- CuprumContext
- The context bound to the current execution scope.
get_context = current_context
def allow(*programs: Program) -> AllowRegistration
Extend the current context's allowlist with additional programs.
Parameters
programs
- Programs to add to the allowlist.
Returns
- AllowRegistration
- A handle that can be detached or used as a context manager.
class AllowRegistration(*programs: Program) -> None
Bases: _TokenRegistration
Registration handle for dynamic allowlist extension.
Supports detach() and context-manager usage for scoped allowing. The token-restoration discipline is documented on _TokenRegistration.
1 member
method AllowRegistration.detach
Release the registration, exactly once.
def env(
*overlays: cabc.Mapping[str, EnvOverlayValue],
**kwvars: EnvOverlayValue | EnvMode,
) -> EnvRegistration
Overlay environment variables on top of the live os.environ.
Mirrors dict in how arguments are combined: positional mappings are merged left-to-right and any keyword arguments win over them. For effective modes other than REPLACE, values are not snapshot against os.environ — the live process environment is read at subprocess spawn time so that variables set after Cuprum is imported (for example by monkeypatch.setenv) remain visible. A REPLACE mode renders from an empty environment instead, so the live process environment stays out of the child.
Parameters
overlays
- Zero or more
Mapping[str, str | UnsetType] instances supplying overlay entries. Useful when the variable name is not a valid Python identifier.
mode
- When an
EnvMode, the policy contributed by this scope. OVERLAY remains the default; another value creates the environment variable named mode for compatibility.
kwvars
- Keyword pairs naming environment variables or
UNSET markers. Identifier-safe variable names are typically expressed this way.
Returns
- EnvRegistration
- A handle that can be detached or used as a context manager.
Notes
env is bound to the contextvars.Context in which it is created. Detach it in that same logical context (thread or task) to avoid ValueError from contextvars.ContextVar.reset.
class EnvRegistration(
overlay: EnvOverlay,
mode: EnvMode = EnvMode.OVERLAY,
) -> None
Bases: _TokenRegistration
Registration handle for a scoped environment overlay.
The overlay is layered on top of any overlay already present in the current context; nested registrations therefore behave as a stack. The token-restoration discipline is documented on _TokenRegistration.
The overlay itself is overlay-only. For effective modes other than REPLACE, the live os.environ is read at subprocess spawn time, when render_env renders the composed policy, so any updates to the process environment after the registration is created — for example via pytest's monkeypatch.setenv — remain visible to subprocesses spawned inside the scope. This is the behaviour the issue requires. A REPLACE mode is the exception: it renders from an empty environment, so the live process environment stays out of the child entirely.
2 members
property EnvRegistration.overlay
overlay: EnvOverlay | None
The immutable overlay this registration applied.
method EnvRegistration.detach
Release the registration, exactly once.
def merge_env_overlays(
parent: EnvOverlay | None,
child: EnvOverlay | None,
) -> EnvOverlay | None
Layer child over parent; None means *inherit unchanged*.
Both layers are kept overlay-only — they never include a snapshot of os.environ. The live process environment is read at spawn time so that callers can monkey-patch or otherwise mutate os.environ after Cuprum has been imported and still have those updates visible to subprocesses spawned inside the scope.
Exposed as public API so that other cuprum modules — and downstream code that builds custom observation tags — can merge overlay layers without reaching for a private symbol.
Parameters
parent collections.abc.Mapping[str, str | UnsetType] | None
- The base overlay layer.
None means *inherit unchanged* — the layer contributes nothing.
child collections.abc.Mapping[str, str | UnsetType] | None
- The overlay layered on top of
parent; its values win on key collisions. None means *inherit unchanged*.
Returns
- collections.abc.Mapping[str, str | UnsetType] | None
- An immutable snapshot of the merged overlay, or
None when both layers are None (meaning *inherit the environment unchanged*).
def resolve_env(*layers: EnvOverlay | None) -> dict[str, str] | None
Merge os.environ (read live) with the supplied overlay layers.
Layers are applied left-to-right; later values win. None *and empty* layers are skipped — an empty overlay contributes nothing, so treating it as a no-op avoids an unnecessary copy of os.environ and lets the subprocess inherit the parent environment directly. When every layer is skipped the function returns None so callers may pass it through to subprocess APIs to mean *inherit the parent environment unchanged*.
The call to os.environ.copy is deferred until at least one overlay is non-empty, so the result reflects whatever the process environment looks like at the moment of resolution — the exact behaviour the issue requires.
Parameters
*layers collections.abc.Mapping[str, str | UnsetType] | None
- Overlay layers applied left-to-right over a live copy of
os.environ; later values win. None and empty layers are skipped.
Returns
- dict[str, str] | None
- The merged environment mapping, or
None when every layer is None or empty (meaning *inherit the parent environment unchanged*).
from cuprum import context
Execution context with scoped allowlists and hooks.
CuprumContext provides a ContextVar-backed execution context that scopes allowlists and hooks for command execution. Contexts support narrowing (restricting the allowlist) and hook registration with deterministic ordering.
Example: >>> from cuprum.context import ScopeConfig, scoped, before, current_context >>> from cuprum.catalogue import ECHO >>> def log_hook(cmd): ... print(f"Running: {cmd}") >>> with scoped(ScopeConfig(allowlist=frozenset([ECHO]))): ... with before(log_hook): ... ctx = current_context() ... ctx.is_allowed(ECHO) True
Scoped environment overlays (issue #100): four public symbols layer environment variables on top of the live os.environ for subprocesses spawned inside the scope. env(*overlays, **kwvars) is a factory that returns an EnvRegistration applying an immutable overlay to the active context, resolved live at subprocess spawn time against os.environ. EnvRegistration is the handle returned by env(); it works as a context manager and exposes detach for explicit teardown. merge_env_overlays(parent, child) is the overlay-only merge that returns an immutable MappingProxyType without reading os.environ — it is the helper used to record the effective overlay in observation events. resolve_env(*layers) is an overlay-only helper that merges os.environ with one or more overlay layers and returns a plain dict or None when no layers contribute; it is not on the subprocess spawn path. Spawning composes through _resolve_env_policy and renders through render_env, which is what consults the effective EnvMode. Precedence at spawn time, from lowest to highest, is: live os.environ < scoped env() overlays (innermost wins) < per-call ExecutionContext.env. This deliberately diverges from plumbum's local.env, which snapshots os.environ at module import time and therefore misses variables added to the process environment after import. The CuprumContext and ScopeConfig dataclasses each carry an env_overlay field (Mapping[str, str] | None) that is coerced to an immutable proxy on construction.
>>> from cuprum.context import env, current_context >>> with env(MY_VAR="hello"): ... current_context().env_overlay["MY_VAR"] 'hello'
Scoped executable bindings (issue #440): bind_executable(program, path_or_resolver, *, allow_relative=False) binds a logical catalogued program to the executable it should run inside the scope, and returns an ExecutableBindingRegistration handle that detaches or acts as a context manager. The CuprumContext and ScopeConfig dataclasses each carry an executable_bindings field coerced to an immutable proxy on construction; merge_executable_bindings(parent, child) records the effective layer without resolving it. Bindings are a second, narrower authority than the allowlist: the allowlist decides which logical programs may run at all, and a binding decides only which executable a permitted program runs. Resolution therefore happens strictly after allowlist enforcement, and CuprumContext.resolve_executable never consults is_allowed or check_allowed.
>>> from cuprum.context import bind_executable, current_context >>> from cuprum.catalogue import ECHO >>> with bind_executable(ECHO, "/opt/tools/echo"): ... current_context().resolve_executable(ECHO, cwd=None) '/opt/tools/echo'
4 exports
constant cuprum.context.UNSET
exception cuprum.context.ContextError
Base class for execution-context domain errors.
class cuprum.context.EnvMode
Choose how a child environment relates to its parent environment.
OVERLAY is the default and layers supplied values over the live parent environment. INHERIT preserves an inherited policy without adding a replacement boundary. REPLACE starts the child with an empty environment before applying its supplied values.
class cuprum.context.UnsetType
Singleton marker requesting removal of an environment variable.
def bind_executable(
program: Program,
path_or_resolver: str | os.PathLike[str] | ExecutableResolver,
*,
allow_relative: bool = False,
) -> ExecutableBindingRegistration
Bind a logical program to an executable for the enclosing scope.
The binding applies only inside the scope; leaving it restores the bindings that were in effect before. A nested registration for the same program overrides the outer one for the inner scope alone, and bindings for distinct programs compose.
Parameters
program
- The logical program to bind.
path_or_resolver
- The executable path, or a zero-argument callable returning one. A resolver is evaluated once per execution, at spawn time.
allow_relative
- When True, a relative path is permitted. It is anchored at the execution's working directory when the execution supplies one; an execution that names no directory leaves the platform to resolve it — against
PATH for a bare name, and otherwise relative to the process's own working directory. Ignored for a resolver.
Returns
- ExecutableBindingRegistration
- A handle that can be detached or used as a context manager.
Raises
InvalidExecutableBindingError
- A supplied path failed validation. The error names the logical program, quotes the path, and carries the classified reason.
class ExecutableBindingRegistration(
program: Program,
binding: ExecutableBinding,
) -> None
Bases: _TokenRegistration
Registration handle for a scoped executable binding.
The binding is layered onto any bindings already present in the current context, so nested registrations behave as a stack: the innermost binding for a program is the one an execution resolves. The token-restoration discipline is documented on _TokenRegistration.
A binding supplies the executable a permitted program runs. It is not a permission: the allowlist continues to decide which logical programs may run at all, and resolution happens only after that decision has been made.
3 members
property ExecutableBindingRegistration.program
The logical program this registration bound.
property ExecutableBindingRegistration.binding
binding: ExecutableBinding
The binding this registration applied.
method ExecutableBindingRegistration.detach
Release the registration, exactly once.
def merge_executable_bindings(
parent: ExecutableBindingOverlay | None,
child: ExecutableBindingOverlay | None,
) -> ExecutableBindingOverlay | None
Layer child over parent; None means *inherit unchanged*.
The child layer wins key by key: an inner scope can rebind a program the outer scope already bound, and can add bindings the outer scope never mentioned. Composition never copies a binding — entries move between layers by identity, so a resolver stays the same callable wherever it is read from.
Parameters
parent collections.abc.Mapping[Program, ExecutableBinding] | None
- The base layer.
None means *inherit unchanged* — the layer contributes nothing.
child collections.abc.Mapping[Program, ExecutableBinding] | None
- The layer applied over
parent; its bindings win on key collisions. None means *inherit unchanged*.
Returns
- collections.abc.Mapping[Program, ExecutableBinding] | None
- An immutable snapshot of the composed layer, or
None when both layers are None, meaning *inherit the effective bindings unchanged*.
from cuprum import executable_binding
Typed bindings from a logical program identity to an executable path.
A Program names an executable for catalogue and policy purposes, and the same value is handed to the operating system as argv[0]. Those two jobs usually coincide, but not always: a tool may live at a version-pinned absolute path, inside a virtual environment, or be replaced by a controlled stub during tests. This module carries the second job as its own value so the first can stay a bounded logical label.
The path vocabulary this module builds on — ExecutablePath, PathBindingRejection, executable_path, and advisory_path_rejection — lives in executable_paths and is re-exported here, so callers reach the whole feature through one import.
Both modules are pure: neither reads or writes CuprumContext, and neither imports the context package at runtime. That keeps the dependency direction acyclic, so the context package and the execution layer can both depend on them.
Resolution and its limits -------------------------
resolve_binding turns a binding plus a working directory into the string to execute. It is pure and total for every binding the constructors accept.
Checking a path and then executing it is not atomic. Between the advisory check and the child's exec, another process with write access to the directory can replace, rename, or re-link the file, and the child will run whatever is there at that moment. Cuprum cannot close that window from inside the process: it would need an O_PATH descriptor plus fexecve, which is not available portably or from asyncio. Operators who need the guarantee that the checked path is the executed binary must supply it from the filesystem, by owning the directory, restricting write permission to a trusted account, and deploying the tree read-only.
11 exports
class cuprum.executable_binding.ExecutableBinding
class ExecutableBinding(
path: ExecutablePath | None = None,
resolver: ExecutableResolver | None = None,
)
The executable a logical program should run.
Exactly one of path and resolver must be set. A path binding is a fixed, already-validated string; a resolver binding is evaluated once per execution, so it can consult state that is only available at spawn time.
Construct through executable_binding, which applies validation and reports the logical program in its errors; constructing directly is supported for callers that already hold a validated ExecutablePath or a trusted resolver.
Parameters
path ExecutablePath | None, optional
- The fixed executable path.
resolver ExecutableResolver | None, optional
- The callable producing the executable at spawn time.
Raises
ValueError
- Neither or both of
path and resolver were supplied.
new type cuprum.executable_binding.ExecutablePath
ExecutablePath = typ.NewType('ExecutablePath', str)
A validated string naming an executable to run.
The string is not necessarily absolute; whether an absolute path is required is decided by executable_path's allow_relative flag, not by the type.
exception cuprum.executable_binding.ExecutableResolutionError
class ExecutableResolutionError(
program: Program,
cause: BaseException,
) -> None
Raised when a bound program's resolver fails to produce an executable.
A resolver is caller-supplied and typically consults state that may be absent — a virtual environment that was never created, a toolchain that is not installed. Each of those is a recognizable failure of the *binding*, so it is reported as one rather than escaping as whichever exception the resolver happened to raise: an escaping FileNotFoundError names a path the caller never configured, and gives no hint that a binding was involved. The original exception is chained as __cause__.
Parameters
program Program
- The logical program whose resolver failed.
cause BaseException
- The exception the resolver raised.
Attributes
program Program
- The logical program whose resolver failed.
cause BaseException
- The exception the resolver raised.
type alias cuprum.executable_binding.ExecutableResolver
ExecutableResolver = cabc.Callable[[], str]
A zero-argument callable producing the executable to run.
Evaluated once per execution, at spawn time. It exists for callers whose executable is only knowable late: a virtual environment concretized during the run, a toolchain selected from configuration, or a test double chosen by the test's own fixtures. Its result is used verbatim, so a resolver that wants its result anchored must return an absolute path itself.
exception cuprum.executable_binding.InvalidExecutableBindingError
class InvalidExecutableBindingError(
program: Program | None,
path: str,
reason: PathBindingRejection,
) -> None
Raised when an executable path or a binding for it cannot be built.
The error lives beside the rejection vocabulary rather than beside the binding type, because both executable_path here and executable_binding in executable_binding raise it, and the binding module already depends on this one.
Parameters
program Program | None
- The logical identity the binding was being built for, or
None when the path was validated on its own.
path str
- The path the caller supplied, quoted verbatim.
reason PathBindingRejection
- The classified reason for the rejection.
Attributes
program Program | None
- The logical identity the binding was being built for, or
None.
path str
- The rejected path, quoted verbatim.
reason PathBindingRejection
- The classified reason for the rejection.
class cuprum.executable_binding.PathBindingRejection
Reason a raw string fails executable-path validation or its advisory check.
Each member's value is the exact error message raised for that category, so callers can reason about the rejection category rather than only pass/fail. Members are declared in the order the checks are applied; the classifiers return the first match.
function cuprum.executable_binding.advisory_path_rejection
def advisory_path_rejection(
raw_value: str,
) -> PathBindingRejection | None
Report whether *raw_value* currently looks like a runnable executable.
This is an advisory probe, not a guarantee. It answers "at the instant it was called, did this path exist as an executable file?" and says nothing about what will be executed later; see the module docstring of executable_binding for the limits of that claim.
A bare name with no directory component is left unchecked, because the platform resolves it against PATH and Cuprum does not replicate that search. Only absolute paths and relative paths containing a separator are probed. The check is skipped entirely on Windows, where the execute bit is not part of the file's identity.
Parameters
raw_value str
- The path to probe.
Returns
- PathBindingRejection | None
NOT_FOUND when the path is absent, NOT_EXECUTABLE when it exists as something other than an executable file, or None when nothing was found to report.
function cuprum.executable_binding.classify_executable_path
def classify_executable_path(
raw_value: str,
*,
allow_relative: bool,
) -> PathBindingRejection | None
Classify why a raw executable-path string is rejected, or None.
Mirrors classify_path_string: the checks run in declaration order, and the first match wins, so a NUL-bearing traversal is reported as a NUL rather than a traversal.
Parameters
raw_value str
- Path string to classify.
allow_relative bool
- When True, relative paths are permitted.
Returns
- PathBindingRejection | None
- The rejection category, or
None when the value is acceptable.
function cuprum.executable_binding.executable_binding
def executable_binding(
program: Program,
path_or_resolver: str | os.PathLike[str] | ExecutableResolver,
*,
allow_relative: bool = False,
) -> ExecutableBinding
Bind a logical *program* to an executable path or resolver.
A callable is stored as a resolver and used verbatim; a string or any other path-like value is validated through executable_path and stored as a normalized path.
Parameters
program Program
- The logical identity the binding is for. It is used only for error reporting; the binding does not itself restrict which program may use it.
path_or_resolver str | os.PathLike[str] | ExecutableResolver
- The executable path, or a zero-argument callable returning one. A path is validated and stored as a
str; a callable is stored unchanged and evaluated at spawn time.
allow_relative bool, optional
- When True, a relative path is permitted. Ignored for a resolver, whose result Cuprum does not inspect. Defaults to False.
Returns
- ExecutableBinding
- The binding.
Raises
InvalidExecutableBindingError
- A supplied path failed validation. The error names the program, quotes the path, and carries the classified reason.
function cuprum.executable_binding.executable_path
def executable_path(
value: str | os.PathLike[str],
*,
allow_relative: bool = False,
) -> ExecutablePath
Validate and normalize a string into an ExecutablePath.
Accepts any os.PathLike — a pathlib.Path or a bare pathlib.PurePosixPath alike — for symmetry with safe_path, but unlike safe_path it does not require an absolute path unless allow_relative is left at its default. Validation is purely syntactic; the filesystem is not consulted, so a path may be validated and bound before the file it names exists.
Parameters
value str | os.PathLike[str]
- The path to validate.
allow_relative bool, optional
- When True, relative paths are permitted. Defaults to False, which requires an absolute path.
Returns
- ExecutablePath
- The normalized path.
Raises
InvalidExecutableBindingError
- The path is empty, contains a NUL byte or a
.. segment, or is relative while allow_relative is False. The error carries the classified reason and a program of None, because no logical identity was supplied.
function cuprum.executable_binding.resolve_binding
def resolve_binding(
binding: ExecutableBinding,
*,
cwd: str | None,
) -> str
Evaluate *binding* into the executable string to run.
A resolver is called once and its result is used verbatim. A path binding is returned as-is when it is already absolute; a relative path binding is anchored at cwd when one is supplied, and returned unchanged otherwise, leaving the platform to resolve it — against PATH for a bare name, or relative to the process's own working directory otherwise.
Parameters
binding ExecutableBinding
- The binding to evaluate.
cwd str | None
- The working directory the execution will run in, or
None.
Returns
- str
- The executable string.
def before(hook: BeforeHook) -> HookRegistration
Register a before-execution hook in the current context.
Parameters
hook
- Callable invoked with the SafeCmd before execution.
Returns
- HookRegistration
- A handle that can be detached or used as a context manager.
def after(hook: AfterHook) -> HookRegistration
Register an after-execution hook in the current context.
Parameters
hook
- Callable invoked with the SafeCmd and CommandResult after execution.
Returns
- HookRegistration
- A handle that can be detached or used as a context manager.
BeforeHook = cabc.Callable[[SafeCmd], None]
AfterHook = cabc.Callable[[SafeCmd, _AnyCommandResult], None]
class HookRegistration(
hook: BeforeHook | AfterHook | ExecHook,
hook_type: typ.Literal['before', 'after', 'observe'],
) -> None
Bases: _TokenRegistration
Registration handle for hooks with detach and context-manager support.
The token-restoration discipline is documented on _TokenRegistration.
Prefer the before, after, and observe factories: each pairs one hook shape with its slot, so a mismatched hook is rejected at the call site. Constructing the handle directly pairs the hook and the tag by hand, and only the tag itself is validated — at runtime.
1 member
method HookRegistration.detach
Release the registration, exactly once.