"""
Reference Paths
===============
When inspecting a JITX design tree, for example using
:py:func:`~jitx.inspect.visit`, the path to the element is returned as a
:py:class:`RefPath` object. This is construced as a tuple of strings, integers,
and :py:class:`Item` objects, which represent attribute access, list index
access, and mapping lookups, respectively, describing how to get from the
reference object to the element being visited.
RefPaths are normally only used for more advanced introspection use-cases and
debugging, and in most cases, a designer would not need to interact with them
directly.
>>> class A(Circuit):
... power = Power()
... awkward = {"a": [Port()]}
>>> circuit = A()
>>> for trace, elem in visit(circuit, Port):
... print(trace.path, "--", repr(elem))
power -- RefPath(("power"))
power.Vp -- RefPath(("power", "Vp"))
power.Vn -- RefPath(("power", "Vn"))
awkward["a"][0] -- RefPath(("awkward", Item("a"), 0))
"""
from __future__ import annotations
from collections.abc import Iterable, Sequence
from io import StringIO
from itertools import chain
from typing import overload
[docs]
class Item:
"""A RefPath Item represents a dictionary or mapping lookup, as opposed to
an attribute lookup or list index, which are represented by strings and
integers, respectively."""
def __init__(self, value: int | str):
self.value = value
def __str__(self):
return repr(self.value)
def __repr__(self):
return f"Item({repr(self.value)})"
def __hash__(self) -> int:
return hash(self.value)
def __eq__(self, other):
if self is other:
return True
elif isinstance(other, Item):
return self.value == other.value
else:
return self.value == other
[docs]
class RefPath(Sequence[str | int | Item]):
"""The path to an element in the design tree. It's made up of a tuple of
strings, integers, and :py:class:`Item` objects, which represent attribute
access, list index access, and mapping lookups, respectively, for
traversing a path through a design tree.
Supports some arithmetic operations:
- ``path1 + tuple`` : Concatenate path with a tuple of steps.
- ``path1 - path2`` : Return a new ``RefPath`` that is the path of
``path1`` relative to ``path2``. If ``path2`` is not a prefix of ``path1``,
``path1`` is returned. If paths are identical or ``path1`` is a parent,
returns empty path. If paths are diverging, the result is undefined.
"""
def __init__(self, steps: Iterable[str | int | Item] = ()):
self.steps = tuple(steps)
def __len__(self):
return len(self.steps)
def __iter__(self):
return iter(self.steps)
def __eq__(self, other):
if self is other:
return True
elif isinstance(other, str):
# TODO parse other instead, and compare properly
return str(self) == other
elif isinstance(other, RefPath):
return self.steps == other.steps
else:
return NotImplemented
@overload
def __getitem__(self, key: int) -> str | int | Item: ...
@overload
def __getitem__(self, key: slice) -> RefPath: ...
def __getitem__(self, key):
if isinstance(key, slice):
return RefPath(self.steps[key])
return self.steps[key]
def __add__(self, other: Iterable[str | int | Item] | str | int | Item):
if isinstance(other, str | int | Item):
return RefPath(chain(self, (other,)))
elif isinstance(other, Iterable):
return RefPath(chain(self, other))
else:
return NotImplemented
def __radd__(self, other: Iterable[str | int | Item] | str | int | Item):
if isinstance(other, str | int | Item):
return RefPath(chain((other,), self))
elif isinstance(other, Iterable):
return RefPath(chain(other, self))
else:
return NotImplemented
def __sub__(self, other: RefPath):
i = 0
N = min(len(self), len(other))
while i < N:
if self[i] != other[i]:
return self[i:]
i += 1
return self[i:]
[docs]
def attribute(self) -> RefPath:
"""Return a new ``RefPath`` that is the path to the last attribute in
the path, effectively stripping off trailing index and mapping lookups."""
for i in range(len(self) - 1, 0, -1):
if type(self[i]) is str:
return self[i:]
return self
def __str__(self):
b = StringIO()
chained = False
for p in self:
if isinstance(p, Item) or not isinstance(p, str):
b.write(f"[{p}]")
else:
if chained:
b.write(".")
b.write(p)
chained = True
return b.getvalue()
def __repr__(self):
return f"RefPath({self.steps!r})"