Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix issue #1882: info not using custom extensions
Browse files Browse the repository at this point in the history
braingram committed Dec 20, 2024
1 parent f15a556 commit 22728e7
Showing 4 changed files with 283 additions and 233 deletions.
2 changes: 2 additions & 0 deletions asdf/_asdf.py
Original file line number Diff line number Diff line change
@@ -1408,6 +1408,7 @@ def schema_info(self, key="description", path=None, preserve_list=True, refresh_
self.tree,
preserve_list=preserve_list,
refresh_extension_manager=refresh_extension_manager,
extension_manager=self.extension_manager,
)

def info(
@@ -1448,6 +1449,7 @@ def info(
show_values=show_values,
identifier="root",
refresh_extension_manager=refresh_extension_manager,
extension_manager=self.extension_manager,
)
print("\n".join(lines))

2 changes: 2 additions & 0 deletions asdf/_display.py
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ def render_tree(
filters=None,
identifier="root",
refresh_extension_manager=False,
extension_manager=None,
):
"""
Render a tree as text with indents showing depth.
@@ -49,6 +50,7 @@ def render_tree(
identifier=identifier,
filters=[] if filters is None else filters,
refresh_extension_manager=refresh_extension_manager,
extension_manager=extension_manager,
)
if info is None:
return []
28 changes: 22 additions & 6 deletions asdf/_node_info.py
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ def _filter_tree(info, filters):
return len(info.children) > 0 or all(f(info.node, info.identifier) for f in filters)


def create_tree(key, node, identifier="root", filters=None, refresh_extension_manager=False):
def create_tree(key, node, identifier="root", filters=None, refresh_extension_manager=False, extension_manager=None):
"""
Create a `NodeSchemaInfo` tree which can be filtered from a base node.
@@ -46,6 +46,7 @@ def create_tree(key, node, identifier="root", filters=None, refresh_extension_ma
identifier,
node,
refresh_extension_manager=refresh_extension_manager,
extension_manager=extension_manager,
)

if len(filters) > 0 and not _filter_tree(schema_info, filters):
@@ -62,6 +63,7 @@ def collect_schema_info(
filters=None,
preserve_list=True,
refresh_extension_manager=False,
extension_manager=None,
):
"""
Collect from the underlying schemas any of the info stored under key, relative to the path
@@ -91,6 +93,7 @@ def collect_schema_info(
identifier=identifier,
filters=[] if filters is None else filters,
refresh_extension_manager=refresh_extension_manager,
extension_manager=extension_manager,
)

info = schema_info.collect_info(preserve_list=preserve_list)
@@ -181,7 +184,7 @@ class NodeSchemaInfo:
The portion of the underlying schema corresponding to the node.
"""

def __init__(self, key, parent, identifier, node, depth, recursive=False, visible=True):
def __init__(self, key, parent, identifier, node, depth, recursive=False, visible=True, extension_manager=None):
self.key = key
self.parent = parent
self.identifier = identifier
@@ -191,6 +194,7 @@ def __init__(self, key, parent, identifier, node, depth, recursive=False, visibl
self.visible = visible
self.children = []
self.schema = None
self.extension_manager = extension_manager or _get_extension_manager()

@classmethod
def traversable(cls, node):
@@ -246,13 +250,15 @@ def set_schema_from_node(self, node, extension_manager):
self.schema = schema

@classmethod
def from_root_node(cls, key, root_identifier, root_node, schema=None, refresh_extension_manager=False):
def from_root_node(
cls, key, root_identifier, root_node, schema=None, refresh_extension_manager=False, extension_manager=None
):
"""
Build a NodeSchemaInfo 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.
"""
extension_manager = _get_extension_manager(refresh_extension_manager)
extension_manager = extension_manager or _get_extension_manager(refresh_extension_manager)

current_nodes = [(None, root_identifier, root_node)]
seen = set()
@@ -263,11 +269,21 @@ def from_root_node(cls, key, root_identifier, root_node, schema=None, refresh_ex

for parent, identifier, node in current_nodes:
if (isinstance(node, (dict, tuple)) or cls.traversable(node)) and id(node) in seen:
info = NodeSchemaInfo(key, parent, identifier, node, current_depth, recursive=True)
info = NodeSchemaInfo(
key,
parent,
identifier,
node,
current_depth,
recursive=True,
extension_manager=extension_manager,
)
parent.children.append(info)

else:
info = NodeSchemaInfo(key, parent, identifier, node, current_depth)
info = NodeSchemaInfo(
key, parent, identifier, node, current_depth, extension_manager=extension_manager
)

if root_info is None:
root_info = info
484 changes: 257 additions & 227 deletions asdf/_tests/test_info.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import contextlib
import os
import pathlib
import re
@@ -6,7 +7,7 @@
import numpy as np

import asdf
from asdf.extension import ExtensionManager, ExtensionProxy, ManifestExtension
from asdf.extension import ExtensionProxy, ManifestExtension
from asdf.resource import DirectoryResourceMapping


@@ -130,6 +131,7 @@ def __asdf_traverse__(self):
}


@contextlib.contextmanager
def manifest_extension(tmp_path):
foo_manifest = """%YAML 1.1
---
@@ -282,13 +284,6 @@ def manifest_extension(tmp_path):
mpath = str(tmp_path / "manifests" / "foo_manifest-1.0.yaml")
with open(mpath, "w") as fmanifest:
fmanifest.write(foo_manifest)
config = asdf.get_config()
config.add_resource_mapping(
DirectoryResourceMapping(str(tmp_path / "manifests"), "asdf://somewhere.org/asdf/manifests/"),
)
config.add_resource_mapping(
DirectoryResourceMapping(str(tmp_path / "schemas"), "asdf://somewhere.org/asdf/schemas/"),
)

class FooConverter:
tags = ["asdf://somewhere.org/asdf/tags/foo-1.0.0"]
@@ -343,13 +338,20 @@ def from_yaml_tree(self, node, tag, ctx):
converter2 = BarConverter()
converter3 = DrinkConverter()

extension = ManifestExtension.from_uri(
"asdf://somewhere.org/asdf/manifests/foo_manifest-1.0",
converters=[converter1, converter2, converter3],
)
config = asdf.get_config()
proxy = ExtensionProxy(extension)
config.add_extension(proxy)
with asdf.config_context() as config:
config.add_resource_mapping(
DirectoryResourceMapping(str(tmp_path / "manifests"), "asdf://somewhere.org/asdf/manifests/"),
)
config.add_resource_mapping(
DirectoryResourceMapping(str(tmp_path / "schemas"), "asdf://somewhere.org/asdf/schemas/"),
)
extension = ManifestExtension.from_uri(
"asdf://somewhere.org/asdf/manifests/foo_manifest-1.0",
converters=[converter1, converter2, converter3],
)
proxy = ExtensionProxy(extension)
config.add_extension(proxy)
yield config


def create_tree():
@@ -372,14 +374,126 @@ def create_tree():


def test_schema_info_support(tmp_path):
manifest_extension(tmp_path)
config = asdf.get_config()
af = asdf.AsdfFile()
af._extension_manager = ExtensionManager(config.extensions)
af.tree = create_tree()
with manifest_extension(tmp_path):
af = asdf.AsdfFile()
af.tree = create_tree()

assert af.schema_info("title") == {
"list_of_stuff": [
{
"attributeOne": {
"title": ("AttributeOne Title", "v1"),
},
"attributeTwo": {
"title": ("AttributeTwo Title", "v2"),
},
"title": ("object with info support 3 title", af.tree["list_of_stuff"][0]),
},
{
"attributeOne": {
"title": ("AttributeOne Title", "x1"),
},
"attributeTwo": {
"title": ("AttributeTwo Title", "x2"),
},
"title": ("object with info support 3 title", af.tree["list_of_stuff"][1]),
},
],
"object": {
"I_example": {"title": ("integer pattern property", 1)},
"S_example": {"title": ("string pattern property", "beep")},
"allof_attribute": {"title": ("allOf example attribute", "good")},
"anyof_attribute": {
"attribute1": {
"title": ("Attribute1 Title", "VAL1"),
},
"attribute2": {
"title": ("Attribute2 Title", "VAL2"),
},
"title": ("object with info support 2 title", af.tree["object"].anyof),
},
"clown": {"title": ("clown name", "Bozo")},
"oneof_attribute": {"title": ("oneOf example attribute", 20)},
"the_meaning_of_life_the_universe_and_everything": {"title": ("Some silly title", 42)},
"title": ("object with info support title", af.tree["object"]),
},
}

assert af.schema_info("title", refresh_extension_manager=True) == {
"list_of_stuff": [
assert af.schema_info("archive_catalog") == {
"list_of_stuff": [
{
"attributeOne": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeOne"]}, "v1"),
},
"attributeTwo": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeTwo"]}, "v2"),
},
},
{
"attributeOne": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeOne"]}, "x1"),
},
"attributeTwo": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeTwo"]}, "x2"),
},
},
],
"object": {
"anyof_attribute": {
"attribute1": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attribute1"]}, "VAL1"),
},
"attribute2": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attribute2"]}, "VAL2"),
},
},
"clown": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.clown"]}, "Bozo"),
},
"the_meaning_of_life_the_universe_and_everything": {
"archive_catalog": ({"datatype": "int", "destination": ["ScienceCommon.silly"]}, 42),
},
},
}

assert af.schema_info("archive_catalog", preserve_list=False) == {
"list_of_stuff": {
0: {
"attributeOne": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeOne"]}, "v1"),
},
"attributeTwo": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeTwo"]}, "v2"),
},
},
1: {
"attributeOne": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeOne"]}, "x1"),
},
"attributeTwo": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeTwo"]}, "x2"),
},
},
},
"object": {
"anyof_attribute": {
"attribute1": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attribute1"]}, "VAL1"),
},
"attribute2": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attribute2"]}, "VAL2"),
},
},
"clown": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.clown"]}, "Bozo"),
},
"the_meaning_of_life_the_universe_and_everything": {
"archive_catalog": ({"datatype": "int", "destination": ["ScienceCommon.silly"]}, 42),
},
},
}

assert af.schema_info("title", "list_of_stuff") == [
{
"attributeOne": {
"title": ("AttributeOne Title", "v1"),
@@ -398,8 +512,9 @@ def test_schema_info_support(tmp_path):
},
"title": ("object with info support 3 title", af.tree["list_of_stuff"][1]),
},
],
"object": {
]

assert af.schema_info("title", "object") == {
"I_example": {"title": ("integer pattern property", 1)},
"S_example": {"title": ("string pattern property", "beep")},
"allof_attribute": {"title": ("allOf example attribute", "good")},
@@ -416,211 +531,91 @@ def test_schema_info_support(tmp_path):
"oneof_attribute": {"title": ("oneOf example attribute", 20)},
"the_meaning_of_life_the_universe_and_everything": {"title": ("Some silly title", 42)},
"title": ("object with info support title", af.tree["object"]),
},
}

assert af.schema_info("archive_catalog", refresh_extension_manager=True) == {
"list_of_stuff": [
{
"attributeOne": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeOne"]}, "v1"),
},
"attributeTwo": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeTwo"]}, "v2"),
},
},
{
"attributeOne": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeOne"]}, "x1"),
},
"attributeTwo": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeTwo"]}, "x2"),
},
},
],
"object": {
"anyof_attribute": {
"attribute1": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attribute1"]}, "VAL1"),
},
"attribute2": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attribute2"]}, "VAL2"),
},
},
"clown": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.clown"]}, "Bozo"),
},
"the_meaning_of_life_the_universe_and_everything": {
"archive_catalog": ({"datatype": "int", "destination": ["ScienceCommon.silly"]}, 42),
},
},
}

assert af.schema_info("archive_catalog", preserve_list=False, refresh_extension_manager=True) == {
"list_of_stuff": {
0: {
"attributeOne": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeOne"]}, "v1"),
},
"attributeTwo": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeTwo"]}, "v2"),
},
},
1: {
"attributeOne": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeOne"]}, "x1"),
},
"attributeTwo": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attributeTwo"]}, "x2"),
},
},
},
"object": {
"anyof_attribute": {
"attribute1": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attribute1"]}, "VAL1"),
},
"attribute2": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.attribute2"]}, "VAL2"),
},
},
"clown": {
"archive_catalog": ({"datatype": "str", "destination": ["ScienceCommon.clown"]}, "Bozo"),
},
"the_meaning_of_life_the_universe_and_everything": {
"archive_catalog": ({"datatype": "int", "destination": ["ScienceCommon.silly"]}, 42),
},
},
}
}

assert af.schema_info("title", "list_of_stuff", refresh_extension_manager=True) == [
{
"attributeOne": {
"title": ("AttributeOne Title", "v1"),
},
"attributeTwo": {
"title": ("AttributeTwo Title", "v2"),
},
"title": ("object with info support 3 title", af.tree["list_of_stuff"][0]),
},
{
"attributeOne": {
"title": ("AttributeOne Title", "x1"),
},
"attributeTwo": {
"title": ("AttributeTwo Title", "x2"),
},
"title": ("object with info support 3 title", af.tree["list_of_stuff"][1]),
},
]

assert af.schema_info("title", "object", refresh_extension_manager=True) == {
"I_example": {"title": ("integer pattern property", 1)},
"S_example": {"title": ("string pattern property", "beep")},
"allof_attribute": {"title": ("allOf example attribute", "good")},
"anyof_attribute": {
assert af.schema_info("title", "object.anyof_attribute") == {
"attribute1": {
"title": ("Attribute1 Title", "VAL1"),
},
"attribute2": {
"title": ("Attribute2 Title", "VAL2"),
},
"title": ("object with info support 2 title", af.tree["object"].anyof),
},
"clown": {"title": ("clown name", "Bozo")},
"oneof_attribute": {"title": ("oneOf example attribute", 20)},
"the_meaning_of_life_the_universe_and_everything": {"title": ("Some silly title", 42)},
"title": ("object with info support title", af.tree["object"]),
}
}

assert af.schema_info("title", "object.anyof_attribute", refresh_extension_manager=True) == {
"attribute1": {
"title": ("Attribute1 Title", "VAL1"),
},
"attribute2": {
assert af.schema_info("title", "object.anyof_attribute.attribute2") == {
"title": ("Attribute2 Title", "VAL2"),
},
"title": ("object with info support 2 title", af.tree["object"].anyof),
}

assert af.schema_info("title", "object.anyof_attribute.attribute2", refresh_extension_manager=True) == {
"title": ("Attribute2 Title", "VAL2"),
}
}

# Test printing the schema_info
assert (
af.schema_info("title", "object.anyof_attribute.attribute2", refresh_extension_manager=True).__repr__()
== "{'title': Attribute2 Title}"
)
# Test printing the schema_info
assert af.schema_info("title", "object.anyof_attribute.attribute2").__repr__() == "{'title': Attribute2 Title}"

assert af.schema_info("title", "object.anyof_attribute.attribute2.foo", refresh_extension_manager=True) is None
assert af.schema_info("title", "object.anyof_attribute.attribute2.foo") is None

assert af.schema_info(refresh_extension_manager=True) == {
"list_of_stuff": [
{
"attributeOne": {"description": ("AttributeOne description", "v1")},
"attributeTwo": {"description": ("AttributeTwo description", "v2")},
"description": ("object description", af.tree["list_of_stuff"][0]),
},
{
"attributeOne": {"description": ("AttributeOne description", "x1")},
"attributeTwo": {"description": ("AttributeTwo description", "x2")},
"description": ("object description", af.tree["list_of_stuff"][1]),
},
],
"object": {
"allof_attribute": {
"description": ("allOf description", "good"),
},
"clown": {
"description": ("clown description", "Bozo"),
},
"description": ("object with info support description", af.tree["object"]),
"oneof_attribute": {
"description": ("oneOf description", 20),
},
"the_meaning_of_life_the_universe_and_everything": {
"description": ("Some silly description", 42),
assert af.schema_info() == {
"list_of_stuff": [
{
"attributeOne": {"description": ("AttributeOne description", "v1")},
"attributeTwo": {"description": ("AttributeTwo description", "v2")},
"description": ("object description", af.tree["list_of_stuff"][0]),
},
{
"attributeOne": {"description": ("AttributeOne description", "x1")},
"attributeTwo": {"description": ("AttributeTwo description", "x2")},
"description": ("object description", af.tree["list_of_stuff"][1]),
},
],
"object": {
"allof_attribute": {
"description": ("allOf description", "good"),
},
"clown": {
"description": ("clown description", "Bozo"),
},
"description": ("object with info support description", af.tree["object"]),
"oneof_attribute": {
"description": ("oneOf description", 20),
},
"the_meaning_of_life_the_universe_and_everything": {
"description": ("Some silly description", 42),
},
},
},
}
}

# Test using a search result
search = af.search("clown")
assert af.schema_info("description", search, refresh_extension_manager=True) == {
"object": {
"clown": {
"description": ("clown description", "Bozo"),
# Test using a search result
search = af.search("clown")
assert af.schema_info("description", search) == {
"object": {
"clown": {
"description": ("clown description", "Bozo"),
},
"description": ("object with info support description", af.tree["object"]),
},
"description": ("object with info support description", af.tree["object"]),
},
}
}


def test_info_object_support(capsys, tmp_path):
manifest_extension(tmp_path)
config = asdf.get_config()
af = asdf.AsdfFile()
af._extension_manager = ExtensionManager(config.extensions)
af.tree = create_tree()
af.info(refresh_extension_manager=True)
with manifest_extension(tmp_path):
af = asdf.AsdfFile()
af.tree = create_tree()
af.info()

captured = capsys.readouterr()
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
assert "clown name" in captured.out
assert "silly" in captured.out
assert "info support 2" in captured.out
assert "Attribute2 Title" in captured.out
assert "allOf example attribute" in captured.out
assert "oneOf example attribute" in captured.out
assert "string pattern property" in captured.out
assert "integer pattern property" in captured.out
assert "AttributeOne" in captured.out
assert "AttributeTwo" in captured.out
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
assert "clown name" in captured.out
assert "silly" in captured.out
assert "info support 2" in captured.out
assert "Attribute2 Title" in captured.out
assert "allOf example attribute" in captured.out
assert "oneOf example attribute" in captured.out
assert "string pattern property" in captured.out
assert "integer pattern property" in captured.out
assert "AttributeOne" in captured.out
assert "AttributeTwo" in captured.out


class RecursiveObjectWithInfoSupport:
@@ -639,25 +634,23 @@ def __str__(self):

def test_recursive_info_object_support(capsys, tmp_path):
tempdir = pathlib.Path(tempfile.mkdtemp())
manifest_extension(tempdir)
config = asdf.get_config()
af = asdf.AsdfFile()
af._extension_manager = ExtensionManager(config.extensions)

recursive_obj = RecursiveObjectWithInfoSupport()
recursive_obj.recursive = recursive_obj
tree = {"random": 3.14159, "rtest": recursive_obj}
af = asdf.AsdfFile()
# we need to do this to avoid validation against the
# manifest (generated in manifest_extension) which is
# now supported with the default asdf standard 1.6.0
# I'm not sure why the manifest has this restriction
# and prior to switching to the default 1.6.0 was ignored
# which allowed this test to pass.
af._tree = tree
af.info(refresh_extension_manager=True)
captured = capsys.readouterr()
assert "recursive reference" in captured.out
with manifest_extension(tempdir):
af = asdf.AsdfFile()

recursive_obj = RecursiveObjectWithInfoSupport()
recursive_obj.recursive = recursive_obj
tree = {"random": 3.14159, "rtest": recursive_obj}
af = asdf.AsdfFile()
# we need to do this to avoid validation against the
# manifest (generated in manifest_extension) which is
# now supported with the default asdf standard 1.6.0
# I'm not sure why the manifest has this restriction
# and prior to switching to the default 1.6.0 was ignored
# which allowed this test to pass.
af._tree = tree
af.info()
captured = capsys.readouterr()
assert "recursive reference" in captured.out


def test_search():
@@ -702,3 +695,40 @@ def __str__(self):
assert "(NewlineStr)\n" in captured.out
assert "(CarriageReturnStr)\n" in captured.out
assert "(NiceStr): nice\n" in captured.out


def test_info_with_custom_extension(capsys):
MY_TAG_URI = "asdf://somewhere.org/tags/foo-1.0.0"
MY_SCHEMA_URI = "asdf://somewhere.org/tags/foo-1.0.0"

schema_bytes = f"""%YAML 1.1
---
$schema: "http://stsci.edu/schemas/yaml-schema/draft-01"
id: {MY_SCHEMA_URI}
title: sentinel""".encode(
"ascii"
)

class MyExtension:
extension_uri = "asdf://somewhere.org/extensions/foo-1.0.0"
tags = [
asdf.extension.TagDefinition(
MY_TAG_URI,
schema_uris=[MY_SCHEMA_URI],
)
]

class Thing:
_tag = MY_TAG_URI

def __asdf_traverse__(self):
return []

with asdf.config_context() as cfg:
cfg.add_resource_mapping({MY_SCHEMA_URI: schema_bytes})
ext = MyExtension()
af = asdf.AsdfFile({"t": Thing()}, extensions=[ext])
af.info(max_cols=None)

captured = capsys.readouterr()
assert "sentinel" in captured.out

0 comments on commit 22728e7

Please sign in to comment.