Repository: asdf-format/asdf

Issue:
closed `AsdfFile` instances hold private reference to tree
After `close` an `AsdfFile` instance will prevent tree access through the `tree` property:
https://github.com/asdf-format/asdf/blob/b0c9a50e8d3add337e1271369a3bf834925176a4/asdf/asdf.py#L552-L562

However, because `AsdfFile._tree` is not cleared, the closed instance will hold references to objects in the tree preventing them from being garbage collected as seen in the following example.

```python
import asdf
import numpy
import weakref

fn = 'test.asdf'
asdf.AsdfFile({'arr': numpy.arange(1000)}).write_to(fn)

with asdf.open(fn, copy_arrays=True, lazy_load=False) as af:
    array_ref = weakref.ref(af['arr'])

# we shouldn't be able to resolve this ref
try:
    print(af['arr'])
except OSError as e:
    print(f"Unable to access array through tree: {e}")

r = array_ref()
if r is not None:
    print(f"Weakref resolved to {r[:10]}...")
```
outputs:
```
Unable to access array through tree: Cannot access data from closed ASDF file
Weakref resolved to [0 1 2 3 4 5 6 7 8 9]...
```

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index d74d5808..d1c4525b 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -19,6 +19,7 @@ The ASDF Standard is at v1.6.0
 - Removed deprecated AsdfFile.open and deprecated asdf.open
   AsdfFile.write_to and AsdfFile.update kwargs [#1592]
 - Fix ``AsdfFile.info`` loading all array data [#1572]
+- Blank out AsdfFile.tree on close [#1575]
 
 2.15.1 (2023-08-07)
 -------------------
diff --git a/asdf/asdf.py b/asdf/asdf.py
index 70b9ee11..4ff59203 100644
--- a/asdf/asdf.py
+++ b/asdf/asdf.py
@@ -454,6 +454,9 @@ class AsdfFile:
             self._fd.close()
             self._fd = None
             self._closed = True
+            # as we're closing the file, also empty out the
+            # tree so that references to array data can be released
+            self._tree = AsdfObject()
         for external in self._external_asdf_by_uri.values():
             external.close()
         self._external_asdf_by_uri.clear()


Test patch:
diff --git a/asdf/_tests/_regtests/test_1558.py b/asdf/_tests/_regtests/test_1558.py
new file mode 100644
index 00000000..508cd1ba
--- /dev/null
+++ b/asdf/_tests/_regtests/test_1558.py
@@ -0,0 +1,34 @@
+import weakref
+
+import numpy as np
+import pytest
+
+import asdf
+
+
+def test_asdffile_tree_cleared_on_close(tmp_path):
+    """
+    closed AsdfFile instances hold private reference to tree
+
+    https://github.com/asdf-format/asdf/issues/1558
+    """
+
+    fn = tmp_path / "test.asdf"
+    asdf.AsdfFile({"a": np.arange(1000), "b": np.arange(42)}).write_to(fn)
+
+    with asdf.open(fn, copy_arrays=True, lazy_load=False) as af:
+        array_weakref = weakref.ref(af["a"])
+        array_ref = af["b"]
+
+    # we shouldn't be able to access the now closed file
+    with pytest.raises(OSError, match="Cannot access data"):
+        af["a"]
+    with pytest.raises(OSError, match="Cannot access data"):
+        af["b"]
+
+    # we also should not be able to resolve the weak reference
+    # meaning the tree should have been cleaned up
+    r = array_weakref()
+    assert r is None
+
+    assert array_ref is not None