Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added extraction of title from schema to show as a comment in info() #1138

Merged
merged 15 commits into from
May 24, 2022
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
2.11.2
------

- Added ability to display title as a comment in using the
``info()`` functionality. [#1138]

2.11.1 (2022-04-15)
-------------------

Expand Down
81 changes: 78 additions & 3 deletions asdf/_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
call that method expecting a dict or list to be returned. The method can
return what it thinks is suitable for display.
"""
import re

import numpy as np

from .schema import load_schema
from .tags.core.ndarray import NDArrayType
from .treeutil import get_children
from .util import is_primitive
Expand All @@ -28,6 +31,8 @@
DEFAULT_MAX_COLS = 120
DEFAULT_SHOW_VALUES = True

EXTENSION_MANAGER = None


def render_tree(
node,
Expand All @@ -36,17 +41,22 @@ def render_tree(
show_values=DEFAULT_SHOW_VALUES,
filters=[],
identifier="root",
refresh_extension_manager=False,
):
"""
Render a tree as text with indents showing depth.
"""
info = _NodeInfo.from_root_node(identifier, node)
info = _NodeInfo.from_root_node(identifier, node, refresh_extension_manager=refresh_extension_manager)

if len(filters) > 0:
if not _filter_tree(info, filters):
return []

renderer = _TreeRenderer(max_rows, max_cols, show_values)
renderer = _TreeRenderer(
max_rows,
max_cols,
show_values,
)
return renderer.render(info)


Expand Down Expand Up @@ -75,18 +85,55 @@ def _format_code(value, code):
return "\x1B[{}m{}\x1B[0m".format(code, value)


def _get_schema_for_property(schema, attr):
subschema = schema.get("properties", {}).get(attr, None)
if subschema is not None:
return subschema
for combiner in ["allOf", "anyOf", "oneOf"]:
for subschema in schema.get(combiner, []):
subsubschema = _get_schema_for_property(subschema, attr)
if subsubschema != {}:
return subsubschema
subschema = schema.get("properties", {}).get("patternProperties", None)
if subschema:
for key in subschema:
if re.search(key, attr):
return subschema[key]
return {}


def _get_schema_for_index(schema, i):
items = schema.get("items", {})
if isinstance(items, list):
if i >= len(items):
return {}
else:
return items[i]
else:
return items


class _NodeInfo:
"""
Container for a node, its state of visibility, and values used to display it.
"""

@classmethod
def from_root_node(cls, root_identifier, root_node):
def from_root_node(cls, root_identifier, root_node, schema=None, refresh_extension_manager=False):
"""
Build a _NodeInfo tree from the given ASDF root node.
Intentionally processes the tree in breadth-first order so that recursively
referenced nodes are displayed at their shallowest reference point.
"""
from .asdf import AsdfFile, get_config
from .extension import ExtensionManager

af = AsdfFile()
if refresh_extension_manager:
config = get_config()
af._extension_manager = ExtensionManager(config.extensions)
extmgr = af.extension_manager

current_nodes = [(None, root_identifier, root_node)]
seen = set()
root_info = None
Expand All @@ -103,14 +150,32 @@ def from_root_node(cls, root_identifier, root_node):
if root_info is None:
root_info = info
if parent is not None:
if parent.schema is not None and not cls.supports_info(node):
# Extract subschema if it exists
subschema = _get_schema_for_property(parent.schema, identifier)
info.schema = subschema
info.title = subschema.get("title", None)
if parent is None and schema is not None:
info.schema = schema
info.title = schema.get("title, None")
parent.children.append(info)
seen.add(id(node))
if cls.supports_info(node):
tnode = node.__asdf_traverse__()
# Look for a title for the attribute if it is a tagged object
tag = node._tag
tagdef = extmgr.get_tag_definition(tag)
schema_uri = tagdef.schema_uris[0]
schema = load_schema(schema_uri)
info.schema = schema
info.title = schema.get("title", None)
else:
tnode = node
if parent is None:
info.schema = schema
for child_identifier, child_node in get_children(tnode):
next_nodes.append((info, child_identifier, child_node))
# extract subschema if appropriate

if len(next_nodes) == 0:
break
Expand All @@ -128,6 +193,8 @@ def __init__(self, parent, identifier, node, depth, recursive=False, visible=Tru
self.recursive = recursive
self.visible = visible
self.children = []
self.title = None
self.schema = None

@classmethod
def supports_info(cls, node):
Expand Down Expand Up @@ -266,6 +333,12 @@ def _mark_visible_tuple(self, root_info):
def _render(self, info, active_depths, is_tail):
"""
Render the tree. Called recursively on child nodes.

is_tail indicates if the child is the last of the children,
needed to indicate the proper connecting character in the tree
display. Likewise, active_depths is used to track which preceeding
depths are incomplete thus need continuing lines preceding in
the tree display.
"""
lines = []

Expand Down Expand Up @@ -306,6 +379,8 @@ def _render_node(self, info, active_depths, is_tail):
else:
line = "{}{} {}".format(prefix, format_bold(info.identifier), value)

if info.title is not None:
line = line + format_faint(format_italic(" # " + info.title))
visible_children = info.visible_children
if len(visible_children) == 0 and len(info.children) > 0:
line = line + format_italic(" ...")
Expand Down
9 changes: 8 additions & 1 deletion asdf/asdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -1510,6 +1510,7 @@ def info(
max_rows=display.DEFAULT_MAX_ROWS,
max_cols=display.DEFAULT_MAX_COLS,
show_values=display.DEFAULT_SHOW_VALUES,
refresh_extension_manager=False,
):
"""
Print a rendering of this file's tree to stdout.
Expand All @@ -1534,8 +1535,14 @@ def info(
Set to False to disable display of primitive values in
the rendered tree.
"""

lines = display.render_tree(
self.tree, max_rows=max_rows, max_cols=max_cols, show_values=show_values, identifier="root"
self.tree,
max_rows=max_rows,
max_cols=max_cols,
show_values=show_values,
identifier="root",
refresh_extension_manager=refresh_extension_manager,
)
print("\n".join(lines))

Expand Down
115 changes: 0 additions & 115 deletions asdf/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,121 +484,6 @@ def test_get_default_resolver():
assert result == "http://stsci.edu/schemas/asdf/core/ndarray-1.0.0"


def test_info_module(capsys, tmpdir):
tree = dict(foo=42, bar="hello", baz=np.arange(20), nested={"woo": "hoo", "yee": "haw"}, long_line="a" * 100)
af = asdf.AsdfFile(tree)

def _assert_correct_info(node_or_path):
asdf.info(node_or_path)
captured = capsys.readouterr()
assert "foo" in captured.out
assert "bar" in captured.out
assert "baz" in captured.out

_assert_correct_info(af)
_assert_correct_info(af.tree)

tmpfile = str(tmpdir.join("written.asdf"))
af.write_to(tmpfile)
af.close()

_assert_correct_info(tmpfile)
_assert_correct_info(pathlib.Path(tmpfile))

for i in range(1, 10):
asdf.info(af, max_rows=i)
lines = capsys.readouterr().out.strip().split("\n")
assert len(lines) <= i

asdf.info(af, max_cols=80)
assert "(truncated)" in capsys.readouterr().out
asdf.info(af, max_cols=None)
captured = capsys.readouterr().out
assert "(truncated)" not in captured
assert "a" * 100 in captured

asdf.info(af, show_values=True)
assert "hello" in capsys.readouterr().out
asdf.info(af, show_values=False)
assert "hello" not in capsys.readouterr().out

tree = {"foo": ["alpha", "bravo", "charlie", "delta", "eagle"]}
af = asdf.AsdfFile(tree)
asdf.info(af, max_rows=(None,))
assert "alpha" not in capsys.readouterr().out
for i in range(1, 5):
asdf.info(af, max_rows=(None, i))
captured = capsys.readouterr()
for val in tree["foo"][0 : i - 1]:
assert val in captured.out
for val in tree["foo"][i - 1 :]:
assert val not in captured.out


def test_info_asdf_file(capsys, tmpdir):
tree = dict(foo=42, bar="hello", baz=np.arange(20), nested={"woo": "hoo", "yee": "haw"}, long_line="a" * 100)
af = asdf.AsdfFile(tree)
af.info()
captured = capsys.readouterr()
assert "foo" in captured.out
assert "bar" in captured.out
assert "baz" in captured.out


class ObjectWithInfoSupport:
def __init__(self):
self._tag = "foo"

def __asdf_traverse__(self):
return {"the_meaning_of_life_the_universe_and_everything": 42, "clown": "Bozo"}


def test_info_object_support(capsys):
tree = dict(random=3.14159, object=ObjectWithInfoSupport())
af = asdf.AsdfFile(tree)
af.info()
captured = capsys.readouterr()
assert "the_meaning_of_life_the_universe_and_everything" in captured.out
assert "clown" in captured.out
assert "42" in captured.out
assert "Bozo" in captured.out


class RecursiveObjectWithInfoSupport:
def __init__(self):
self._tag = "foo"
self.the_meaning = 42
self.clown = "Bozo"
self.recursive = None

def __asdf_traverse__(self):
return {"the_meaning": self.the_meaning, "clown": self.clown, "recursive": self.recursive}


def test_recursive_info_object_support(capsys):
recursive_obj = RecursiveObjectWithInfoSupport()
recursive_obj.recursive = recursive_obj
tree = dict(random=3.14159, rtest=recursive_obj)
af = asdf.AsdfFile(tree)
af.info()
captured = capsys.readouterr()
assert "recursive reference" in captured.out


def test_search():
tree = dict(foo=42, bar="hello", baz=np.arange(20))
af = asdf.AsdfFile(tree)

result = af.search("foo")
assert result.node == 42

result = af.search(type="ndarray")
assert (result.node == tree["baz"]).all()

result = af.search(value="hello")
assert result.node == "hello"


def test_history_entries(tmpdir):
path = str(tmpdir.join("test.asdf"))
message = "Twas brillig, and the slithy toves"
Expand Down
Loading