Skip to content

API

Runtime support library for projects built with pyrig.

core

Foundational utilities shared across the project.

constants

Constants used throughout the project.

dependencies

Installed package dependency graph and the cross-package discovery it enables.

discovery

Subclass and module discovery scoped across installed package dependents.

dependency_graph cached
dependency_graph() -> DependencyGraph

Return the dependency graph of pyrig_runtime and its dependents.

Built once and cached. Pruned to pyrig_runtime and every package that depends on it, directly or transitively; packages it depends on are not included.

Returns:

Type Description
DependencyGraph

Directed graph rooted at pyrig_runtime, containing only its

DependencyGraph

ancestors.

Note

The returned instance is shared across all callers. Do not mutate it.

Source code in src/pyrig_runtime/core/dependencies/discovery.py
@cache
def dependency_graph() -> DependencyGraph:
    """Return the dependency graph of `pyrig_runtime` and its dependents.

    Built once and cached. Pruned to `pyrig_runtime` and every package that
    depends on it, directly or transitively; packages it depends on are not
    included.

    Returns:
        Directed graph rooted at `pyrig_runtime`, containing only its
        ancestors.

    Note:
        The returned instance is shared across all callers. Do not mutate it.
    """
    graph = DependencyGraph()
    graph.prune(root=pyrig_runtime.__name__)
    return graph
dependent_packages cached
dependent_packages(
    package: ModuleType,
) -> tuple[ModuleType, ...]

Return every installed package that depends on package.

The result is cached per unique package argument.

Parameters:

Name Type Description Default
package ModuleType

Package whose dependents should be discovered.

required

Returns:

Type Description
ModuleType

Tuple of imported module objects for every package that depends on

...

package, directly or transitively, in dependency order

tuple[ModuleType, ...]

(dependencies before dependents). Does not include package

tuple[ModuleType, ...]

itself.

Raises:

Type Description
KeyError

If package is not pyrig_runtime or one of its dependents.

Source code in src/pyrig_runtime/core/dependencies/discovery.py
@cache
def dependent_packages(package: ModuleType) -> tuple[ModuleType, ...]:
    """Return every installed package that depends on `package`.

    The result is cached per unique `package` argument.

    Args:
        package: Package whose dependents should be discovered.

    Returns:
        Tuple of imported module objects for every package that depends on
        `package`, directly or transitively, in dependency order
        (dependencies before dependents). Does not include `package`
        itself.

    Raises:
        KeyError: If `package` is not `pyrig_runtime` or one of its
            dependents.
    """
    graph = dependency_graph()
    return tuple(import_modules(graph.sorted_ancestors(package.__name__)))
equivalent_modules_across_dependencies
equivalent_modules_across_dependencies(
    module: ModuleType,
) -> Iterator[ModuleType]

Yield the equivalent module from every dependent of module's root package.

For each installed package that depends on the root of module, locates the module at the same sub-path within that dependent and yields it if the import succeeds. The root package itself is excluded from results.

Parameters:

Name Type Description Default
module ModuleType

Module whose root determines which dependents to search and whose sub-path within that root locates the corresponding module in each dependent.

required

Yields:

Type Description
ModuleType

Successfully imported module objects, in dependency order.

ModuleType

Dependents are silently skipped whenever importing the equivalent

ModuleType

module fails, whether because no module exists at that sub-path

ModuleType

or because the import itself raises.

Source code in src/pyrig_runtime/core/dependencies/discovery.py
def equivalent_modules_across_dependencies(
    module: ModuleType,
) -> Iterator[ModuleType]:
    """Yield the equivalent module from every dependent of `module`'s root package.

    For each installed package that depends on the root of `module`,
    locates the module at the same sub-path within that dependent and
    yields it if the import succeeds. The root package itself is excluded
    from results.

    Args:
        module: Module whose root determines which dependents to search
            and whose sub-path within that root locates the corresponding
            module in each dependent.

    Yields:
        Successfully imported module objects, in dependency order.
        Dependents are silently skipped whenever importing the equivalent
        module fails, whether because no module exists at that sub-path
        or because the import itself raises.
    """
    for package in dependent_packages(root_module(module)):
        package_module = replace_root_module(module, package.__name__, default=None)
        if package_module is not None:
            yield package_module
subclasses_across_dependencies
subclasses_across_dependencies[T](
    cls: type[T], module: ModuleType
) -> Iterator[type[T]]

Yield subclasses of cls defined at the same sub-path as module.

The search covers module itself, every sub-module if module is a package, and the equivalently-located module (and its own sub-modules, if a package) in every installed package that depends on module's root package.

Parameters:

Name Type Description Default
cls type[T]

Base class whose subclasses should be discovered.

required
module ModuleType

Module or package that scopes the search, and whose root package determines which dependents are searched.

required

Yields:

Type Description
type[T]

Subclass types of cls found anywhere in the search scope. The

type[T]

order is stable across calls but reflects discovery order, not

type[T]

any deliberate priority.

Note

Every module within the search scope is imported as a side effect, executing any module-level code it contains.

Source code in src/pyrig_runtime/core/dependencies/discovery.py
def subclasses_across_dependencies[T](
    cls: type[T],
    module: ModuleType,
) -> Iterator[type[T]]:
    """Yield subclasses of `cls` defined at the same sub-path as `module`.

    The search covers `module` itself, every sub-module if `module` is a
    package, and the equivalently-located module (and its own sub-modules,
    if a package) in every installed package that depends on `module`'s
    root package.

    Args:
        cls: Base class whose subclasses should be discovered.
        module: Module or package that scopes the search, and whose root
            package determines which dependents are searched.

    Yields:
        Subclass types of `cls` found anywhere in the search scope. The
        order is stable across calls but reflects discovery order, not
        any deliberate priority.

    Note:
        Every module within the search scope is imported as a side effect,
        executing any module-level code it contains.
    """
    for package in filter(
        is_package,
        chain(
            (module,),
            equivalent_modules_across_dependencies(module=module),
        ),
    ):
        register_package_modules(package)

    module_name = module.__name__
    root_name = root_module_name(module_name)
    for subclass in discover_subclasses(cls):
        if replace_root_module_name(
            subclass.__module__,
            root_name,
        ).startswith(module_name):
            yield subclass

distribution

Utilities for parsing metadata text from installed Python distributions.

distribution_header
distribution_header(metadata: str) -> str

Return the header portion of a distribution's metadata.

The header is the metadata content before the first blank line, containing the single-line RFC 822 header fields (e.g. Name, Requires-Dist). If no blank line is found, the entire metadata text is returned.

Parameters:

Name Type Description Default
metadata str

The full metadata of an installed distribution.

required

Returns:

Type Description
str

The header portion of the distribution's metadata.

Source code in src/pyrig_runtime/core/dependencies/distribution.py
def distribution_header(metadata: str) -> str:
    """Return the header portion of a distribution's metadata.

    The header is the metadata content before the first blank line,
    containing the single-line RFC 822 header fields (e.g. `Name`,
    `Requires-Dist`). If no blank line is found, the entire metadata
    text is returned.

    Args:
        metadata: The full metadata of an installed distribution.

    Returns:
        The header portion of the distribution's metadata.
    """
    end = metadata.find("\n\n")
    return metadata[:end] if end != -1 else metadata
distribution_header_value_pattern
distribution_header_value_pattern(
    field_name: str,
) -> Pattern[str]

Compile a regex that matches a single-line metadata header field.

Matches lines of the form field_name: value. Use findall on the result to collect every value when the header repeats.

Parameters:

Name Type Description Default
field_name str

Name of the header field to match, exactly as it appears in the metadata (e.g. Name).

required

Returns:

Type Description
Pattern[str]

A case-sensitive pattern with each match's value captured in the

Pattern[str]

first group.

Source code in src/pyrig_runtime/core/dependencies/distribution.py
def distribution_header_value_pattern(field_name: str) -> re.Pattern[str]:
    """Compile a regex that matches a single-line metadata header field.

    Matches lines of the form `field_name: value`. Use `findall` on the
    result to collect every value when the header repeats.

    Args:
        field_name: Name of the header field to match, exactly as it
            appears in the metadata (e.g. `Name`).

    Returns:
        A case-sensitive pattern with each match's value captured in the
        first group.
    """
    return re.compile(rf"^{field_name}:[ \t]*(.*)$", re.MULTILINE)
distribution_metadata
distribution_metadata(dist: Distribution) -> str | None

Return the full metadata text of a distribution, or None if it has none.

Source code in src/pyrig_runtime/core/dependencies/distribution.py
def distribution_metadata(dist: Distribution) -> str | None:
    """Return the full metadata text of a distribution, or `None` if it has none."""
    return dist.read_text("METADATA")
distribution_name
distribution_name(metadata: str) -> str

Return the name of a distribution from its metadata.

Parameters:

Name Type Description Default
metadata str

The full metadata of an installed distribution.

required

Returns:

Type Description
str

The name of the distribution.

Source code in src/pyrig_runtime/core/dependencies/distribution.py
def distribution_name(metadata: str) -> str:
    """Return the name of a distribution from its metadata.

    Args:
        metadata: The full metadata of an installed distribution.

    Returns:
        The name of the distribution.
    """
    return regex_find(DISTRIBUTION_NAME_PATTERN, metadata)
distribution_requirement_as_module_name
distribution_requirement_as_module_name(req: str) -> str

Extract the importable module name from a dependency requirement string.

