diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tdirectoryfile.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tdirectoryfile.py index 0c9d0e41e4874..2066d8fde89bd 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tdirectoryfile.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tdirectoryfile.py @@ -69,7 +69,12 @@ def _TDirectoryFile_Get(self, namecycle): if key: class_name = key.GetClassName() address = self.GetObjectChecked(namecycle, class_name) - return ROOT._cppyy.bind_object(address, class_name) + obj = ROOT._cppyy.bind_object(address, class_name) + # Objects that are not registered in the directory (e.g. TGraph or + # non-TObjects like ROOT::RNTuple) are owned by the caller + if address and not (isinstance(obj, ROOT.TObject) and self.GetList().FindObject(obj)): + ROOT.SetOwnership(obj, True) + return obj # no key? for better or worse, call normal Get() return self._Get(namecycle) diff --git a/bindings/pyroot/pythonizations/test/tdirectory_tfile.py b/bindings/pyroot/pythonizations/test/tdirectory_tfile.py index 64ec169f7881d..479a5fc0b4330 100644 --- a/bindings/pyroot/pythonizations/test/tdirectory_tfile.py +++ b/bindings/pyroot/pythonizations/test/tdirectory_tfile.py @@ -210,6 +210,58 @@ def test_keys_title(self): finput.Close() +class TFileGetOwnership(unittest.TestCase): + """ + Test that the objects returned by Get are owned by Python unless they are + owned by the directory (#16178) + """ + + filename = "tfilegetownership.root" + + @classmethod + def setUpClass(cls): + with TFile(cls.filename, "recreate") as f: + ROOT.TH1F("h", "h", 10, 0, 1).Write() + ROOT.TTree("t", "t").Write() + f.mkdir("dir") + ROOT.TGraph(10).Write("g") + ROOT.TObjString("s").Write("s") + f.WriteObject(ROOT.std.vector["double"](10), "v") + + @classmethod + def tearDownClass(cls): + os.remove(cls.filename) + + def test_owned_by_directory(self): + with TFile(self.filename) as f: + for name in ["h", "t", "dir"]: + obj = f.Get(name) + # Whether these objects are registered in the directory may + # depend on ROOT::Experimental::ObjectAutoRegistrationEnabled() + # in the future, so check the ownership against the actual + # registration + registered = bool(f.GetList().FindObject(name)) + self.assertEqual(obj.__python_owns__, not registered, msg=name) + # The directory must keep the object alive after the Python + # object is gone + del obj + if registered: + # Check before Get(), which could reload the object from disk. + self.assertTrue(f.GetList().FindObject(name), msg=name) + self.assertEqual(f.Get(name).GetName(), name) + + def test_owned_by_python(self): + with TFile(self.filename) as f: + objs = {name: f.Get(name) for name in ["g", "s", "v"]} + for name, obj in objs.items(): + self.assertTrue(obj.__python_owns__, msg=name) + self.assertFalse(f.GetList().FindObject(name), msg=name) + # The objects are still usable after the file is closed + self.assertEqual(objs["g"].GetN(), 10) + self.assertEqual(objs["s"].GetString(), "s") + self.assertEqual(objs["v"].size(), 10) + + class TFileConstructor(unittest.TestCase): """ Test for the TFile constructor