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 support for __asdf_traverse__ method to allow custom introspect… #1052

Merged
merged 8 commits into from
Jan 26, 2022
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
2.8.4 (unreleased)
------------------

- Added the capability for tag classes to provide an interface
to asdf info functionality to obtain information about the
class attributes rather than appear as an opaque class object.
[#1052]

- Fix tag listing when extension is not fully implemented. [#1034]

2.8.3 (2021-12-13)
Expand Down
29 changes: 24 additions & 5 deletions asdf/_display.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
"""
Utilities for displaying the content of an ASDF tree.

Normally these tools only will introspect dicts, lists, and primitive values
(with an exception for arrays). However, if the object that is generated
by the converter mechanism has a __asdf_traverse__() method, then it will
call that method expecting a dict or list to be returned. The method can
return what it thinks is suitable for display.
"""
import numpy as np

Expand Down Expand Up @@ -82,7 +88,9 @@ def from_root_node(cls, root_identifier, root_node):
next_nodes = []

for parent, identifier, node in current_nodes:
if (isinstance(node, dict) or isinstance(node, list) or isinstance(node, tuple)) and id(node) in seen:
if (isinstance(node, dict) or
isinstance(node, tuple) or
cls.supports_info(node)) and id(node) in seen:
info = _NodeInfo(parent, identifier, node, current_depth, recursive=True)
parent.children.append(info)
else:
Expand All @@ -92,8 +100,11 @@ def from_root_node(cls, root_identifier, root_node):
if parent is not None:
parent.children.append(info)
seen.add(id(node))

for child_identifier, child_node in get_children(node):
if cls.supports_info(node):
tnode = node.__asdf_traverse__()
else:
tnode = node
for child_identifier, child_node in get_children(tnode):
next_nodes.append((info, child_identifier, child_node))

if len(next_nodes) == 0:
Expand All @@ -105,8 +116,7 @@ def from_root_node(cls, root_identifier, root_node):
return root_info

def __init__(
self, parent, identifier, node, depth, recursive=False, visible=True
):
self, parent, identifier, node, depth, recursive=False, visible=True):
self.parent = parent
self.identifier = identifier
self.node = node
Expand All @@ -115,6 +125,15 @@ def __init__(
self.visible = visible
self.children = []

@classmethod
def supports_info(cls, node):
"""
This method determines if the node is an instance of a class that
supports introspection by the info machinery. This determined by
the presence of a __asdf_traverse__ method.
"""
return hasattr(node, "__asdf_traverse__")

@property
def visible_children(self):
return [c for c in self.children if c.visible]
Expand Down
46 changes: 46 additions & 0 deletions asdf/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,7 @@ def _assert_correct_info(node_or_path):
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),
Expand All @@ -557,6 +558,51 @@ def test_info_asdf_file(capsys, tmpdir):
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)
Expand Down
11 changes: 11 additions & 0 deletions docs/asdf/extending/extensions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,17 @@ an additional list of class names that previously identified the extension:
"foo_package.extensions.FooExtension",
]

.. _exposing_extension_object_internals:

Making converted object's contents visible to `info` and `search`
-----------------------------------------------------------------

If the object produced by the extension supports a class method
`.__asdf_traverse__` then it can be used by those tools to expose the contents
of the object. That method should accept no arguments and return either a
dict of attributes and their values, or a list if the object itself is
list-like.

.. _extending_extensions_installing:

Installing an extension
Expand Down
6 changes: 6 additions & 0 deletions docs/asdf/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,12 @@ For example, to show all top-level nodes and 5 of each's children:
The `AsdfFile.info` method behaves similarly to `asdf.info`, rendering
the tree of the associated `AsdfFile`.

Normally `asdf.info` will not show the contents of asdf nodes turned
into Python custom objects, but if that object supports a special
method, you may see the contents of such objects.
See :ref:`exposing_extension_object_internals` for how
to implement such support for `asdf.info` and `asdf.search`.

Searching the ASDF tree
=======================

Expand Down