Version specifiers, extras, and anything else after the package name are discarded. Hyphens are normalized to underscores; dots are kept, so namespace packages remain dotted.

Parameters:

Name Type Description Default
req str

A dependency requirement string, e.g. my-package[extra]>=1.0.

required

Returns:

Type Description
str

The package name in snake_case, e.g. my_package.

Example

distribution_requirement_as_module_name("my-package[extra]>=1.0.0") 'my_package'

Source code in src/pyrig_runtime/core/dependencies/distribution.py
def distribution_requirement_as_module_name(req: str) -> str:
    """Extract the importable module name from a dependency requirement string.

    Version specifiers, extras, and anything else after the package name
    are discarded. Hyphens are normalized to underscores; dots are kept, so
    namespace packages remain dotted.

    Args:
        req: A dependency requirement string, e.g. `my-package[extra]>=1.0`.

    Returns:
        The package name in snake_case, e.g. `my_package`.

    Example:
        >>> distribution_requirement_as_module_name("my-package[extra]>=1.0.0")
        'my_package'
    """
    return kebab_to_snake_case(regex_find(REQUIRES_DIST_NAME_PATTERN, req))
distribution_requirements
distribution_requirements(metadata: str) -> list[str]

Return the list of dependency requirements from a distribution's metadata.

Source code in src/pyrig_runtime/core/dependencies/distribution.py
def distribution_requirements(metadata: str) -> list[str]:
    """Return the list of dependency requirements from a distribution's metadata."""
    return DISTRIBUTION_REQUIRES_DIST_PATTERN.findall(metadata)
distribution_summary
distribution_summary(metadata: str) -> str

Return the summary recorded in an installed distribution's metadata.

Parameters:

Name Type Description Default
metadata str

The full metadata of an installed distribution.

required

Returns:

Type Description
str

The distribution's summary description.

Raises:

Type Description
LookupError

If the metadata has no Summary field.

Source code in src/pyrig_runtime/core/dependencies/distribution.py
def distribution_summary(metadata: str) -> str:
    """Return the summary recorded in an installed distribution's metadata.

    Args:
        metadata: The full metadata of an installed distribution.

    Returns:
        The distribution's summary description.

    Raises:
        LookupError: If the metadata has no `Summary` field.
    """
    return regex_find(DISTRIBUTION_SUMMARY_PATTERN, metadata)

graph

Directed graph of installed Python package dependency relationships.

DependencyGraph
DependencyGraph()

Bases: DiGraph

Directed graph of installed Python package dependencies.

Nodes are package names normalized to their importable module form (hyphens become underscores); an edge A → B means "A depends on B". The graph is built at instantiation by scanning every installed distribution.

Source code in src/pyrig_runtime/core/graph.py
def __init__(self) -> None:
    """Initialize the directed graph by building it."""
    self.nodes: set[str] = set()
    self.edges: dict[str, set[str]] = {}
    self.reverse_edges: dict[str, set[str]] = {}
    self.build()
build
build() -> None

Build the graph from installed Python distributions.

Distributions whose metadata cannot be read are skipped and do not become nodes.

Source code in src/pyrig_runtime/core/dependencies/graph.py
def build(self) -> None:
    """Build the graph from installed Python distributions.

    Distributions whose metadata cannot be read are skipped and do
    not become nodes.
    """
    for dist in importlib.metadata.distributions():
        name, deps = self.parse_name_and_deps(dist)
        if not name:
            continue
        self.add_node(name)
        for dep in deps:
            self.add_edge(name, dep)
parse_name_and_deps
parse_name_and_deps(
    dist: Distribution,
) -> tuple[str, Iterator[str]]

Extract the package name and dependencies from a distribution.

The name and every dependency name are normalized to an importable module name; dots are preserved for namespace packages (e.g. zope.interface remains zope.interface).

Parameters:

Name Type Description Default
dist Distribution

Distribution to extract metadata from.

required

Returns:

Type Description
str

A two-tuple (name, deps) where deps is an iterator over the

Iterator[str]

normalized name of each dependency the distribution declares. If

tuple[str, Iterator[str]]

the distribution's metadata cannot be read, name is the empty

tuple[str, Iterator[str]]

string and deps yields nothing.

Raises:

Type Description
LookupError

If the distribution's metadata can be read but does not declare a Name field.

Source code in src/pyrig_runtime/core/dependencies/graph.py
def parse_name_and_deps(
    self,
    dist: importlib.metadata.Distribution,
) -> tuple[str, Iterator[str]]:
    """Extract the package name and dependencies from a distribution.

    The name and every dependency name are normalized to an importable
    module name; dots are preserved for namespace packages (e.g.
    `zope.interface` remains `zope.interface`).

    Args:
        dist: Distribution to extract metadata from.

    Returns:
        A two-tuple `(name, deps)` where `deps` is an iterator over the
        normalized name of each dependency the distribution declares. If
        the distribution's metadata cannot be read, `name` is the empty
        string and `deps` yields nothing.

    Raises:
        LookupError: If the distribution's metadata can be read but does
            not declare a `Name` field.
    """
    metadata = distribution_metadata(dist)
    if metadata is None:
        return "", iter(())
    header = distribution_header(metadata)
    return kebab_to_snake_case(distribution_name(header)), (
        distribution_requirement_as_module_name(req)
        for req in distribution_requirements(header)
    )

subclass

Abstract base for cross-package subclass discovery without explicit registration.

DependencySubclass

Abstract base enabling plugin-style subclass discovery across installed packages.

Subclasses declare a discovery scope by overriding the discovery hook, and the base class automatically finds every subclass defined at that scope, both within its root package and across every installed package that depends on it. The scope may be a single module, to keep discovery narrow, or a whole sub-package, to widen it to a full module hierarchy. No explicit registration is required.

__str__
__str__() -> str

Return the fully qualified name of this instance's class.

Source code in src/pyrig_runtime/core/dependencies/subclass.py
def __str__(self) -> str:
    """Return the fully qualified name of this instance's class."""
    return str(self.__class__)
concrete_leaves classmethod
concrete_leaves() -> Iterator[type[Self]]

Yield all concrete leaf subclasses found within the declared discovery scope.

Yields:

Type Description
type[Self]

Non-abstract leaf subclass types.

Source code in src/pyrig_runtime/core/dependencies/subclass.py
@classmethod
def concrete_leaves(cls) -> Iterator[type[Self]]:
    """Yield all concrete leaf subclasses found within the declared discovery scope.

    Yields:
        Non-abstract leaf subclass types.
    """
    return filter_concrete_classes(cls.leaves())
discovery_module abstractmethod classmethod
discovery_module() -> ModuleType

Return the module or package that scopes discovery of this class.

Used by subclasses() to scope cross-package discovery to the correct namespace. Every concrete subclass must override this to declare where its own implementation classes live: returning a package widens discovery to that package's whole module hierarchy, while returning a plain module keeps discovery narrow to that single module.

The base implementation returns pyrig_runtime.rig.

Returns:

Type Description
ModuleType

The module or package that scopes the search for this class's

ModuleType

subclasses.

Note

The returned module's root package must be pyrig_runtime itself or one of its installed dependents; otherwise discovery fails.

Source code in src/pyrig_runtime/core/dependencies/subclass.py
@classmethod
@abstractmethod
def discovery_module(cls) -> ModuleType:
    """Return the module or package that scopes discovery of this class.

    Used by `subclasses()` to scope cross-package discovery to the correct
    namespace. Every concrete subclass must override this to declare where its
    own implementation classes live: returning a package widens discovery to
    that package's whole module hierarchy, while returning a plain module
    keeps discovery narrow to that single module.

    The base implementation returns `pyrig_runtime.rig`.

    Returns:
        The module or package that scopes the search for this class's
        subclasses.

    Note:
        The returned module's root package must be `pyrig_runtime` itself
        or one of its installed dependents; otherwise discovery fails.
    """
    return rig
leaf classmethod
leaf() -> type[Self]

Return the leaf subclass for this class's discovery scope.

Returns the class itself if no subclasses are discovered. Otherwise returns the first value leaves() yields for this class: if every discovered leaf shares one merge key, that is this class's one true leaf; if leaves span more than one merge key, every group but the first encountered is silently discarded, so leaf() is only meaningful for hierarchies that resolve to a single merge key.

Returns:

Type Description
type[Self]

The first leaf subclass type leaves() yields, or the class

type[Self]

itself if none are found.

Raises:

Type Description
TypeError

If the leaves in the first merge-key group cannot be combined into a single class.

Note

Discovery runs fresh on every call, and merging generates a new type each time, so two calls do not return the identical object when leaves are merged — only L caches a stable result. Which merged leaf's behavior wins for any method they both define should not be relied upon.

Source code in src/pyrig_runtime/core/dependencies/subclass.py
@classmethod
def leaf(cls) -> type[Self]:
    """Return the leaf subclass for this class's discovery scope.

    Returns the class itself if no subclasses are discovered. Otherwise
    returns the first value `leaves()` yields for this class: if every
    discovered leaf shares one merge key, that is this class's one
    true leaf; if leaves span more than one merge key, every group but
    the first encountered is silently discarded, so `leaf()` is only
    meaningful for hierarchies that resolve to a single merge key.

    Returns:
        The first leaf subclass type `leaves()` yields, or the class
        itself if none are found.

    Raises:
        TypeError: If the leaves in the first merge-key group cannot
            be combined into a single class.

    Note:
        Discovery runs fresh on every call, and merging generates a
        new type each time, so two calls do not return the identical
        object when leaves are merged — only `L` caches a stable
        result. Which merged leaf's behavior wins for any method
        they both define should not be relied upon.
    """
    return next(cls.leaves(), cls)
