Repository: asdf-format/asdf

Issue:
Objects that implement `__asdf_traverse__` are expected to have a `_tag`
# Description of the problem

Adding `__asdf_traverse__` to an object will cause `info` (and possibly other commands) to fail if the object does not also contain a `_tag` attribute.

# Example of the problem

```python
import asdf

class MyContainer:
    def __init__(self, data):
        self.data = data

    def __asdf_traverse__(self):
        return self.data

c = MyContainer([1, 2, 3])
af = asdf.AsdfFile()
af['c'] = c
af.info()
```

The above example produces the following error:
```
Traceback (most recent call last):
  File "/Users/bgraham/projects/src/asdf/traverse_error.py", line 15, in <module>
    af.info()
  File "/Users/bgraham/projects/src/asdf/asdf/_asdf.py", line 1426, in info
    lines = display.render_tree(
  File "/Users/bgraham/projects/src/asdf/asdf/_display.py", line 45, in render_tree
    info = create_tree(
  File "/Users/bgraham/projects/src/asdf/asdf/_node_info.py", line 44, in create_tree
    schema_info = NodeSchemaInfo.from_root_node(
  File "/Users/bgraham/projects/src/asdf/asdf/_node_info.py", line 285, in from_root_node
    info.set_schema_from_node(node, extension_manager)
  File "/Users/bgraham/projects/src/asdf/asdf/_node_info.py", line 242, in set_schema_from_node
    tag_def = extension_manager.get_tag_definition(node._tag)
AttributeError: 'MyContainer' object has no attribute '_tag'
```

# System information

asdf version: main
python version: 3.10
operating system: mac osx


Discussion:
braingram: A few unrelated errors are seen in downstream:
- the [weldx errors](https://github.com/asdf-format/asdf/actions/runs/7629799922/job/20784056067?pr=1739#step:10:932) are due to a new pandas version
- the [specutils error] is due to a failure to fetch iers data
- the [stdatamodels error] is due to a crds update

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 178ffea0..bb568140 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,8 @@
+3.3.0 (unreleased)
+------------------
+
+- Fix ``__asdf_traverse__`` for non-tagged objects [#1739]
+
 3.2.0 (2024-04-05)
 ------------------
 
diff --git a/asdf/_node_info.py b/asdf/_node_info.py
index e61b2224..f45ef3b1 100644
--- a/asdf/_node_info.py
+++ b/asdf/_node_info.py
@@ -282,7 +282,15 @@ class NodeSchemaInfo:
 
                     if cls.traversable(node):
                         t_node = node.__asdf_traverse__()
-                        info.set_schema_from_node(node, extension_manager)
+                        if hasattr(node, "_tag") and isinstance(node._tag, str):
+                            try:
+                                info.set_schema_from_node(node, extension_manager)
+                            except KeyError:
+                                # if _tag is not a valid tag, no schema will be found
+                                # and a KeyError will be raised. We don't raise the KeyError
+                                # (or another exception) here as the node (object) might
+                                # be using _tag for a non-ASDF purpose.
+                                pass
 
                     else:
                         t_node = node


Test patch:
diff --git a/asdf/_tests/_regtests/test_1738.py b/asdf/_tests/_regtests/test_1738.py
new file mode 100644
index 00000000..633c268d
--- /dev/null
+++ b/asdf/_tests/_regtests/test_1738.py
@@ -0,0 +1,36 @@
+import asdf
+
+
+def test_info_on_non_tagged_asdf_traverse_object():
+    """
+    Calling info with a tree containing an object that implements
+    __asdf_traverse__ but does not have a _tag results in an
+    error
+
+    https://github.com/asdf-format/asdf/issues/1738
+    """
+
+    class MyContainer:
+        def __init__(self, data):
+            self.data = data
+
+        def __asdf_traverse__(self):
+            return self.data
+
+    c = MyContainer([1, 2, 3])
+    af = asdf.AsdfFile()
+    af["c"] = c
+
+    # info should not error out
+    af.info()
+
+    # and search should work with the container
+    assert af.search(type_=int).paths == ["root['c'][0]", "root['c'][1]", "root['c'][2]"]
+
+    # this should work even if _tag exists (and is not a tag)
+    c._tag = {}
+    af.info()
+    assert af.search(type_=int).paths == ["root['c'][0]", "root['c'][1]", "root['c'][2]"]
+    c._tag = "a"
+    af.info()
+    assert af.search(type_=int).paths == ["root['c'][0]", "root['c'][1]", "root['c'][2]"]