Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions referencing/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,78 @@ def __repr__(self) -> str:
summary = f"{pluralized}"
return f"<Registry ({size} {summary})>"

def display_as_tree(self) -> str:
"""
Return a unicode tree representation of the resources in this registry.

Useful for debugging what is contained in a registry. Resources are
grouped by their common URI prefixes and displayed using unicode
box-drawing characters.

Example output::

http://example.com/ – Resource(...)
├── foo/ – Resource(...)
│ ├── bar/ – Resource(...)
│ └── baz/ – Resource(...)
http://example.org/ – Resource(...)

"""
uris = sorted(self._resources)
if not uris:
return "<empty registry>"

lines: list[str] = []
# Track roots: top-level URIs that are not prefixes of each other
roots: list[str] = []
for uri in uris:
# A URI is a root if no existing root is a proper prefix of it
if not any(uri != root and uri.startswith(root) for root in roots):
roots.append(uri)

def _render(
uri: str,
all_uris: list[str],
prefix: str = "",
connector: str = "",
) -> None:
resource = self._resources.get(uri)
resource_repr = repr(resource) if resource is not None else "?"
lines.append(f"{prefix}{connector}{uri} \u2013 {resource_repr}")

child_prefix = prefix + (
" "
if connector == "\u2514\u2500\u2500 "
else "\u2502 "
if connector
else ""
)

children = [
u
for u in all_uris
if u != uri
and u.startswith(uri)
and "/" not in u[len(uri) :].rstrip("/")
]
children.sort()
for i, child in enumerate(children):
is_last = i == len(children) - 1
child_connector = (
"\u2514\u2500\u2500 " if is_last else "\u251c\u2500\u2500 "
)
_render(
child,
all_uris,
prefix=child_prefix,
connector=child_connector,
)

for root in roots:
_render(root, uris, prefix="", connector="")

return "\n".join(lines)

def get_or_retrieve(self, uri: URI) -> Retrieved[D, Resource[D]]:
"""
Get a resource from the registry, crawling or retrieving if necessary.
Expand Down
46 changes: 46 additions & 0 deletions referencing/tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,52 @@ def test_repr_one_resource(self):
def test_repr_empty(self):
assert repr(Registry()) == "<Registry (0 resources)>"

def test_display_as_tree_empty(self):
"""
An empty registry displays as '<empty registry>'.
"""
assert Registry().display_as_tree() == "<empty registry>"

def test_display_as_tree_flat(self):
"""
A flat set of unrelated URIs renders each on its own line.
"""
one = Resource.opaque(contents={"title": "one"})
two = Resource.opaque(contents={"title": "two"})
registry = Registry().with_resources(
[
("http://example.com/one", one),
("http://example.com/two", two),
]
)
tree = registry.display_as_tree()
assert "http://example.com/one" in tree
assert "http://example.com/two" in tree

def test_display_as_tree_nested(self):
"""
URIs sharing a common prefix are rendered as a nested tree.
"""
root = Resource.opaque(contents={})
child = Resource.opaque(contents={})
grandchild = Resource.opaque(contents={})
registry = Registry().with_resources(
[
("http://example.com/", root),
("http://example.com/foo/", child),
("http://example.com/foo/bar/", grandchild),
]
)
tree = registry.display_as_tree()
lines = tree.splitlines()
# Root appears first with no indentation connector
assert lines[0].startswith("http://example.com/")
# Nested children use box-drawing connectors
assert any(
"\u251c\u2500\u2500" in line or "\u2514\u2500\u2500" in line
for line in lines[1:]
)


class TestResource:
def test_from_contents_from_json_schema(self):
Expand Down
Loading