leaves classmethod
leaves() -> Iterator[type[Self]]

Yield leaf subclasses discovered within the declared discovery scope.

Only leaf-level subclasses are considered; any intermediate parent classes that also appear in the result are omitted. The remaining leaves are grouped by merge_key(): a group with a single leaf is yielded as-is, while a group with several leaves is combined into one newly generated subclass inheriting from every leaf in that group, letting independently-installed packages cooperatively extend the same class by sharing a merge key.

Yields:

Type Description
type[Self]

Leaf subclass types, one per distinct merge_key() value.

Raises:

Type Description
TypeError

If the leaves within a merge key group cannot be combined into a single class.

Source code in src/pyrig_runtime/core/dependencies/subclass.py
@classmethod
def leaves(cls) -> Iterator[type[Self]]:
    """Yield leaf subclasses discovered within the declared discovery scope.

    Only leaf-level subclasses are considered; any intermediate parent
    classes that also appear in the result are omitted. The remaining leaves
    are grouped by `merge_key()`: a group with a single leaf is yielded as-is,
    while a group with several leaves is combined into one newly generated subclass
    inheriting from every leaf in that group, letting independently-installed
    packages cooperatively extend the same class by sharing a merge key.

    Yields:
        Leaf subclass types, one per distinct `merge_key()` value.

    Raises:
        TypeError: If the leaves within a merge key group cannot be
            combined into a single class.
    """
    by_merge_key: dict[Hashable, list[type[Self]]] = defaultdict(list)
    for subclass in filter_leaf_classes(cls.subclasses()):
        by_merge_key[subclass.merge_key()].append(subclass)
    for subclasses in by_merge_key.values():
        subcls = subclasses[0]
        if len(subclasses) == 1:
            yield subcls
        else:
            yield generate_class(
                name=subcls.__name__,
                bases=tuple(subclasses),
            )
merge_key classmethod
merge_key() -> Hashable

Return the key that decides which leaf subclasses get merged together.

Leaf subclasses that return an equal merge key are combined into one generated subclass by leaves(); those with different keys stay apart. Override to group cooperating implementations under a shared key. The default returns the class name, so same-named leaf overrides across dependent packages merge automatically.

Returns:

Type Description
Hashable

A value comparable with == against the merge keys of other

Hashable

subclasses.

Source code in src/pyrig_runtime/core/dependencies/subclass.py
@classmethod
def merge_key(cls) -> Hashable:
    """Return the key that decides which leaf subclasses get merged together.

    Leaf subclasses that return an equal merge key are combined into
    one generated subclass by `leaves()`; those with different keys
    stay apart. Override to group cooperating implementations under a
    shared key. The default returns the class name, so same-named
    leaf overrides across dependent packages merge automatically.

    Returns:
        A value comparable with `==` against the merge keys of other
        subclasses.
    """
    return cls.__name__
sort_key classmethod
sort_key() -> SupportsRichComparison

Return the sort key used to order this class relative to peer subclasses.

Used by sorted_subclasses() to order a collection of subclasses. Override to sort by priority, numeric position, or any other criterion. The default returns the class name, giving alphabetical ordering.

Returns:

Type Description
SupportsRichComparison

A value comparable with < against the sort keys of other

SupportsRichComparison

subclasses.

Source code in src/pyrig_runtime/core/dependencies/subclass.py
@classmethod
def sort_key(cls) -> "SupportsRichComparison":
    """Return the sort key used to order this class relative to peer subclasses.

    Used by `sorted_subclasses()` to order a collection of subclasses.
    Override to sort by priority, numeric position, or any other criterion.
    The default returns the class name, giving alphabetical ordering.

    Returns:
        A value comparable with `<` against the sort keys of other
        subclasses.
    """
    return cls.__name__
sorted_subclasses classmethod
sorted_subclasses(
    subclasses: Iterable[type[Self]],
) -> list[type[Self]]

Sort the given subclasses using each subclass's sort_key().

Does not perform any discovery.

Parameters:

Name Type Description Default
subclasses Iterable[type[Self]]

Subclass types to sort.

required

Returns:

Type Description
list[type[Self]]

The same subclass types sorted by their sort_key().

Source code in src/pyrig_runtime/core/dependencies/subclass.py
@classmethod
def sorted_subclasses(
    cls,
    subclasses: Iterable[type[Self]],
) -> list[type[Self]]:
    """Sort the given subclasses using each subclass's `sort_key()`.

    Does not perform any discovery.

    Args:
        subclasses: Subclass types to sort.

    Returns:
        The same subclass types sorted by their `sort_key()`.
    """
    return sorted(
        subclasses,
        key=methodcaller(cls.sort_key.__name__),
    )
subclasses classmethod
subclasses() -> Iterator[type[Self]]

Yield every subclass discovered within the declared discovery scope.

Includes intermediate parent classes; unlike leaves(), the result is not filtered down to leaves.

Yields:

Type Description
type[Self]

Subclass types found anywhere in the discovery scope. The

type[Self]

order is stable across calls but reflects discovery order,

type[Self]

not any deliberate priority.

Source code in src/pyrig_runtime/core/dependencies/subclass.py
@classmethod
def subclasses(cls) -> Iterator[type[Self]]:
    """Yield every subclass discovered within the declared discovery scope.

    Includes intermediate parent classes; unlike `leaves()`, the
    result is not filtered down to leaves.

    Yields:
        Subclass types found anywhere in the discovery scope. The
        order is stable across calls but reflects discovery order,
        not any deliberate priority.
    """
    return subclasses_across_dependencies(
        cls,
        module=cls.discovery_module(),
    )
DependencySubclassMeta

Bases: ABCMeta

Metaclass backing DependencySubclass with the cached I/L properties.

I property
I: C

Return a cached instance of L.

The instance is created once per class and reused on every subsequent access.

Returns:

Type Description
C

An instance of L.

Raises:

Type Description
TypeError

If L is abstract and cannot be instantiated, or if resolving L itself raises.

L property
L: type[C]

Return the cached result of leaf().

Computed once per class on first access and reused on every subsequent access.

Returns:

Type Description
type[C]

The same value leaf() returns for this class.

Raises:

Type Description
TypeError

If leaf() raises.

__str__
__str__() -> str

Return the fully qualified name of this class.

Source code in src/pyrig_runtime/core/dependencies/subclass.py
def __str__(cls) -> str:
    """Return the fully qualified name of this class."""
    return fully_qualified_name(cls)

graph

Abstract directed graph foundation with forward and reverse edge traversal.

DiGraph

DiGraph()

Bases: ABC

Abstract directed graph with forward and reverse adjacency tracking.

Subclasses implement build to populate nodes and edges.

Attributes:

Name Type Description
nodes set[str]

Set of all node identifiers currently in the graph.

edges dict[str, set[str]]

Forward adjacency map from each node to its outgoing neighbors.

reverse_edges dict[str, set[str]]

Reverse adjacency map from each node to its incoming neighbors.

Source code in src/pyrig_runtime/core/graph.py
def __init__(self) -> None:
    """Initialize the directed graph by building it."""
    self.nodes: set[str] = set()
    self.edges: dict[str, set[str]] = {}
    self.reverse_edges: dict[str, set[str]] = {}
    self.build()
add_edge
add_edge(source: str, target: str) -> None

Add a directed edge from source to target.

Creates both nodes if they do not already exist.

Parameters:

Name Type Description Default
source str

Edge origin node.

required
target str

Edge destination node.

required
Source code in src/pyrig_runtime/core/graph.py
def add_edge(self, source: str, target: str) -> None:
    """Add a directed edge from source to target.

    Creates both nodes if they do not already exist.

    Args:
        source: Edge origin node.
        target: Edge destination node.
    """
    self.add_node(source)
    self.add_node(target)
    self.edges[source].add(target)
    self.reverse_edges[target].add(source)
add_node
add_node(node: str) -> None

Add a node to the graph. No-op if the node already exists.

Source code in src/pyrig_runtime/core/graph.py
def add_node(self, node: str) -> None:
    """Add a node to the graph. No-op if the node already exists."""
    if node not in self.nodes:
        self.nodes.add(node)
        self.edges[node] = set()
        self.reverse_edges[node] = set()
ancestors
ancestors(target: str) -> set[str]

Find all nodes that have a directed path to the target node.

Parameters:

Name Type Description Default
target str

Node to find ancestors for.

required

Returns:

Type Description
set[str]

Set of nodes with a directed path to the target. The target

set[str]

itself is included only if the graph has a cycle back to it.

Raises:

Type Description
KeyError

If the target node is not in the graph.

