Source code for jitx.inspect

"""
JITX Object Introspection
=========================

The JITX object introspection is based on traversing the design tree using the
:py:func:`~jitx.inspect.visit` function. Other utility and helper functions are
also provided but they all rely on the :py:func:`~jitx.inspect.visit` function
to do the traversal.
"""

from __future__ import annotations

from collections.abc import Callable, Generator
from dataclasses import dataclass
from types import UnionType
from typing import get_args, get_origin, overload, Any

from .container import Composite
from .transform import IDENTITY, Transform
from .placement import Kinematic, Placement

from ._structural import Proxy, Ref, Structural, RefPath as RefPath, traverse_base


_empty_path = RefPath()


@overload
def visit[T: UnionType](
    root,
    types: T,
    /,
    through: tuple[type, ...] | UnionType | None = None,
    *,
    path: RefPath = _empty_path,
    transform: Transform | None = IDENTITY,
    opaque: tuple[type, ...] | UnionType | None = None,
    refs: bool = False,
    filter: Callable[[T], bool] | None = None,
) -> Generator[tuple[Trace, T], None, None]: ...


@overload
def visit[T](
    root,
    type: type[T] | tuple[type[T], ...],
    /,
    through: tuple[type, ...] | UnionType | None = None,
    *,
    path: RefPath = _empty_path,
    transform: Transform | None = IDENTITY,
    opaque: tuple[type, ...] | UnionType | None = None,
    refs: bool = False,
    filter: Callable[[T], bool] | None = None,
) -> Generator[tuple[Trace, T], None, None]: ...


[docs] def visit( root, types: type | tuple[type, ...] | UnionType, /, through: tuple[type, ...] | UnionType | None = None, *, path: RefPath = _empty_path, transform: Transform | None = IDENTITY, opaque: tuple[type, ...] | UnionType | None = None, refs: bool = False, filter: Callable[[Any], bool] | None = None, ): """Find elements in an object structure. Objects of the specified ``types`` will be returned, traversing down through the object tree structure. If ``through`` is specified the traversal will only pass through those when looking for matching elements, otherwise all JITX structural elements will be traversed. Args: root: The root object to start traversing types: A type, tuple of types, or union of types to find through: Optional argument to limit which objects to recurse through. If undefined, only :py:class:`~jitx._structural.Structural` elements will be visited. path: Optional starting path transform: Optional starting transform, if unset will default to the identity transform. Set to None to suppress computing transforms. opaque: Optional argument to limit which objects to recurse through. If unspecified, or None, contents of :py:class:`~jitx._structural.Ref` elements will be skipped. To recurse through all structural elements, including Ref elements, set this to an empty tuple. filter: Optional filter function to apply to the elements. Only elements that pass the filter will be returned. Yields: Pairs of :py:class:`~Trace` to the element, and the element itself. """ if through is None: through = (Structural,) elif isinstance(through, UnionType): through = get_args(through) if transform is not None: # make sure we stop at these to grab the transform through = through + (Composite,) def normalize_types(ts: type | tuple[type, ...] | UnionType): if isinstance(ts, type): ts = (ts,) elif isinstance(ts, UnionType): ts = get_args(ts) elif get_origin(ts) is not None: origin_type = get_origin(ts) if isinstance(origin_type, type): ts = (origin_type,) else: raise ValueError("Unsupported visit type: {ts}") ts = tuple(get_origin(t) or t for t in ts) for t in ts: if not isinstance(t, type): raise ValueError("Unsupported visit type: {t}") return ts types = normalize_types(types) if opaque is None: opaque = (Ref,) else: opaque = normalize_types(opaque) + (Ref,) checktypes = through + types visited = set() def _visit( root_path: RefPath, ob: Any, xform: Transform | None, trace: Trace | None, id_path: set[int], ): for path, elem in traverse_base( ob, checktypes, (), root_path, refs=refs, opaque=opaque ): if refs: # use id to bypass unhashable issues if id(elem) in visited: continue visited.add(id(elem)) xf = xform # isinstance(Kinematic) for proxies doesn't work, since Kinematic # doesn't use that meta class. if xf is not None and issubclass(Proxy.type(elem), Kinematic): transform = elem.transform if transform is not None: xf = xf * transform if issubclass(Proxy.type(elem), types): # do not use xf here, as it'll have elem's transform applied if filter is None or filter(elem): yield Trace(path, xform, ob, trace), elem if issubclass(Proxy.type(elem), through) and not issubclass( Proxy.type(elem), opaque ): if id(elem) in id_path: raise ValueError(f"Cycle detected for object {ob} at {path}") id_path.add(id(elem)) yield from _visit( path, elem, xf, Trace(path, xform, ob, trace), id_path ) id_path.remove(id(elem)) return _visit(path, root, transform, Trace(path, transform, None, None), set())
[docs] @dataclass class Trace: """The Trace inspection object represents a path through the design tree to reach a particular element, it is returned by the :py:func:`visit` function.""" path: RefPath """The path to the element.""" transform: Transform | Placement | None """The accumulated transform leading up to this element, which may be a :py:class:`Placement`, or None if one could not be determined. Note that the element's own transform, if it has one, is not included in this transform.""" parent: Any = None """The parent structural element.""" trace: Trace | None = None """The trace to the parent object, if any."""
@overload def decompose[T]( ob, type: type[T], /, *, refs=False, filter: Callable[[T], bool] | None = None, ) -> Generator[T, None, None]: ... @overload def decompose( ob, types: tuple[type, ...] | UnionType, /, *, refs=False, filter: Callable[[Any], bool] | None = None, ) -> Generator[Any, None, None]: ...
[docs] def decompose( ob, types: type | tuple[type, ...] | UnionType, /, *, refs=False, filter: Callable[[Any], bool] | None = None, ): """Collect fields of a type or types into a sequence that can be used for deconstruction assignment. The elements are all semi-shallow children, traversing lists and dicts, but not other structural elements. Note that objects of subtypes can be returned as well. >>> class A: ... other = "field" ... ports = [Port(), DiffPair()] ... another = GPIO() >>> p1, p2, p3 = decompose(A(), Port) >>> type(p1) is Port and type(p2) is DiffPair and type(p3) is GPIO True """ return extract(ob, types, through=(), refs=refs, filter=filter)
@overload def extract[T]( ob, type: type[T], /, through: tuple[type, ...] | UnionType | None = None, *, opaque: tuple[type, ...] | UnionType | None = None, refs=False, filter: Callable[[T], bool] | None = None, ) -> Generator[T, None, None]: ... @overload def extract( ob, types: tuple[type, ...] | UnionType, /, through: tuple[type, ...] | UnionType | None = None, *, opaque: tuple[type, ...] | UnionType | None = None, refs=False, filter: Callable[[Any], bool] | None = None, ) -> Generator[Any, None, None]: ...
[docs] def extract( ob, types: type | tuple[type, ...] | UnionType, /, through: tuple[type, ...] | UnionType | None = None, *, opaque: tuple[type, ...] | UnionType | None = None, refs=False, filter: Callable[[Any], bool] | None = None, ): """Recursively traverse all objects in the tree and yield all objects of a particular type or types. This is similar to :py:func:`~visit`, but does not generate the meta information associated with each returned element. >>> def component_ports(comp:Component) -> list[Port]: ... return list(extract(comp, Port)) """ for _, elem in visit( ob, types, through, transform=None, opaque=opaque, filter=filter, refs=refs ): yield elem