Skip to content

Commit f844ce6

Browse files
committed
Added Group.tree method
1 parent 8a33df7 commit f844ce6

File tree

5 files changed

+154
-7
lines changed

5 files changed

+154
-7
lines changed

docs/tutorial.rst

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -330,10 +330,10 @@ representation of the hierarchy, e.g.::
330330

331331
>>> root.tree()
332332
/
333-
└── foo
334-
└── bar
335-
├── baz (10000, 10000) int32
336-
└── quux (10000, 10000) int32
333+
└── foo
334+
└── bar
335+
├── baz (10000, 10000) int32
336+
└── quux (10000, 10000) int32
337337

338338
The :func:`zarr.convenience.open` function provides a convenient way to create or
339339
re-open a group stored in a directory on the file-system, with sub-groups stored in
@@ -424,6 +424,12 @@ Groups also have the :func:`zarr.hierarchy.Group.tree` method, e.g.::
424424
├── bar (1000000,) int64
425425
└── baz (1000, 1000) float32
426426

427+
428+
.. note::
429+
430+
:func:`zarr.Group.tree` requires the optional `rich <https://rich.readthedocs.io/en/stable/>`_
431+
dependency. It can be installed with the ``[tree]`` extra.
432+
427433
If you're using Zarr within a Jupyter notebook (requires
428434
`ipytree <https://github.com/QuantStack/ipytree>`_), calling ``tree()`` will generate an
429435
interactive tree representation, see the `repr_tree.ipynb notebook

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ optional = [
101101
'lmdb',
102102
'universal-pathlib>=0.0.22',
103103
]
104+
tree = [
105+
"rich",
106+
]
104107

105108
[project.urls]
106109
"Bug Tracker" = "https://github.com/zarr-developers/zarr-python/issues"

src/zarr/_tree.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import io
2+
3+
from zarr.core.group import AsyncGroup
4+
5+
try:
6+
import rich
7+
import rich.console
8+
import rich.tree
9+
except ImportError as e:
10+
raise ImportError("'rich' is required for Group.tree") from e
11+
12+
13+
class TreeRepr:
14+
def __init__(self, tree: rich.tree.Tree) -> None:
15+
self.tree = tree
16+
17+
def __repr__(self) -> str:
18+
console = rich.console.Console(file=io.StringIO())
19+
console.print(self.tree)
20+
return str(console.file.getvalue())
21+
22+
23+
async def group_tree_async(group: AsyncGroup, max_depth: int | None = None) -> TreeRepr:
24+
tree = rich.tree.Tree(label=f"[b]{group.name}[/b]")
25+
nodes = {"": tree}
26+
members = sorted([x async for x in group.members(max_depth=max_depth)])
27+
28+
for key, node in members:
29+
if key.count("/") == 0:
30+
parent_key = ""
31+
else:
32+
parent_key = key.rsplit("/", 1)[0]
33+
parent = nodes[parent_key]
34+
35+
# We want what the spec calls the node "name", the part excluding all leading
36+
# /'s and path segments. But node.name includes all that, so we build it here.
37+
name = key.rsplit("/")[-1]
38+
if isinstance(node, AsyncGroup):
39+
label = f"[b]{name}[/b]"
40+
else:
41+
label = f"[b]{name}[/b] {node.shape} {node.dtype}"
42+
nodes[key] = parent.add(label)
43+
44+
return TreeRepr(tree)

src/zarr/core/group.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1270,8 +1270,30 @@ async def array_values(
12701270
async for _, array in self.arrays():
12711271
yield array
12721272

1273-
async def tree(self, expand: bool = False, level: int | None = None) -> Any:
1274-
raise NotImplementedError
1273+
async def tree(self, expand: bool | None = None, level: int | None = None) -> Any:
1274+
"""
1275+
Return a tree-like representation of a hierarchy.
1276+
1277+
This requires the optional ``rich`` dependency.
1278+
1279+
Parameters
1280+
----------
1281+
expand : bool, optional
1282+
This keyword is not yet supported. A NotImplementedError is raised if
1283+
it's used.
1284+
level : int, optional
1285+
The maximum depth below this Group to display in the tree.
1286+
1287+
Returns
1288+
-------
1289+
TreeRepr
1290+
A pretty-printable object displaying the hierarchy.
1291+
"""
1292+
from zarr._tree import group_tree_async
1293+
1294+
if expand is not None:
1295+
raise NotImplementedError("'expanded' is not yet implemented.")
1296+
return await group_tree_async(self, max_depth=level)
12751297

12761298
async def empty(
12771299
self, *, name: str, shape: ChunkCoords, **kwargs: Any
@@ -1504,7 +1526,25 @@ def array_values(self) -> Generator[Array, None]:
15041526
for _, array in self.arrays():
15051527
yield array
15061528

1507-
def tree(self, expand: bool = False, level: int | None = None) -> Any:
1529+
def tree(self, expand: bool | None = None, level: int | None = None) -> Any:
1530+
"""
1531+
Return a tree-like representation of a hierarchy.
1532+
1533+
This requires the optional ``rich`` dependency.
1534+
1535+
Parameters
1536+
----------
1537+
expand : bool, optional
1538+
This keyword is not yet supported. A NotImplementedError is raised if
1539+
it's used.
1540+
level : int, optional
1541+
The maximum depth below this Group to display in the tree.
1542+
1543+
Returns
1544+
-------
1545+
TreeRepr
1546+
A pretty-printable object displaying the hierarchy.
1547+
"""
15081548
return self._sync(self._async_group.tree(expand=expand, level=level))
15091549

15101550
def create_group(self, name: str, **kwargs: Any) -> Group:

tests/test_tree.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import textwrap
2+
from typing import Any
3+
4+
import pytest
5+
6+
import zarr
7+
8+
9+
@pytest.mark.parametrize("root_name", [None, "root"])
10+
def test_tree(root_name: Any) -> None:
11+
g = zarr.group(path=root_name)
12+
A = g.create_group("A")
13+
B = g.create_group("B")
14+
C = B.create_group("C")
15+
D = C.create_group("C")
16+
17+
A.create_array(name="x", shape=(2), dtype="float64")
18+
A.create_array(name="y", shape=(0,), dtype="int8")
19+
B.create_array(name="x", shape=(0,))
20+
C.create_array(name="x", shape=(0,))
21+
D.create_array(name="x", shape=(0,))
22+
23+
result = repr(g.tree())
24+
root = root_name or ""
25+
26+
expected = textwrap.dedent(f"""\
27+
/{root}
28+
├── A
29+
│ ├── x (2,) float64
30+
│ └── y (0,) int8
31+
└── B
32+
├── C
33+
│ ├── C
34+
│ │ └── x (0,) float64
35+
│ └── x (0,) float64
36+
└── x (0,) float64
37+
""")
38+
39+
assert result == expected
40+
41+
result = repr(g.tree(level=0))
42+
expected = textwrap.dedent(f"""\
43+
/{root}
44+
├── A
45+
└── B
46+
""")
47+
48+
assert result == expected
49+
50+
51+
def test_expand_not_implemented() -> None:
52+
g = zarr.group()
53+
with pytest.raises(NotImplementedError):
54+
g.tree(expand=True)

0 commit comments

Comments
 (0)