From f858be3901944561ce1c6ecaf167012fe78420db Mon Sep 17 00:00:00 2001 From: Yash Dhawan Date: Wed, 13 May 2026 20:12:32 +0530 Subject: [PATCH 1/2] feat: add Registry.display_as_tree() unicode tree output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a display_as_tree() method to Registry for debugging. Outputs resources as a unicode box-drawing tree grouped by URI prefix, e.g.: http://example.com/ - Resource(...) ├── foo/ - Resource(...) │ └── bar/ - Resource(...) http://example.org/ - Resource(...) Closes #17 --- referencing/_core.py | 61 ++++++++++++++++++++++++++++++++++ referencing/tests/test_core.py | 39 ++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/referencing/_core.py b/referencing/_core.py index 4206845..635e871 100644 --- a/referencing/_core.py +++ b/referencing/_core.py @@ -407,6 +407,67 @@ def __repr__(self) -> str: summary = f"{pluralized}" return f"" + 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 "" + + 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. diff --git a/referencing/tests/test_core.py b/referencing/tests/test_core.py index 3edddbc..9bbbaf4 100644 --- a/referencing/tests/test_core.py +++ b/referencing/tests/test_core.py @@ -575,6 +575,45 @@ def test_repr_one_resource(self): def test_repr_empty(self): assert repr(Registry()) == "" + def test_display_as_tree_empty(self): + """ + An empty registry displays as ''. + """ + assert Registry().display_as_tree() == "" + + 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): From 164e6395b5d38fd151ecee94bc7976427be13ae7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 14:42:53 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- referencing/_core.py | 29 ++++++++++++++++++++--------- referencing/tests/test_core.py | 27 +++++++++++++++++---------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/referencing/_core.py b/referencing/_core.py index 635e871..60b56e0 100644 --- a/referencing/_core.py +++ b/referencing/_core.py @@ -433,10 +433,7 @@ def display_as_tree(self) -> str: 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 - ): + if not any(uri != root and uri.startswith(root) for root in roots): roots.append(uri) def _render( @@ -449,19 +446,33 @@ def _render( 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 "") + child_prefix = prefix + ( + " " + if connector == "\u2514\u2500\u2500 " + else "\u2502 " + if connector + else "" + ) children = [ - u for u in all_uris + u + for u in all_uris if u != uri and u.startswith(uri) - and "/" not in u[len(uri):].rstrip("/") + 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) + 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="") diff --git a/referencing/tests/test_core.py b/referencing/tests/test_core.py index 9bbbaf4..c47c2e7 100644 --- a/referencing/tests/test_core.py +++ b/referencing/tests/test_core.py @@ -587,10 +587,12 @@ def test_display_as_tree_flat(self): """ 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), - ]) + 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 @@ -602,17 +604,22 @@ def test_display_as_tree_nested(self): 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), - ]) + 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:]) + assert any( + "\u251c\u2500\u2500" in line or "\u2514\u2500\u2500" in line + for line in lines[1:] + ) class TestResource: