diff --git a/docs/api/hierarchy.rst b/docs/api/hierarchy.rst index 799657c8d0..5e0a22507c 100644 --- a/docs/api/hierarchy.rst +++ b/docs/api/hierarchy.rst @@ -19,6 +19,7 @@ Groups (``zarr.hierarchy``) .. automethod:: visitkeys .. automethod:: visitvalues .. automethod:: visititems + .. automethod:: tree .. automethod:: create_group .. automethod:: require_group .. automethod:: create_groups diff --git a/notebooks/repr_tree.ipynb b/notebooks/repr_tree.ipynb new file mode 100644 index 0000000000..a45162d720 --- /dev/null +++ b/notebooks/repr_tree.ipynb @@ -0,0 +1,204 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import zarr\n", + "\n", + "# Python 2/3 trick\n", + "try:\n", + " unicode\n", + "except NameError:\n", + " unicode = str" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "g1 = zarr.group()\n", + "g3 = g1.create_group('bar')\n", + "g3.create_group('baz')\n", + "g5 = g3.create_group('quux')\n", + "g5.create_dataset('baz', shape=100, chunks=10)\n", + "g7 = g3.create_group('zoo')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/\n", + " +-- bar\n", + " +-- baz\n", + " +-- quux\n", + " | +-- baz[...]\n", + " +-- zoo\n" + ] + } + ], + "source": [ + "print(bytes(g1.tree()).decode())" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "bar\n", + " ├── baz\n", + " ├── quux\n", + " │ └── baz[...]\n", + " └── zoo\n" + ] + } + ], + "source": [ + "print(unicode(g3.tree()))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "
\n", + "\n", + "
\n" + ], + "text/plain": [ + "/\n", + " └── bar\n", + " ├── baz\n", + " ├── quux\n", + " │ └── baz[...]\n", + " └── zoo" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "g1.tree()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/requirements.txt b/requirements.txt index 28a7ceece4..10478995ee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +asciitree nose numpy fasteners diff --git a/requirements_dev.txt b/requirements_dev.txt index d273262177..f54c565d2e 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,5 +1,6 @@ appdirs==1.4.3 args==0.1.0 +asciitree==0.3.3 certifi==2017.7.27.1 chardet==3.0.4 clint==0.5.1 diff --git a/requirements_rtfd.txt b/requirements_rtfd.txt index 86091ed956..55730713f7 100644 --- a/requirements_rtfd.txt +++ b/requirements_rtfd.txt @@ -1,3 +1,4 @@ +asciitree setuptools setuptools_scm sphinx diff --git a/setup.py b/setup.py index 11523fd229..c420174a75 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ 'setuptools-scm>1.5.4' ], install_requires=[ + 'asciitree', 'numpy>=1.7', 'fasteners', 'numcodecs>=0.2.0', diff --git a/zarr/hierarchy.py b/zarr/hierarchy.py index dafbe4d238..2ead695c30 100644 --- a/zarr/hierarchy.py +++ b/zarr/hierarchy.py @@ -3,18 +3,17 @@ from collections import MutableMapping from itertools import islice - import numpy as np from zarr.compat import PY2 from zarr.attrs import Attributes from zarr.core import Array -from zarr.storage import (contains_array, contains_group, init_group, DictStore, group_meta_key, - attrs_key, listdir, rmdir) -from zarr.creation import (array, create, empty, zeros, ones, full, empty_like, zeros_like, - ones_like, full_like, normalize_store_arg) -from zarr.util import (normalize_storage_path, normalize_shape, InfoReporter, +from zarr.storage import (contains_array, contains_group, init_group, + DictStore, group_meta_key, attrs_key, listdir, rmdir) +from zarr.creation import (array, create, empty, zeros, ones, full, + empty_like, zeros_like, ones_like, full_like, normalize_store_arg) +from zarr.util import (normalize_storage_path, normalize_shape, InfoReporter, TreeViewer, is_valid_python_name, instance_dir) from zarr.errors import err_contains_array, err_contains_group, err_group_not_found, err_read_only from zarr.meta import decode_group_metadata @@ -62,6 +61,7 @@ class Group(MutableMapping): visitkeys visitvalues visititems + tree create_group require_group create_groups @@ -546,6 +546,34 @@ def visititems(self, func): base_len = len(self.name) return self.visitvalues(lambda o: func(o.name[base_len:].lstrip("/"), o)) + def tree(self): + """Provide a ``print``-able display of the hierarchy. + + Examples + -------- + >>> import zarr + >>> g1 = zarr.group() + >>> g2 = g1.create_group('foo') + >>> g3 = g1.create_group('bar') + >>> g4 = g3.create_group('baz') + >>> g5 = g3.create_group('quux') + >>> d1 = g5.create_dataset('baz', shape=100, chunks=10) + >>> print(g1.tree()) + / + ├── bar + │ ├── baz + │ └── quux + │ └── baz[...] + └── foo + >>> print(g3.tree()) + bar + ├── baz + └── quux + └── baz[...] + """ + + return TreeViewer(self) + def _write_op(self, f, *args, **kwargs): # guard condition diff --git a/zarr/tests/test_hierarchy.py b/zarr/tests/test_hierarchy.py index 48adc51a89..881767ea6b 100644 --- a/zarr/tests/test_hierarchy.py +++ b/zarr/tests/test_hierarchy.py @@ -4,6 +4,7 @@ import tempfile import atexit import shutil +import textwrap import os import pickle import warnings @@ -18,6 +19,7 @@ from zarr.storage import (DictStore, DirectoryStore, ZipStore, init_group, init_array, attrs_key, array_meta_key, group_meta_key, atexit_rmtree, NestedDirectoryStore) from zarr.core import Array +from zarr.compat import PY2, text_type from zarr.hierarchy import Group, group, open_group from zarr.attrs import Attributes from zarr.errors import PermissionError @@ -623,6 +625,109 @@ def visitor1(val, *args): eq(True, g1.visitvalues(visitor1)) eq(True, g1.visititems(visitor1)) + def test_tree(self): + # setup + g1 = self.create_group() + g2 = g1.create_group('foo') + g3 = g1.create_group('bar') + g3.create_group('baz') + g5 = g3.create_group('quux') + g5.create_dataset('baz', shape=100, chunks=10) + + # test + bg1 = textwrap.dedent(u"""\ + / + +-- bar + | +-- baz + | +-- quux + | +-- baz[...] + +-- foo""").encode() + eq(bg1, bytes(g1.tree())) + ug1 = textwrap.dedent(u"""\ + / + ├── bar + │ ├── baz + │ └── quux + │ └── baz[...] + └── foo""") + eq(ug1, text_type(g1.tree())) + sg1 = ug1 + if PY2: + sg1 = bg1 + eq(sg1, repr(g1.tree())) + hg1 = textwrap.dedent(u"""\ +
+ +
""") + eq(hg1, g1.tree()._repr_html_().split("")[1].strip()) + + bg2 = textwrap.dedent(u"""\ + foo""").encode() + eq(bg2, bytes(g2.tree())) + ug2 = textwrap.dedent(u"""\ + foo""") + eq(ug2, text_type(g2.tree())) + sg2 = ug2 + if PY2: + sg2 = bg2 + eq(sg2, repr(g2.tree())) + hg2 = textwrap.dedent(u"""\ +
+ +
""") + eq(hg2, g2.tree()._repr_html_().split("")[1].strip()) + + bg3 = textwrap.dedent(u"""\ + bar + +-- baz + +-- quux + +-- baz[...]""").encode() + eq(bg3, bytes(g3.tree())) + ug3 = textwrap.dedent(u"""\ + bar + ├── baz + └── quux + └── baz[...]""") + eq(ug3, text_type(g3.tree())) + sg3 = ug3 + if PY2: + sg3 = bg3 + eq(sg3, repr(g3.tree())) + hg3 = textwrap.dedent(u"""\ +
+ +
""") + eq(hg3, g3.tree()._repr_html_().split("")[1].strip()) + def test_empty_getitem_contains_iterators(self): # setup g = self.create_group() diff --git a/zarr/util.py b/zarr/util.py index 0333210575..a0b888d885 100644 --- a/zarr/util.py +++ b/zarr/util.py @@ -1,9 +1,11 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import, print_function, division import operator -from textwrap import TextWrapper +from textwrap import TextWrapper, dedent import numbers +from asciitree import BoxStyle, LeftAligned +from asciitree.traversal import Traversal import numpy as np @@ -306,6 +308,171 @@ def _repr_html_(self): return info_html_report(items) +class ZarrGroupTraversal(Traversal): + + def get_children(self, node): + return getattr(node, "values", lambda: [])() + + def get_root(self, tree): + return tree + + def get_text(self, node): + name = node.name.split("/")[-1] or "/" + name += "[...]" if hasattr(node, "dtype") else "" + return name + + +def custom_html_sublist(group, indent): + traverser = ZarrGroupTraversal(tree=group) + result = "" + + result += ( + """{0}
  • {1}
    """.format( + indent, traverser.get_text(group) + ) + ) + + children = traverser.get_children(group) + if children: + result += """\n{0}{0}\n{0}".format(indent) + + result += "
  • \n" + + return result + + +def custom_html_list(group, indent=" "): + result = "" + + # Add custom CSS style for our HTML list + result += """\n\n" + + # Insert the HTML list + result += """
    \n""" + result += "\n" + result += "
    \n" + + return result + + +class TreeViewer(object): + + def __init__(self, group): + self.group = group + + self.text_kwargs = dict( + horiz_len=2, + label_space=1, + indent=1 + ) + + self.bytes_kwargs = dict( + UP_AND_RIGHT="+", + HORIZONTAL="-", + VERTICAL="|", + VERTICAL_AND_RIGHT="+" + ) + + self.unicode_kwargs = dict( + UP_AND_RIGHT=u"\u2514", + HORIZONTAL=u"\u2500", + VERTICAL=u"\u2502", + VERTICAL_AND_RIGHT=u"\u251C" + ) + + def __bytes__(self): + drawer = LeftAligned( + traverse=ZarrGroupTraversal(), + draw=BoxStyle(gfx=self.bytes_kwargs, **self.text_kwargs) + ) + + result = drawer(self.group) + + # Unicode characters slip in on Python 3. + # So we need to straighten that out first. + if not PY2: + result = result.encode() + + return result + + def __unicode__(self): + drawer = LeftAligned( + traverse=ZarrGroupTraversal(), + draw=BoxStyle(gfx=self.unicode_kwargs, **self.text_kwargs) + ) + + return drawer(self.group) + + def __repr__(self): + if PY2: + return self.__bytes__() + else: + return self.__unicode__() + + def _repr_html_(self): + return custom_html_list(self.group) + + def check_array_shape(param, array, shape): if not hasattr(array, 'shape'): raise TypeError('parameter {!r}: expected an array-like object, got {!r}'