Source code in src/pyrig_runtime/core/graph.py
def ancestors(self, target: str) -> set[str]:
    """Find all nodes that have a directed path to the target node.

    Args:
        target: Node to find ancestors for.

    Returns:
        Set of nodes with a directed path to the target. The target
        itself is included only if the graph has a cycle back to it.

    Raises:
        KeyError: If the target node is not in the graph.
    """
    visited: set[str] = set(self.reverse_edges[target])
    queue: deque[str] = deque(visited)

    while queue:
        node = queue.popleft()
        for neighbor in self.reverse_edges[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

    return visited
build abstractmethod
build() -> None

Populate the graph with nodes and edges.

Called during construction. Subclasses must add every node and edge that belongs to the graph.

Source code in src/pyrig_runtime/core/graph.py
@abstractmethod
def build(self) -> None:
    """Populate the graph with nodes and edges.

    Called during construction. Subclasses must add every node and edge
    that belongs to the graph.
    """
prune
prune(root: str) -> None

Remove every node except root and its ancestors.

Edges to or from a removed node are removed along with it.

Parameters:

Name Type Description Default
root str

Node whose ancestors are kept.

required

Raises:

Type Description
KeyError

If root is not in the graph.

Source code in src/pyrig_runtime/core/graph.py
def prune(self, root: str) -> None:
    """Remove every node except `root` and its ancestors.

    Edges to or from a removed node are removed along with it.

    Args:
        root: Node whose ancestors are kept.

    Raises:
        KeyError: If `root` is not in the graph.
    """
    keep = self.ancestors(root) | {root}
    self.nodes = keep
    self.edges = {n: self.edges[n] & keep for n in keep}
    self.reverse_edges = {n: self.reverse_edges[n] & keep for n in keep}
sorted_ancestors
sorted_ancestors(target: str) -> Iterable[str]

Return the ancestors of target in topological order.

Each ancestor appears after every ancestor it has an outgoing edge to.

Parameters:

Name Type Description Default
target str

Node to find ancestors of.

required

Yields:

Type Description
Iterable[str]

Each ancestor of target, in topological order. Yields nothing

Iterable[str]

if target has no ancestors.

Raises:

Type Description
KeyError

If target is not in the graph.

CycleError

If the ancestor subgraph contains a cycle, making topological sorting impossible.

Source code in src/pyrig_runtime/core/graph.py
def sorted_ancestors(self, target: str) -> Iterable[str]:
    """Return the ancestors of `target` in topological order.

    Each ancestor appears after every ancestor it has an outgoing edge to.

    Args:
        target: Node to find ancestors of.

    Yields:
        Each ancestor of `target`, in topological order. Yields nothing
        if `target` has no ancestors.

    Raises:
        KeyError: If `target` is not in the graph.
        graphlib.CycleError: If the ancestor subgraph contains a cycle,
            making topological sorting impossible.
    """
    return self.topological_sort_subgraph(self.ancestors(target))
topological_sort_subgraph
topological_sort_subgraph(nodes: set[str]) -> Iterable[str]

Sort a subset of nodes in topological order.

If there is an edge from A to B, B appears before A in the result. Nodes with no dependency relationship between them may appear in any relative order.

Only edges whose both endpoints are in nodes are considered; edges to or from nodes outside the subset are ignored.

Parameters:

Name Type Description Default
nodes set[str]

The subset of nodes to sort.

required

Yields:

Type Description
Iterable[str]

Each node in nodes, in topological order.

Raises:

Type Description
KeyError

If any node in nodes is not part of the graph.

CycleError

If the subgraph contains a cycle, making topological sorting impossible.

Source code in src/pyrig_runtime/core/graph.py
def topological_sort_subgraph(self, nodes: set[str]) -> Iterable[str]:
    """Sort a subset of nodes in topological order.

    If there is an edge from A to B, B appears before A in the result.
    Nodes with no dependency relationship between them may appear in
    any relative order.

    Only edges whose both endpoints are in `nodes` are considered; edges
    to or from nodes outside the subset are ignored.

    Args:
        nodes: The subset of nodes to sort.

    Yields:
        Each node in `nodes`, in topological order.

    Raises:
        KeyError: If any node in `nodes` is not part of the graph.
        graphlib.CycleError: If the subgraph contains a cycle, making
            topological sorting impossible.
    """
    return TopologicalSorter(
        {node: self.edges[node] & nodes for node in nodes},
    ).static_order()

introspection

Runtime introspection primitives for classes, callables, modules, and packages.

classes

Utilities for Python classes.

discover_subclasses
discover_subclasses[T](
    cls: type[T],
) -> tuple[type[T], ...]

Discover all transitive subclasses of cls currently loaded in memory.

Each subclass appears exactly once, even when reachable through multiple inheritance paths. Does not trigger any imports, so only subclasses from already-imported modules are included in the result.

Parameters:

Name Type Description Default
cls type[T]

Base class to find subclasses of.

required

Returns:

Type Description
tuple[type[T], ...]

Tuple of all transitive subclass types, excluding cls itself.

Source code in src/pyrig_runtime/core/introspection/classes.py
def discover_subclasses[T](cls: type[T]) -> tuple[type[T], ...]:
    """Discover all transitive subclasses of `cls` currently loaded in memory.

    Each subclass appears exactly once, even when reachable through multiple
    inheritance paths. Does not trigger any imports, so only subclasses from
    already-imported modules are included in the result.

    Args:
        cls: Base class to find subclasses of.

    Returns:
        Tuple of all transitive subclass types, excluding `cls` itself.
    """
    visited: dict[type[T], None] = {}
    stack = cls.__subclasses__()
    while stack:
        subclass = stack.pop()
        if subclass in visited:
            continue
        visited[subclass] = None
        stack.extend(subclass.__subclasses__())
    return tuple(visited)
filter_concrete_classes
filter_concrete_classes[T](
    classes: Iterable[type[T]],
) -> Iterator[type[T]]

Filter out abstract classes from a collection.

A class is considered abstract when it has one or more unimplemented abstract methods and therefore cannot be instantiated directly.

Parameters:

Name Type Description Default
classes Iterable[type[T]]

Iterable of class types to filter.

required

Yields:

Type Description
type[T]

Concrete (non-abstract) classes from the input, in the same order

type[T]

as classes.

Source code in src/pyrig_runtime/core/introspection/classes.py
def filter_concrete_classes[T](classes: Iterable[type[T]]) -> Iterator[type[T]]:
    """Filter out abstract classes from a collection.

    A class is considered abstract when it has one or more unimplemented
    abstract methods and therefore cannot be instantiated directly.

    Args:
        classes: Iterable of class types to filter.

    Yields:
        Concrete (non-abstract) classes from the input, in the same order
        as `classes`.
    """
    return filterfalse(inspect.isabstract, classes)
filter_leaf_classes
filter_leaf_classes[T](
    classes: Iterable[type[T]],
) -> Iterator[type[T]]

Yield only leaf classes, removing any ancestors present in the collection.

A class is kept only when no other class in the collection is a strict subclass of it. The original iterable is not modified.

Parameters:

Name Type Description Default
classes Iterable[type[T]]

Iterable of class types to filter.

required

Yields:

Type Description
type[T]

Classes that have no subclasses present in the same collection, in

type[T]

the same order as classes.

Source code in src/pyrig_runtime/core/introspection/classes.py
def filter_leaf_classes[T](
    classes: Iterable[type[T]],
) -> Iterator[type[T]]:
    """Yield only leaf classes, removing any ancestors present in the collection.

    A class is kept only when no other class in the collection is a strict
    subclass of it. The original iterable is not modified.

    Args:
        classes: Iterable of class types to filter.

    Yields:
        Classes that have no subclasses present in the same collection, in
        the same order as `classes`.
    """
    classes = tuple(classes)
    parents = {parent for cls in classes for parent in cls.__mro__[1:]}
    return filterfalse(parents.__contains__, classes)
generate_class
generate_class[T](
    name: str,
    bases: tuple[type[T], ...],
    methods: Iterable[FunctionType] = (),
    namespace: dict[str, Any] | None = None,
) -> type[T]

Dynamically create a class from base classes, methods, and attributes.

Parameters:

Name Type Description Default
name str

Name of the new class, used as its __name__.

required
bases tuple[type[T], ...]

Base classes the new class inherits from.

required
methods Iterable[FunctionType]

Functions to add to the class, each under its own __name__.

()
namespace dict[str, Any] | None

Extra attributes for the class body, keyed by name. Mutated in place with the methods entries added on top, so a method whose name matches a key here overrides it. Defaults to a new, empty dict when omitted.

None

Returns:

Type Description
type[T]

The newly created class.

Raises:

Type Description
TypeError

If bases cannot be combined into a class with a consistent method resolution order.

Note

The generated class's __module__ is whatever the underlying type() call infers from the calling context, which is not necessarily the caller's own module. Pass "__module__" in namespace to set it explicitly.

Source code in src/pyrig_runtime/core/introspection/classes.py
def generate_class[T](
    name: str,
    bases: tuple[type[T], ...],
    methods: Iterable[FunctionType] = (),
    namespace: dict[str, Any] | None = None,
) -> type[T]:
    """Dynamically create a class from base classes, methods, and attributes.

    Args:
        name: Name of the new class, used as its `__name__`.
        bases: Base classes the new class inherits from.
        methods: Functions to add to the class, each under its own `__name__`.
        namespace: Extra attributes for the class body, keyed by name. Mutated
            in place with the `methods` entries added on top, so a method
            whose name matches a key here overrides it. Defaults to a new,
            empty dict when omitted.

    Returns:
        The newly created class.

    Raises:
        TypeError: If `bases` cannot be combined into a class with a
            consistent method resolution order.

    Note:
        The generated class's `__module__` is whatever the underlying
        `type()` call infers from the calling context, which is not
        necessarily the caller's own module. Pass `"__module__"` in
        `namespace` to set it explicitly.
    """
    if namespace is None:
        namespace = {}
    for method in methods:
        namespace[method.__name__] = method
    return cast(
        "type[T]",
        type(
            name,
            bases,
            namespace,
        ),
    )

functions

Utilities for Python functions.

filter_module_functions
filter_module_functions(
    module: ModuleType, members: Iterable[Any]
) -> Iterator[Callable[..., Any]]

Yield functions from members defined directly in module.

Parameters:

Name Type Description Default
module ModuleType

Module to filter functions for.

required
members Iterable[Any]

Iterable of candidate members to filter.

required

Yields:

Type Description
Callable[..., Any]

Each function defined in module from members.

Source code in src/pyrig_runtime/core/introspection/functions.py
def filter_module_functions(
    module: ModuleType,
    members: Iterable[Any],
) -> Iterator[Callable[..., Any]]:
    """Yield functions from `members` defined directly in `module`.

    Args:
        module: Module to filter functions for.
        members: Iterable of candidate members to filter.

    Yields:
        Each function defined in `module` from `members`.
    """
    for member in members:
        unwrapped_member = unwrap_obj(member)
        if (
            inspect.isfunction(unwrapped_member)
            and unwrapped_member.__module__ == module.__name__
        ):
            yield member
module_functions
module_functions(
    module: ModuleType,
) -> Iterator[Callable[..., Any]]

Yield all functions defined directly in a module.

Excludes objects imported from other modules.

Parameters:

Name Type Description Default
module ModuleType

Module to inspect.

required

Yields:

Type Description
Callable[..., Any]

Each function defined in module.

Source code in src/pyrig_runtime/core/introspection/functions.py
def module_functions(
    module: ModuleType,
) -> Iterator[Callable[..., Any]]:
    """Yield all functions defined directly in a module.

    Excludes objects imported from other modules.

    Args:
        module: Module to inspect.

    Yields:
        Each function defined in `module`.
    """
    yield from filter_module_functions(module, obj_members(module))

inspection

Utilities for inspecting Python objects.

obj_members
obj_members(
    obj: object,
    predicate: Callable[[Any], bool] | None = None,
) -> Iterator[Any]

Yield the values of an object's members without invoking descriptors.

Members are read statically, so properties with side effects are not triggered. __annotate__ and __annotate_func__ are always excluded from the result.

Parameters:

Name Type Description Default
obj object

Object to inspect (class, module, or any Python object).

required
predicate Callable[[Any], bool] | None

Optional filter. When given, only members for which it returns True are included.

None

Yields:

Type Description
Any

Each matching member's value.

Source code in src/pyrig_runtime/core/introspection/inspection.py
def obj_members(
    obj: object,
    predicate: Callable[[Any], bool] | None = None,
) -> Iterator[Any]:
    """Yield the values of an object's members without invoking descriptors.

    Members are read statically, so properties with side effects are not
    triggered. `__annotate__` and `__annotate_func__` are always excluded
    from the result.

    Args:
        obj: Object to inspect (class, module, or any Python object).
        predicate: Optional filter. When given, only members for which it
            returns `True` are included.

    Yields:
        Each matching member's value.
    """
    excluded = {"__annotate__", "__annotate_func__"}
    return (
        value
        for member, value in inspect.getmembers_static(obj, predicate=predicate)
        if member not in excluded
    )
unwrap_obj
unwrap_obj(obj: Callable[..., Any]) -> FunctionType | type
unwrap_obj(obj: property) -> FunctionType
unwrap_obj[T](obj: T) -> T
unwrap_obj(obj: Any) -> Any

Unwrap a Python object to its innermost underlying object.

Recognizes properties, bound methods, classmethods, staticmethods, and functools.wraps-style decorator chains. All layers are removed, not just the outermost one.

Parameters:

Name Type Description Default
obj Any

Python object to unwrap.

required

Returns:

Type Description
Any

The innermost Python object after all wrapping layers have been removed.

Source code in src/pyrig_runtime/core/introspection/inspection.py
def unwrap_obj(obj: Any) -> Any:
    """Unwrap a Python object to its innermost underlying object.

    Recognizes properties, bound methods, classmethods, staticmethods, and
    `functools.wraps`-style decorator chains. All layers are removed, not just
    the outermost one.

    Args:
        obj: Python object to unwrap.

    Returns:
        The innermost Python object after all wrapping layers have been removed.
    """
    prev = None
    while prev is not obj:
        prev = obj
        if (func := getattr(obj, "__func__", None)) is not None:
            obj = func
        if (fget := getattr(obj, "fget", None)) is not None:
            obj = fget
        if hasattr(obj, "__wrapped__"):
            obj = inspect.unwrap(obj)
    return obj

modules

Utilities for Python modules.

import_modules
import_modules(
    module_names: Iterable[str],
) -> Iterator[ModuleType]

Import multiple modules by name, lazily.

Modules are imported on demand as the result is iterated, not eagerly.

Parameters:

Name Type Description Default
module_names Iterable[str]

Dotted module names to import.

required

Yields:

Type Description
ModuleType

Each imported module, in the order the names are iterated.

Source code in src/pyrig_runtime/core/introspection/modules.py
def import_modules(module_names: Iterable[str]) -> Iterator[ModuleType]:
    """Import multiple modules by name, lazily.

    Modules are imported on demand as the result is iterated, not eagerly.

    Args:
        module_names: Dotted module names to import.

    Yields:
        Each imported module, in the order the names are iterated.
    """
    return (import_module(name) for name in module_names)
iter_modules
iter_modules(
    package: ModuleType,
) -> Iterator[tuple[ModuleType, bool]]

Import and yield each direct child of a package, in discovery order.

Only the immediate children are visited; nested sub-packages are not recursed into.

Parameters:

Name Type Description Default
package ModuleType

Package to iterate. Must have a __path__ attribute (i.e., must be a package, not a plain module).

required

Yields:

Type Description
ModuleType

(module, is_package) pairs where module is the imported child and

bool

is_package is True when the child is itself a sub-package.

Note

Importing each child is a deliberate side effect — any module-level code in those children executes on demand as the iterator is consumed.

Source code in src/pyrig_runtime/core/introspection/modules.py
def iter_modules(package: ModuleType) -> Iterator[tuple[ModuleType, bool]]:
    """Import and yield each direct child of a package, in discovery order.

    Only the immediate children are visited; nested sub-packages are not
    recursed into.

    Args:
        package: Package to iterate. Must have a `__path__` attribute
            (i.e., must be a package, not a plain module).

    Yields:
        `(module, is_package)` pairs where `module` is the imported child and
        `is_package` is `True` when the child is itself a sub-package.

    Note:
        Importing each child is a deliberate side effect — any module-level
        code in those children executes on demand as the iterator is consumed.
    """
    for _finder, name, is_package in pkgutil_iter_modules(
        package.__path__,
        prefix=package.__name__ + ".",
    ):
        mod = import_module(name)
        yield mod, is_package
replace_root_module
replace_root_module(
    module: ModuleType, root: str
) -> ModuleType
replace_root_module[T](
    module: ModuleType, root: str, default: T
) -> ModuleType | T
replace_root_module(
    module: ModuleType, root: str, default: Any = MISSING
) -> ModuleType | Any

Import the equivalent module under a different root package.

Replaces the first dotted segment of module's name with root and attempts to import the resulting module name. Later segments are left untouched even if they happen to share the old root's name.

Parameters:

Name Type Description Default
module ModuleType

Module whose root segment should be swapped.

required
root str

Root package name to substitute in.

required
default Any

Value to return if the import fails. If not provided, the exception propagates unchanged.

MISSING

Returns:

Type Description
ModuleType | Any

The imported module at the equivalent sub-path under root, or

ModuleType | Any

default if the import fails and default was provided.

Source code in src/pyrig_runtime/core/introspection/modules.py
def replace_root_module(
    module: ModuleType,
    root: str,
    default: Any = MISSING,
) -> ModuleType | Any:
    """Import the equivalent module under a different root package.

    Replaces the first dotted segment of `module`'s name with `root` and
    attempts to import the resulting module name. Later segments are left
    untouched even if they happen to share the old root's name.

    Args:
        module: Module whose root segment should be swapped.
        root: Root package name to substitute in.
        default: Value to return if the import fails. If not provided,
            the exception propagates unchanged.

    Returns:
        The imported module at the equivalent sub-path under `root`, or
        `default` if the import fails and `default` was provided.
    """
    return safe_import_module(
        replace_root_module_name(module.__name__, root),
        default=default,
    )
replace_root_module_name
replace_root_module_name(name: str, root: str) -> str

Return the equivalent module name under a different root package.

Replaces the first dotted segment of name with root. Later segments are left untouched even if they happen to share the old root's name. For a top-level name with no dots, the entire string is replaced and root alone is returned.

Parameters:

Name Type Description Default
name str

Dotted module name (e.g., "package.subpackage.module").

required
root str

Root package name to substitute in.

required

Returns:

Type Description
str

The equivalent dotted module name under root.

Example

replace_root_module_name("some_package.subpackage.module", "other_package") 'other_package.subpackage.module'

Source code in src/pyrig_runtime/core/introspection/modules.py
def replace_root_module_name(name: str, root: str) -> str:
    """Return the equivalent module name under a different root package.

    Replaces the first dotted segment of `name` with `root`. Later segments
    are left untouched even if they happen to share the old root's name.
    For a top-level `name` with no dots, the entire string is replaced and
    `root` alone is returned.

    Args:
        name: Dotted module name (e.g., `"package.subpackage.module"`).
        root: Root package name to substitute in.

    Returns:
        The equivalent dotted module name under `root`.

    Example:
        >>> replace_root_module_name("some_package.subpackage.module", "other_package")
        'other_package.subpackage.module'
    """
    return name.replace(root_module_name(name), root, 1)
root_module
root_module(module: ModuleType) -> ModuleType

Import and return the top-level package of the given module.

For a module named "package.subpackage.module", the module corresponding to "package" is returned. For a top-level module with no dots in its name, the module for that same name is returned.

Parameters:

Name Type Description Default
module ModuleType

Module to resolve the root package for.

required

Returns:

Type Description
ModuleType

The module corresponding to the first segment of the dotted name.

Source code in src/pyrig_runtime/core/introspection/modules.py
def root_module(module: ModuleType) -> ModuleType:
    """Import and return the top-level package of the given module.

    For a module named `"package.subpackage.module"`, the module corresponding
    to `"package"` is returned. For a top-level module with no dots in its name,
    the module for that same name is returned.

    Args:
        module: Module to resolve the root package for.

    Returns:
        The module corresponding to the first segment of the dotted name.
    """
    return import_module(root_module_name(module.__name__))
root_module_name
root_module_name(name: str) -> str

Return the name of the top-level package of the given module.

For a module named "package.subpackage.module", the string "package" is returned. For a top-level module with no dots in its name, that same name is returned.

Parameters:

Name Type Description Default
name str

Dotted module name (e.g., "package.subpackage.module").

required

Returns:

Type Description
str

The first segment of the dotted module name.

Example

root_module_name("some_package.subpackage.module") 'some_package'

Source code in src/pyrig_runtime/core/introspection/modules.py
def root_module_name(name: str) -> str:
    """Return the name of the top-level package of the given module.

    For a module named `"package.subpackage.module"`, the string `"package"`
    is returned. For a top-level module with no dots in its name, that same
    name is returned.

    Args:
        name: Dotted module name (e.g., `"package.subpackage.module"`).

    Returns:
        The first segment of the dotted module name.

    Example:
        >>> root_module_name("some_package.subpackage.module")
        'some_package'
    """
    return name.split(".", 1)[0]
safe_import_module
safe_import_module(
    module_name: str,
    package: str | None = ...,
    *,
    exceptions: tuple[type[BaseException], ...] = ...,
) -> ModuleType
safe_import_module[T](
    module_name: str,
    package: str | None = ...,
    *,
    default: T,
    exceptions: tuple[type[BaseException], ...] = ...,
) -> ModuleType | T
safe_import_module(
    module_name: str,
    package: str | None = None,
    *,
    default: Any = MISSING,
    exceptions: tuple[type[BaseException], ...] = (
        Exception,
    ),
) -> ModuleType | Any

Import a module by name, with an optional fallback on failure.

By default, catches any Exception raised during import — not just ImportError — so an error from the module's own top-level code (e.g., a stray ValueError) is caught too. Pass exceptions to narrow what gets caught.

Parameters:

Name Type Description Default
module_name str

Dotted module name (e.g., "package.subpackage.module").

required
package str | None

Anchor package for relative imports, forwarded to import_module.

None
default Any

Value to return if the import raises a caught exception. If not provided, the exception propagates unchanged.

MISSING
exceptions tuple[type[BaseException], ...]

Exception types to catch. Defaults to (Exception,).

(Exception,)

Returns:

Type Description
ModuleType | Any

The imported module, or default if a caught exception is raised

ModuleType | Any

and default was provided.

Source code in src/pyrig_runtime/core/introspection/modules.py
def safe_import_module(
    module_name: str,
    package: str | None = None,
    *,
    default: Any = MISSING,
    exceptions: tuple[type[BaseException], ...] = (Exception,),
) -> ModuleType | Any:
    """Import a module by name, with an optional fallback on failure.

    By default, catches any `Exception` raised during import — not just
    `ImportError` — so an error from the module's own top-level code (e.g.,
    a stray `ValueError`) is caught too. Pass `exceptions` to narrow what
    gets caught.

    Args:
        module_name: Dotted module name (e.g., `"package.subpackage.module"`).
        package: Anchor package for relative imports, forwarded to
            `import_module`.
        default: Value to return if the import raises a caught exception.
            If not provided, the exception propagates unchanged.
        exceptions: Exception types to catch. Defaults to `(Exception,)`.

    Returns:
        The imported module, or `default` if a caught exception is raised
        and `default` was provided.
    """
    return safe_call(
        import_module,
        kwargs={"name": module_name, "package": package},
        default=default,
        exceptions=exceptions,
    )

packages

Utilities for Python packages.

is_package
is_package(module: ModuleType) -> bool

Return True if module is a package rather than a plain module.

Source code in src/pyrig_runtime/core/introspection/packages.py
def is_package(module: ModuleType) -> bool:
    """Return `True` if `module` is a package rather than a plain module."""
    return hasattr(module, "__path__")
register_package_modules cached
register_package_modules(package: ModuleType) -> None

Ensure all modules in a package hierarchy are imported.

Parameters:

Name Type Description Default
package ModuleType

Root package whose entire module hierarchy will be imported.

required
Note

Cached per package — subsequent calls with the same package do nothing.

Source code in src/pyrig_runtime/core/introspection/packages.py
@cache
def register_package_modules(package: ModuleType) -> None:
    """Ensure all modules in a package hierarchy are imported.

    Args:
        package: Root package whose entire module hierarchy will be imported.

    Note:
        Cached per package — subsequent calls with the same package do
        nothing.
    """
    _ = tuple(walk_package(package))
walk_package
walk_package(
    package: ModuleType,
) -> Iterator[tuple[ModuleType, bool]]

Walk all modules in a package hierarchy, recursing into sub-packages.

Importing each visited module is a side effect of iteration. The root package itself is not yielded.

Parameters:

Name Type Description Default
package ModuleType

Root package to start traversal from.

required

Yields:

Type Description
ModuleType

(module, is_package) pairs for each visited module, where

bool

is_package is True when the module is itself a sub-package.

Source code in src/pyrig_runtime/core/introspection/packages.py
def walk_package(package: ModuleType) -> Iterator[tuple[ModuleType, bool]]:
    """Walk all modules in a package hierarchy, recursing into sub-packages.

    Importing each visited module is a side effect of iteration. The root
    `package` itself is not yielded.

    Args:
        package: Root package to start traversal from.

    Yields:
        `(module, is_package)` pairs for each visited module, where
        `is_package` is `True` when the module is itself a sub-package.
    """
    for module, is_pkg in iter_modules(package):
        if is_pkg:
            yield module, True
            yield from walk_package(module)
        else:
            yield module, False

strings

Utilities for working with strings.

fully_qualified_name

fully_qualified_name(
    obj: MethodType | FunctionType | type,
) -> str

Return the fully qualified name of a callable.

The returned name consists of the callable's module and qualified name, preserving any enclosing classes or functions. E.g., for a method foo in class Bar in module baz, the fully qualified name is "baz.Bar.foo".

Parameters:

Name Type Description Default
obj MethodType | FunctionType | type

The callable (function, method, or class).

required

Returns:

Type Description
str

The callable's fully qualified name.

Source code in src/pyrig_runtime/core/strings.py
def fully_qualified_name(obj: MethodType | FunctionType | type) -> str:
    """Return the fully qualified name of a callable.

    The returned name consists of the callable's module and qualified name,
    preserving any enclosing classes or functions.
    E.g., for a method `foo` in class `Bar` in module `baz`, the fully qualified
    name is `"baz.Bar.foo"`.

    Args:
        obj: The callable (function, method, or class).

    Returns:
        The callable's fully qualified name.
    """
    return f"{obj.__module__}.{obj.__qualname__}"

kebab_to_snake_case

kebab_to_snake_case(value: str) -> str

Convert a kebab-case string to snake_case, replacing hyphens with underscores.

Source code in src/pyrig_runtime/core/strings.py
def kebab_to_snake_case(value: str) -> str:
    """Convert a kebab-case string to snake_case, replacing hyphens with underscores."""
    return value.replace("-", "_")

regex_find

regex_find(pattern: Pattern[str], text: str) -> str

Return the first captured group from a regex search on the given text.

Parameters:

Name Type Description Default
pattern Pattern[str]

A compiled regex pattern with at least one capturing group.

required
text str

The text to search within.

required

Returns:

Type Description
str

The first captured group from the regex search.

Raises:

Type Description
LookupError

If no match is found for the pattern in the text.

Source code in src/pyrig_runtime/core/strings.py
def regex_find(pattern: re.Pattern[str], text: str) -> str:
    """Return the first captured group from a regex search on the given text.

    Args:
        pattern: A compiled regex pattern with at least one capturing group.
        text: The text to search within.

    Returns:
        The first captured group from the regex search.

    Raises:
        LookupError: If no match is found for the pattern in the text.
    """
    match = pattern.search(text)
    if match is None:
        msg = f"No match found for pattern {pattern.pattern} in text."
        raise LookupError(msg)
    return match[1]

snake_to_kebab_case

snake_to_kebab_case(value: str) -> str

Convert a snake_case string to kebab-case, replacing underscores with hyphens.

Source code in src/pyrig_runtime/core/strings.py
def snake_to_kebab_case(value: str) -> str:
    """Convert a snake_case string to kebab-case, replacing underscores with hyphens."""
    return value.replace("_", "-")

wrappers

Utilities for wrapping callables.

safe_call

safe_call[T, D](
    func: Callable[..., T],
    *,
    args: tuple[Any, ...] = (),
    kwargs: dict[str, Any] | None = None,
    default: D,
    exceptions: tuple[type[BaseException], ...] = ...,
) -> T | D
safe_call[T](
    func: Callable[..., T],
    *,
    args: tuple[Any, ...] = (),
    kwargs: dict[str, Any] | None = None,
    exceptions: tuple[type[BaseException], ...] = ...,
) -> T
safe_call(
    func: Callable[..., Any],
    *,
    args: tuple[Any, ...] = (),
    kwargs: dict[str, Any] | None = None,
    default: Any = MISSING,
    exceptions: tuple[type[BaseException], ...] = (
        Exception,
    ),
) -> Any

Call func, returning default if a caught exception is raised.

Parameters:

Name Type Description Default
func Callable[..., Any]

Callable to invoke.

required
args tuple[Any, ...]

Positional arguments forwarded to func.

()
kwargs dict[str, Any] | None

Keyword arguments forwarded to func.

None
default Any

Value to return when a caught exception is raised. If omitted, the exception propagates instead.

MISSING
exceptions tuple[type[BaseException], ...]

Exception types to catch. Defaults to (Exception,).

(Exception,)

Returns:

Type Description
Any

The return value of func(*args, **kwargs), or default if a

Any

caught exception is raised and default was provided.

Source code in src/pyrig_runtime/core/wrappers.py
def safe_call(
    func: Callable[..., Any],
    *,
    args: tuple[Any, ...] = (),
    kwargs: dict[str, Any] | None = None,
    default: Any = MISSING,
    exceptions: tuple[type[BaseException], ...] = (Exception,),
) -> Any:
    """Call `func`, returning `default` if a caught exception is raised.

    Args:
        func: Callable to invoke.
        args: Positional arguments forwarded to `func`.
        kwargs: Keyword arguments forwarded to `func`.
        default: Value to return when a caught exception is raised. If
            omitted, the exception propagates instead.
        exceptions: Exception types to catch. Defaults to `(Exception,)`.

    Returns:
        The return value of `func(*args, **kwargs)`, or `default` if a
        caught exception is raised and `default` was provided.
    """
    try:
        return func(*args, **(kwargs or {}))
    except exceptions:
        if default is MISSING:
            raise
        return default

rig

Convention-based namespace that dependent projects mirror to extend behavior.

cli

CLI subsystem for pyrig-runtime-based projects.

Dependent projects inherit a runnable CLI entry point and can extend it with project-specific commands alongside shared built-in commands.

cli

Typer CLI application builder with cross-package command discovery.

CLI

Bases: DependencySubclass

Typer application builder for pyrig-runtime-based projects.

Builds and runs the command-line application for any project that depends on pyrig-runtime. A dependent project may subclass CLI to override any step of the build to fit its needs.

app
app() -> Typer

Build a fully configured Typer application.

Source code in src/pyrig_runtime/rig/cli/cli.py
def app(self) -> typer.Typer:
    """Build a fully configured Typer application."""
    app = self.base_app()
    return self.build_app(app)
base_app
base_app() -> Typer

Create an empty base Typer application.

Returns:

Type Description
Typer

A new Typer app configured to show help when invoked without

Typer

arguments.

Source code in src/pyrig_runtime/rig/cli/cli.py
def base_app(self) -> typer.Typer:
    """Create an empty base Typer application.

    Returns:
        A new Typer app configured to show help when invoked without
        arguments.
    """
    return typer.Typer(**self.base_app_kwargs())
base_app_kwargs
base_app_kwargs() -> dict[str, Any]

Return keyword arguments for creating the base Typer application.

This base configuration makes sure that calling the CLI without any arguments will display the help message, and that the help text is the same as the description of the invoking project.

Returns:

Type Description
dict[str, Any]

A dictionary of keyword arguments to pass to typer.Typer.

Source code in src/pyrig_runtime/rig/cli/cli.py
def base_app_kwargs(self) -> dict[str, Any]:
    """Return keyword arguments for creating the base Typer application.

    This base configuration makes sure that calling the CLI without
    any arguments will display the help message, and that the help text
    is the same as the description of the invoking project.

    Returns:
        A dictionary of keyword arguments to pass to `typer.Typer`.
    """
    return {
        "no_args_is_help": True,
        "help": self.help_text(),
    }
build_app
build_app(app: Typer) -> Typer

Register the callback and all commands onto the given app.

Parameters:

Name Type Description Default
app Typer

The Typer app to populate.

required

Returns:

Type Description
Typer

The same app instance, now fully configured.

Source code in src/pyrig_runtime/rig/cli/cli.py
def build_app(self, app: typer.Typer) -> typer.Typer:
    """Register the callback and all commands onto the given app.

    Args:
        app: The Typer app to populate.

    Returns:
        The same app instance, now fully configured.
    """
    self.register_callback(app)
    self.register_subcommands(app)
    self.register_shared_subcommands(app)
    return app
callback
callback(
    verbose: Annotated[
        int,
        Option(
            --verbose,
            -v,
            count=True,
            help="Increase verbosity: -v (DEBUG), -vv (modules), -vvv (timestamps)",
        ),
    ] = 0,
    quiet: Annotated[
        int,
        Option(
            --quiet,
            -q,
            count=True,
            help="Decrease verbosity: -q (WARNING), -qq (ERROR), -qqq (CRITICAL)",
        ),
    ] = 0,
) -> None

Apply the verbosity options for the current invocation.

Parameters:

Name Type Description Default
verbose Annotated[int, Option(--verbose, -v, count=True, help='Increase verbosity: -v (DEBUG), -vv (modules), -vvv (timestamps)')]

Number of times verbosity was increased (e.g. via -v).

0
quiet Annotated[int, Option(--quiet, -q, count=True, help='Decrease verbosity: -q (WARNING), -qq (ERROR), -qqq (CRITICAL)')]

Number of times verbosity was decreased (e.g. via -q).

0
Source code in src/pyrig_runtime/rig/cli/cli.py
def callback(
    self,
    verbose: Annotated[
        int,
        typer.Option(
            "--verbose",
            "-v",
            count=True,
            help="Increase verbosity: -v (DEBUG), -vv (modules), -vvv (timestamps)",
        ),
    ] = 0,
    quiet: Annotated[
        int,
        typer.Option(
            "--quiet",
            "-q",
            count=True,
            help="Decrease verbosity: -q (WARNING), -qq (ERROR), -qqq (CRITICAL)",
        ),
    ] = 0,
) -> None:
    """Apply the verbosity options for the current invocation.

    Args:
        verbose: Number of times verbosity was increased (e.g. via `-v`).
        quiet: Number of times verbosity was decreased (e.g. via `-q`).
    """
    self.configure_logging(verbose, quiet)
configure_logging
configure_logging(verbose: int, quiet: int) -> None

Configure the logging level and format for the current invocation.

Each increment of verbose lowers the log level by one step (toward DEBUG); each increment of quiet raises it by one step (toward CRITICAL). The message format also expands as verbose increases: the level name is added first, then the module name, then a timestamp.

The resulting level is not clamped to the standard range, so a high enough verbose or quiet count can push it below DEBUG or above CRITICAL.

Parameters:

Name Type Description Default
verbose int

Number of times verbosity was increased (e.g. via -v).

required
quiet int

Number of times verbosity was decreased (e.g. via -q).

required
Note

Uses logging.basicConfig with force=True to ensure that the configuration is applied even if logging has already been configured by the calling project or other dependencies.

Source code in src/pyrig_runtime/rig/cli/cli.py
def configure_logging(self, verbose: int, quiet: int) -> None:
    """Configure the logging level and format for the current invocation.

    Each increment of `verbose` lowers the log level by one step (toward
    DEBUG); each increment of `quiet` raises it by one step (toward
    CRITICAL). The message format also expands as `verbose` increases:
    the level name is added first, then the module name, then a
    timestamp.

    The resulting level is not clamped to the standard range, so a high
    enough `verbose` or `quiet` count can push it below `DEBUG` or above
    `CRITICAL`.

    Args:
        verbose: Number of times verbosity was increased (e.g. via `-v`).
        quiet: Number of times verbosity was decreased (e.g. via `-q`).

    Note:
        Uses `logging.basicConfig` with `force=True` to ensure that the
        configuration is applied even if logging has already been configured
        by the calling project or other dependencies.
    """
    level = logging.INFO
    step = logging.INFO - logging.DEBUG
    level -= step * verbose
    level += step * quiet

    verbose_names = 1
    verbose_modules = verbose_names + 1
    verbose_timestamps = verbose_modules + 1

    if verbose >= verbose_timestamps:
        fmt = "%(asctime)s %(levelname)s [%(name)s] %(message)s"
    elif verbose == verbose_modules:
        fmt = "%(levelname)s [%(name)s] %(message)s"
    elif verbose == verbose_names:
        fmt = "%(levelname)s: %(message)s"
    else:
        fmt = "%(message)s"

    logging.basicConfig(level=level, format=fmt, force=True)
discovery_module classmethod
discovery_module() -> ModuleType

Return this module, keeping subclass discovery scoped to it alone.

Source code in src/pyrig_runtime/rig/cli/cli.py
@classmethod
def discovery_module(cls) -> ModuleType:
    """Return this module, keeping subclass discovery scoped to it alone."""
    return sys.modules[__name__]
help_text
help_text() -> str

Return the help text for the invoking project.

Returns:

Type Description
str

The project's distribution summary, or an empty string if no

str

metadata is available for it.

Source code in src/pyrig_runtime/rig/cli/cli.py
def help_text(self) -> str:
    """Return the help text for the invoking project.

    Returns:
        The project's distribution summary, or an empty string if no
        metadata is available for it.
    """
    metadata = distribution_metadata(
        distribution(self.project_name()),
    )
    if metadata is None:
        return ""
    return distribution_summary(metadata)
merge_key classmethod
merge_key() -> Hashable

Return a constant merge key.

There is only one CLI so different subclasses should converge into a single subclass Returns: The name of this class: CLI.

Source code in src/pyrig_runtime/rig/cli/cli.py
@classmethod
def merge_key(cls) -> Hashable:
    """Return a constant merge key.

    There is only one CLI so different subclasses should converge into
    a single subclass
    Returns:
        The name of this class: `CLI`.
    """
    return CLI.__name__
module_subcommand_groups
module_subcommand_groups(
    module: ModuleType,
) -> dict[str, Typer]

Return the Typer command groups found in a subcommands module.

Scans the module's namespace for typer.Typer instances. Both natively defined and imported instances are included. The key for each entry is the kebab-case form of the attribute name it is bound to.

Parameters:

Name Type Description Default
module ModuleType

The subcommands module to scan.

required

Returns:

Type Description
dict[str, Typer]

Mapping of kebab-case attribute name to the typer.Typer instance

dict[str, Typer]

bound to it.

Source code in src/pyrig_runtime/rig/cli/cli.py
def module_subcommand_groups(self, module: ModuleType) -> dict[str, typer.Typer]:
    """Return the Typer command groups found in a subcommands module.

    Scans the module's namespace for `typer.Typer` instances. Both natively
    defined and imported instances are included. The key for each entry is
    the kebab-case form of the attribute name it is bound to.

    Args:
        module: The subcommands module to scan.

    Returns:
        Mapping of kebab-case attribute name to the `typer.Typer` instance
        bound to it.
    """
    return {
        snake_to_kebab_case(name): obj
        for name, obj in vars(module).items()
        if isinstance(obj, typer.Typer)
    }
package_name
package_name() -> str

Return the snake_case package name of the invoking project.

For example, if the project is invoked as uv run my-project, the package name is my_project.

Source code in src/pyrig_runtime/rig/cli/cli.py
def package_name(self) -> str:
    """Return the snake_case package name of the invoking project.

    For example, if the project is invoked as `uv run my-project`, the
    package name is `my_project`.
    """
    return kebab_to_snake_case(self.project_name())
project_name
project_name() -> str

Return the stem of sys.argv[0] as the invoking project name.

When a project is invoked through a registered console-script entry point (e.g. uv run my-project), sys.argv[0] is the path to that script, so its stem is the project name as it was registered.

Source code in src/pyrig_runtime/rig/cli/cli.py
def project_name(self) -> str:
    """Return the stem of `sys.argv[0]` as the invoking project name.

    When a project is invoked through a registered console-script entry point
    (e.g. `uv run my-project`), `sys.argv[0]` is the path to that script, so
    its stem is the project name as it was registered.
    """
    return Path(sys.argv[0]).stem
register_callback
register_callback(app: Typer) -> None

Attach the verbosity callback to the given app.

Parameters:

Name Type Description Default
app Typer

The Typer app to attach the callback to.

required
Source code in src/pyrig_runtime/rig/cli/cli.py
def register_callback(self, app: typer.Typer) -> None:
    """Attach the verbosity callback to the given app.

    Args:
        app: The Typer app to attach the callback to.
    """
    app.callback()(self.callback)
register_direct_subcommands
register_direct_subcommands(
    app: Typer, module: ModuleType
) -> None

Register every function defined in a module as a top-level command.

Adds each function found directly in module to app as a flat Typer command. Imported functions are excluded.

Parameters:

Name Type Description Default
app Typer

The Typer app to register the commands onto.

required
module ModuleType

The subcommands module to scan for command functions.

required
Source code in src/pyrig_runtime/rig/cli/cli.py
def register_direct_subcommands(self, app: typer.Typer, module: ModuleType) -> None:
    """Register every function defined in a module as a top-level command.

    Adds each function found directly in `module` to `app` as a flat Typer
    command. Imported functions are excluded.

    Args:
        app: The Typer app to register the commands onto.
        module: The subcommands module to scan for command functions.
    """
    for func in module_functions(module):
        app.command()(func)
register_shared_subcommands
register_shared_subcommands(app: Typer) -> None

Discover and register shared commands from pyrig-runtime and its dependents.

Parameters:

Name Type Description Default
app Typer

The Typer app to register the commands onto.

required
Note

Commands are registered in dependency order (pyrig-runtime first, then dependent packages in topological order). When two packages define a command with the same name, the last registration takes precedence.

Source code in src/pyrig_runtime/rig/cli/cli.py
def register_shared_subcommands(self, app: typer.Typer) -> None:
    """Discover and register shared commands from pyrig-runtime and its dependents.

    Args:
        app: The Typer app to register the commands onto.

    Note:
        Commands are registered in dependency order (pyrig-runtime first,
        then dependent packages in topological order). When two packages
        define a command with the same name, the last registration takes
        precedence.
    """
    for shared_subcommands_module in chain(
        (shared_subcommands,),
        equivalent_modules_across_dependencies(
            shared_subcommands,
        ),
    ):
        self.register_direct_subcommands(app=app, module=shared_subcommands_module)
        self.register_subcommand_groups(app=app, module=shared_subcommands_module)
register_subcommand_groups
register_subcommand_groups(
    app: Typer, module: ModuleType
) -> None

Register every typer.Typer instance in a module as a named command group.

Attaches each typer.Typer found in the module's namespace to app, using the kebab-case form of the attribute name as the group name.

Parameters:

Name Type Description Default
app Typer

The Typer app to register the command groups onto.

required
module ModuleType

The subcommands module to scan for group instances.

required
Source code in src/pyrig_runtime/rig/cli/cli.py
def register_subcommand_groups(self, app: typer.Typer, module: ModuleType) -> None:
    """Register every `typer.Typer` instance in a module as a named command group.

    Attaches each `typer.Typer` found in the module's namespace to `app`,
    using the kebab-case form of the attribute name as the group name.

    Args:
        app: The Typer app to register the command groups onto.
        module: The subcommands module to scan for group instances.
    """
    for name, group in self.module_subcommand_groups(module).items():
        app.add_typer(group, name=name)
register_subcommands
register_subcommands(app: Typer) -> None

Discover and register project-specific commands for the invoking project.

Any dependent project can define its own CLI commands by adding functions or typer.Typer groups to <package>.rig.cli.subcommands.

Parameters:

Name Type Description Default
app Typer

The Typer app to register the commands onto.

required
Note

If the invoking project's subcommands module cannot be imported, registration is silently skipped.

Source code in src/pyrig_runtime/rig/cli/cli.py
def register_subcommands(self, app: typer.Typer) -> None:
    """Discover and register project-specific commands for the invoking project.

    Any dependent project can define its own CLI commands by adding
    functions or `typer.Typer` groups to `<package>.rig.cli.subcommands`.

    Args:
        app: The Typer app to register the commands onto.

    Note:
        If the invoking project's subcommands module cannot be imported,
        registration is silently skipped.
    """
    subcommands_module = replace_root_module(
        subcommands,
        root=self.package_name(),
        default=None,
    )

    if subcommands_module is None:
        return

    self.register_direct_subcommands(app=app, module=subcommands_module)
    self.register_subcommand_groups(app=app, module=subcommands_module)
run
run() -> None

Build and invoke the Typer application.

Source code in src/pyrig_runtime/rig/cli/cli.py
def run(self) -> None:
    """Build and invoke the Typer application."""
    self.app()()

commands

Implementations of CLI command actions for pyrig-runtime-based projects.

version

Version display command for pyrig-runtime-based project CLIs.

project_version
project_version() -> None

Print the name and installed version of the project running this CLI.

Reports the version of the project whose CLI is currently running. The project must be installed for its version to be available.

Source code in src/pyrig_runtime/rig/cli/commands/version.py
def project_version() -> None:
    """Print the name and installed version of the project running this CLI.

    Reports the version of the project whose CLI is currently running.
    The project must be installed for its version to be available.
    """
    project_name = CLI.I.project_name()
    typer.echo(f"{project_name} {version(project_name)}")

main

Console-script entry point for pyrig-runtime-based projects.

main
main() -> None

Run the CLI application.

Source code in src/pyrig_runtime/rig/cli/main.py
6
7
8
def main() -> None:
    """Run the CLI application."""
    CLI.I.run()

shared_subcommands

Shared CLI commands for all dependent packages.

In every installed pyrig-runtime based package functions defined directly in this module are registered as top-level CLI commands and module-level typer.Typer instances are registered as command groups, with each group's name derived from the kebab-case form of the variable name.

version
version() -> None

Print the name and installed version.

Reports the version of whichever project's CLI entry point was used to invoke this command. The project must be installed.

Example
$ uv run my-project version
my-project 0.4.1
Source code in src/pyrig_runtime/rig/cli/shared_subcommands.py
def version() -> None:
    """Print the name and installed version.

    Reports the version of whichever project's CLI entry point was used to
    invoke this command. The project must be installed.

    Example:
        ```
        $ uv run my-project version
        my-project 0.4.1
        ```
    """
    from pyrig_runtime.rig.cli.commands.version import project_version  # noqa: PLC0415

    project_version()

subcommands

Project-specific CLI commands.

Functions defined directly in this module are registered as top-level CLI commands. Module-level typer.Typer instances are registered as command groups, with each group's name derived from the kebab-case form of the variable name.