From ed96e5895d6193bc941a7b9a14c1ec1309885433 Mon Sep 17 00:00:00 2001 From: silverweed Date: Mon, 20 Jul 2026 14:06:26 +0200 Subject: [PATCH 1/8] [rfile] Some minor fixups to RFile (mostly doc comments) --- io/io/inc/ROOT/RFile.hxx | 51 +++++++++++++++++++++++----------------- io/io/src/RFile.cxx | 4 ++-- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/io/io/inc/ROOT/RFile.hxx b/io/io/inc/ROOT/RFile.hxx index c98cb09a42024..f17b6f6ce67a7 100644 --- a/io/io/inc/ROOT/RFile.hxx +++ b/io/io/inc/ROOT/RFile.hxx @@ -255,11 +255,11 @@ class RFile final { friend TFile *Internal::GetRFileTFile(RFile &rfile); /// Flags used in PutInternal() - enum PutFlags { + enum EPutFlags { /// When encountering an object at the specified path, overwrite it with the new one instead of erroring out. - kPutAllowOverwrite = 0x1, + kPutFlag_AllowOverwrite = 0x1, /// When overwriting an object, preserve the existing one and create a new cycle, rather than removing it. - kPutOverwriteKeepCycle = 0x2, + kPutFlag_OverwriteKeepCycle = 0x2, }; std::unique_ptr fFile; @@ -348,21 +348,24 @@ public: /// Retrieves an object from the file. /// `path` should be a string such that `IsValidPath(path) == true`, otherwise an exception will be thrown. /// See \ref ValidateAndNormalizePath() for info about valid path names. - /// If the object is not there returns a null pointer. + /// \return A copy of the object at `path`, or `nullptr` if there is no object of type `T` at the specified path. template std::unique_ptr Get(std::string_view path) const { void *obj = GetUntyped(path, typeid(T)); return std::unique_ptr(static_cast(obj)); } - - /// Puts an object into the file. - /// The application retains ownership of the object. + + /// Puts object `obj` into the file. + /// The object will be effectively copied into the file, so any further modifications won't be seen by the object + /// inside the file. + /// Note that the object is not necessarily written to storage until the RFile is closed or `Flush()` is called. + /// /// `path` should be a string such that `IsValidPath(path) == true`, otherwise an exception will be thrown. /// See \ref ValidateAndNormalizePath() for info about valid path names. /// - /// Throws a RException if `path` already identifies a valid object or directory. - /// Throws a RException if the file was opened in read-only mode. + /// \throws ROOT::RException if `path` already identifies a valid object or directory. + /// \throws ROOT::RException if the file was opened in read-only mode. template void Put(std::string_view path, const T &obj) { @@ -371,24 +374,26 @@ public: } /// Puts an object into the file, overwriting any previously-existing object at that path. - /// The application retains ownership of the object. + /// See Put for more details. /// - /// If an object already exists at that path, it is kept as a backup cycle unless `backupPrevious` is false. - /// Note that even if `backupPrevious` is false, any existing cycle except the latest will be preserved. + /// If an object already exists at that path,AND it has type `T`, it is kept as a backup cycle + /// unless `createNewCycle` is false. + /// If the existing object's type differs from `T`, an exception is thrown. + /// Note that even if `createNewCycle` is false only the *latest* cycle will be deleted. /// - /// Throws a RException if `path` is already the path of a directory. - /// Throws a RException if the file was opened in read-only mode. + /// \throws ROOT::RException if `path` is already the path of a directory. + /// \throws ROOT::RException if the file was opened in read-only mode. template - void Overwrite(std::string_view path, const T &obj, bool backupPrevious = true) + void Overwrite(std::string_view path, const T &obj, bool createNewCycle = true) { static_assert(!std::is_base_of_v, "Writing TTree with RFile is unsupported."); - std::uint32_t flags = kPutAllowOverwrite; - flags |= backupPrevious * kPutOverwriteKeepCycle; + std::uint32_t flags = kPutFlag_AllowOverwrite; + flags |= createNewCycle * kPutFlag_OverwriteKeepCycle; PutInternal(path, obj, flags); } /// Writes all objects and the file structure to disk. - /// Returns the number of bytes written. + /// \return the number of bytes written. size_t Flush(); /// Flushes the RFile if needed and closes it, disallowing any further reading or writing. @@ -396,6 +401,8 @@ public: /// Returns an iterable over all keys of objects and/or directories written into this RFile starting at path /// `basePath` (defaulting to include the content of all subdirectories). + /// The iterable yields values of type ROOT::Experimental::RKeyInfo. + /// /// By default, keys referring to directories are not returned: only those referring to leaf objects are. /// If `basePath` is the path of a leaf object, only `basePath` itself will be returned. /// If `basePath` is the path of a directory, it won't appear in the listing. @@ -408,17 +415,17 @@ public: /// Example usage: /// ~~~{.cpp} /// for (RKeyInfo key : file->ListKeys()) { - /// /* iterate over all objects in the RFile */ + /// // iterate over all objects in the RFile /// cout << key.GetPath() << ";" << key.GetCycle() << " of type " << key.GetClassName() << "\n"; /// } /// for (RKeyInfo key : file->ListKeys("", kListDirs|kListObjects|kListRecursive)) { - /// /* iterate over all objects and directories in the RFile */ + /// // iterate over all objects and directories in the RFile /// } /// for (RKeyInfo key : file->ListKeys("a/b", kListObjects)) { - /// /* iterate over all objects that are immediate children of directory "a/b" */ + /// // iterate over all objects that are immediate children of directory "a/b" /// } /// for (RKeyInfo key : file->ListKeys("foo", kListDirs|kListRecursive)) { - /// /* iterate over all directories under directory "foo", recursively */ + /// // iterate over all directories under directory "foo", recursively /// } /// ~~~ RFileKeyIterable ListKeys(std::string_view basePath = "", std::uint32_t flags = kListObjects | kListRecursive) const diff --git a/io/io/src/RFile.cxx b/io/io/src/RFile.cxx index 6ceab8e99db55..c41c9a0d0f2f1 100644 --- a/io/io/src/RFile.cxx +++ b/io/io/src/RFile.cxx @@ -379,8 +379,8 @@ void RFile::PutUntyped(std::string_view pathSV, const std::type_info &type, cons } } - const bool allowOverwrite = (flags & kPutAllowOverwrite) != 0; - const bool backupCycle = (flags & kPutOverwriteKeepCycle) != 0; + const bool allowOverwrite = (flags & kPutFlag_AllowOverwrite) != 0; + const bool backupCycle = (flags & kPutFlag_OverwriteKeepCycle) != 0; const Option_t *writeOpts = ""; if (!allowOverwrite) { const TKey *existing = dir->GetKey(tokens[tokens.size() - 1].c_str()); From 906c238fffebf3bcf23102be7d62c71d4085c31c Mon Sep 17 00:00:00 2001 From: silverweed Date: Tue, 21 Jul 2026 16:36:17 +0200 Subject: [PATCH 2/8] [io] Add overload of TDirectoryFile::WriteObjectAny accepting title Then call it from the current WriteObjectAny --- io/io/inc/TDirectoryFile.h | 1 + io/io/src/TDirectoryFile.cxx | 120 ++++++++++++++++++----------------- 2 files changed, 64 insertions(+), 57 deletions(-) diff --git a/io/io/inc/TDirectoryFile.h b/io/io/inc/TDirectoryFile.h index 16f2d2edaa4e4..dd0ff02a7f767 100644 --- a/io/io/inc/TDirectoryFile.h +++ b/io/io/inc/TDirectoryFile.h @@ -125,6 +125,7 @@ class TDirectoryFile : public TDirectory { Int_t WriteTObject(const TObject *obj, const char *name=nullptr, Option_t *option="", Int_t bufsize=0) override; Int_t WriteObjectAny(const void *obj, const char *classname, const char *name, Option_t *option="", Int_t bufsize=0) override; Int_t WriteObjectAny(const void *obj, const TClass *cl, const char *name, Option_t *option="", Int_t bufsize=0) override; + Int_t WriteObjectAny(const void *obj, const char *title, const TClass *cl, const char *name, Option_t *option="", Int_t bufsize=0); void WriteDirHeader() override; void WriteKeys() override; diff --git a/io/io/src/TDirectoryFile.cxx b/io/io/src/TDirectoryFile.cxx index 14ae6a2fb7896..9072e9efad3b1 100644 --- a/io/io/src/TDirectoryFile.cxx +++ b/io/io/src/TDirectoryFile.cxx @@ -2011,63 +2011,7 @@ Int_t TDirectoryFile::WriteTObject(const TObject *obj, const char *name, Option_ return nbytes; } -//////////////////////////////////////////////////////////////////////////////// -/// Write object from pointer of class classname in this directory. -/// -/// obj may not derive from TObject. See TDirectoryFile::WriteTObject for comments -/// -/// ## Very important note -/// The value passed as 'obj' needs to be from a pointer to the type described by classname. -/// For example: -/// ~~~{.cpp} -/// TopClass *top; -/// BottomClass *bottom; -/// top = bottom; -/// ~~~ -/// you can do: -/// ~~~{.cpp} -/// directory->WriteObjectAny(top,"top","name of object"); -/// directory->WriteObjectAny(bottom,"bottom","name of object"); -/// ~~~ -/// BUT YOU CAN NOT DO the following since it will fail with multiple inheritance: -/// ~~~{.cpp} -/// directory->WriteObjectAny(top,"bottom","name of object"); -/// ~~~ -/// We STRONGLY recommend to use -/// ~~~{.cpp} -/// TopClass *top = ....; -/// directory->WriteObject(top,"name of object") -/// ~~~ -/// See also remarks in TDirectoryFile::WriteTObject - -Int_t TDirectoryFile::WriteObjectAny(const void *obj, const char *classname, const char *name, Option_t *option, Int_t bufsize) -{ - TClass *cl = TClass::GetClass(classname); - if (!cl) { - TObject *info_obj = *(TObject**)obj; - TVirtualStreamerInfo *info = dynamic_cast(info_obj); - if (!info) { - Error("WriteObjectAny","Unknown class: %s",classname); - return 0; - } else { - cl = info->GetClass(); - } - } - return WriteObjectAny(obj,cl,name,option,bufsize); -} - -//////////////////////////////////////////////////////////////////////////////// -/// Write object of class with dictionary cl in this directory. -/// -/// obj may not derive from TObject -/// To get the TClass* cl pointer, one can use -/// -/// TClass *cl = TClass::GetClass("classname"); -/// -/// An alternative is to call the function WriteObjectAny above. -/// see TDirectoryFile::WriteTObject for comments - -Int_t TDirectoryFile::WriteObjectAny(const void *obj, const TClass *cl, const char *name, Option_t *option, Int_t bufsize) +Int_t TDirectoryFile::WriteObjectAny(const void *obj, const char *title, const TClass *cl, const char *name, Option_t *option, Int_t bufsize) { TDirectory::TContext ctxt(this); @@ -2137,6 +2081,7 @@ Int_t TDirectoryFile::WriteObjectAny(const void *obj, const TClass *cl, const ch oldkey = GetKey(oname); } key = fFile->CreateKey(this, obj, cl, oname, bsize); + key->SetTitle(title); if (newName) delete [] newName; if (!key->GetSeekKey()) { @@ -2156,6 +2101,67 @@ Int_t TDirectoryFile::WriteObjectAny(const void *obj, const TClass *cl, const ch return nbytes; } +//////////////////////////////////////////////////////////////////////////////// +/// Write object from pointer of class classname in this directory. +/// +/// obj may not derive from TObject. See TDirectoryFile::WriteTObject for comments +/// +/// ## Very important note +/// The value passed as 'obj' needs to be from a pointer to the type described by classname. +/// For example: +/// ~~~{.cpp} +/// TopClass *top; +/// BottomClass *bottom; +/// top = bottom; +/// ~~~ +/// you can do: +/// ~~~{.cpp} +/// directory->WriteObjectAny(top,"top","name of object"); +/// directory->WriteObjectAny(bottom,"bottom","name of object"); +/// ~~~ +/// BUT YOU CAN NOT DO the following since it will fail with multiple inheritance: +/// ~~~{.cpp} +/// directory->WriteObjectAny(top,"bottom","name of object"); +/// ~~~ +/// We STRONGLY recommend to use +/// ~~~{.cpp} +/// TopClass *top = ....; +/// directory->WriteObject(top,"name of object") +/// ~~~ +/// See also remarks in TDirectoryFile::WriteTObject + +Int_t TDirectoryFile::WriteObjectAny(const void *obj, const char *classname, const char *name, Option_t *option, Int_t bufsize) +{ + TClass *cl = TClass::GetClass(classname); + if (!cl) { + TObject *info_obj = *(TObject**)obj; + TVirtualStreamerInfo *info = dynamic_cast(info_obj); + if (!info) { + Error("WriteObjectAny","Unknown class: %s",classname); + return 0; + } else { + cl = info->GetClass(); + } + } + return WriteObjectAny(obj,cl,name,option,bufsize); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Write object of class with dictionary cl in this directory. +/// +/// obj may not derive from TObject +/// To get the TClass* cl pointer, one can use +/// +/// TClass *cl = TClass::GetClass("classname"); +/// +/// An alternative is to call the function WriteObjectAny above. +/// see TDirectoryFile::WriteTObject for comments + +Int_t TDirectoryFile::WriteObjectAny(const void *obj, const TClass *cl, const char *name, Option_t *option, Int_t bufsize) +{ + return WriteObjectAny(obj, /*title=*/ "", cl, name, option, bufsize); +} + //////////////////////////////////////////////////////////////////////////////// /// Overwrite the Directory header record. From e0ce193f6e43a0f6c15fbe8fab1121c0e8f05a4c Mon Sep 17 00:00:00 2001 From: silverweed Date: Tue, 21 Jul 2026 16:37:36 +0200 Subject: [PATCH 3/8] [rfile] Add possibility to set object title in RFile::PutInternal --- io/io/inc/ROOT/RFile.hxx | 7 ++++--- io/io/src/RFile.cxx | 8 ++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/io/io/inc/ROOT/RFile.hxx b/io/io/inc/ROOT/RFile.hxx index f17b6f6ce67a7..283989015c85d 100644 --- a/io/io/inc/ROOT/RFile.hxx +++ b/io/io/inc/ROOT/RFile.hxx @@ -273,13 +273,14 @@ class RFile final { std::variant> type) const; /// Writes `obj` to file, without taking its ownership. - void PutUntyped(std::string_view path, const std::type_info &type, const void *obj, std::uint32_t flags); + void PutUntyped(std::string_view path, const std::type_info &type, const void *obj, std::uint32_t flags, + std::string_view title); /// \see Put template - void PutInternal(std::string_view path, const T &obj, std::uint32_t flags) + void PutInternal(std::string_view path, const T &obj, std::uint32_t flags, std::string_view title = "") { - PutUntyped(path, typeid(T), &obj, flags); + PutUntyped(path, typeid(T), &obj, flags, title); } /// Given `path`, returns the TKey corresponding to the object at that path (assuming the path is fully split, i.e. diff --git a/io/io/src/RFile.cxx b/io/io/src/RFile.cxx index c41c9a0d0f2f1..b4c6dc8f282e0 100644 --- a/io/io/src/RFile.cxx +++ b/io/io/src/RFile.cxx @@ -328,7 +328,8 @@ void *RFile::GetUntyped(std::string_view path, return obj; } -void RFile::PutUntyped(std::string_view pathSV, const std::type_info &type, const void *obj, std::uint32_t flags) +void RFile::PutUntyped(std::string_view pathSV, const std::type_info &type, const void *obj, std::uint32_t flags, + std::string_view title) { const TClass *cls = TClass::GetClass(type); if (!cls) @@ -392,7 +393,10 @@ void RFile::PutUntyped(std::string_view pathSV, const std::type_info &type, cons writeOpts = "WriteDelete"; } - int success = dir->WriteObjectAny(obj, cls, tokens[tokens.size() - 1].c_str(), writeOpts); + const char *objName = tokens[tokens.size() - 1].c_str(); + assert(dynamic_cast(dir)); + int success = + static_cast(dir)->WriteObjectAny(obj, std::string(title).c_str(), cls, objName, writeOpts); if (!success) { throw ROOT::RException(R__FAIL(std::string("Failed to write ") + path + " to file")); From 01c55068d841f84f2125e449571f45fb2c69c6d5 Mon Sep 17 00:00:00 2001 From: silverweed Date: Tue, 21 Jul 2026 17:06:28 +0200 Subject: [PATCH 4/8] [rfile] Add possibility of setting an object title when writing --- .../python/ROOT/_pythonization/_rfile.py | 4 ++-- io/io/inc/ROOT/RFile.hxx | 17 ++++++++++++----- io/io/test/rfile.cxx | 6 +++++- io/io/test/rfile.py | 3 ++- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py index 849da65996d7f..8ba8a1e579ada 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py @@ -58,7 +58,7 @@ class _RFile_Put: def __init__(self, rfile): self._rfile = rfile - def __call__(self, name, obj): + def __call__(self, name, obj, title = ""): """ Non-templated Put() """ @@ -70,7 +70,7 @@ def __call__(self, name, obj): raise TypeError(f"type {objType} is not supported by ROOT I/O") else: className = objType.__cpp_name__ - self._rfile.Put[className](name, obj) + self._rfile.Put[className](name, obj, title) def __getitem__(self, template_arg): """ diff --git a/io/io/inc/ROOT/RFile.hxx b/io/io/inc/ROOT/RFile.hxx index 283989015c85d..bda736b06f45b 100644 --- a/io/io/inc/ROOT/RFile.hxx +++ b/io/io/inc/ROOT/RFile.hxx @@ -356,8 +356,8 @@ public: void *obj = GetUntyped(path, typeid(T)); return std::unique_ptr(static_cast(obj)); } - - /// Puts object `obj` into the file. + + /// Puts object `obj` into the file, optionally giving it a title. /// The object will be effectively copied into the file, so any further modifications won't be seen by the object /// inside the file. /// Note that the object is not necessarily written to storage until the RFile is closed or `Flush()` is called. @@ -368,10 +368,10 @@ public: /// \throws ROOT::RException if `path` already identifies a valid object or directory. /// \throws ROOT::RException if the file was opened in read-only mode. template - void Put(std::string_view path, const T &obj) + void Put(std::string_view path, const T &obj, std::string_view title = "") { static_assert(!std::is_base_of_v, "Writing TTree with RFile is unsupported."); - PutInternal(path, obj, /* flags = */ 0); + PutInternal(path, obj, /* flags = */ 0, title); } /// Puts an object into the file, overwriting any previously-existing object at that path. @@ -388,9 +388,16 @@ public: void Overwrite(std::string_view path, const T &obj, bool createNewCycle = true) { static_assert(!std::is_base_of_v, "Writing TTree with RFile is unsupported."); + Overwrite(path, obj, "", createNewCycle); + } + + /// Like Overwrite(std::string_view, const T&, bool) but allows giving a title to the object. + template + void Overwrite(std::string_view path, const T &obj, std::string_view title, bool createNewCycle = true) + { std::uint32_t flags = kPutFlag_AllowOverwrite; flags |= createNewCycle * kPutFlag_OverwriteKeepCycle; - PutInternal(path, obj, flags); + PutInternal(path, obj, flags, title); } /// Writes all objects and the file structure to disk. diff --git a/io/io/test/rfile.cxx b/io/io/test/rfile.cxx index 48dc835a9a283..60b9eec6fd73f 100644 --- a/io/io/test/rfile.cxx +++ b/io/io/test/rfile.cxx @@ -296,13 +296,15 @@ TEST(RFile, PutOverwrite) { TH1D hist("hist", "", 100, -10, 10); hist.FillRandom("gaus", 1000); - file->Put("hist", hist); + file->Put("hist", hist, "My Histo"); } { auto hist = file->Get("hist"); ASSERT_TRUE(hist); EXPECT_EQ(static_cast(hist->GetEntries()), 1000); + auto key = file->GetKeyInfo("hist").value(); + EXPECT_EQ(key.GetTitle(), "My Histo"); } // Try putting another object at the same path, should fail @@ -332,6 +334,8 @@ TEST(RFile, PutOverwrite) // ...but any cycle before the latest should still be there! hist = file->Get("hist;1"); EXPECT_NE(hist, nullptr); + auto key = file->GetKeyInfo("hist;1").value(); + EXPECT_EQ(key.GetTitle(), "My Histo"); } } diff --git a/io/io/test/rfile.py b/io/io/test/rfile.py index 992288befce56..29a49719263be 100644 --- a/io/io/test/rfile.py +++ b/io/io/test/rfile.py @@ -61,7 +61,7 @@ def test_getkeyinfo(self): with ROOT.RFile.Recreate(fileName) as rfile: hist = ROOT.TH1D("hist", "", 100, -10, 10) hist.FillRandom("gaus", 10) - rfile.Put("hist", hist) + rfile.Put("hist", hist, "My Histo") rfile.Put("foo/hist", hist) rfile.Put("foo/bar/hist", hist) rfile.Put("foo/bar/hist2", hist) @@ -71,6 +71,7 @@ def test_getkeyinfo(self): key = rfile.GetKeyInfo("hist") self.assertEqual(key.GetPath(), "hist") self.assertEqual(key.GetClassName(), "TH1D") + self.assertEqual(key.GetTitle(), "My Histo") key = rfile.GetKeyInfo("does_not_exist") self.assertEqual(key, None) From 1d47d53b89b620c215af0ff7e8f64183150d1d45 Mon Sep 17 00:00:00 2001 From: silverweed Date: Mon, 3 Aug 2026 14:37:19 +0200 Subject: [PATCH 5/8] [rfile] Support Put overload for TNamed objects This will automatically call Put(obj.GetName(), obj, obj.GetTitle()). Added a pythonization as well. --- .../python/ROOT/_pythonization/_rfile.py | 18 ++++++++++-- io/io/inc/ROOT/RFile.hxx | 11 ++++++-- io/io/src/RFile.cxx | 7 +++++ io/io/test/rfile.cxx | 25 +++++++++++++++++ io/io/test/rfile.py | 28 ++++++++++++++++++- 5 files changed, 83 insertions(+), 6 deletions(-) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py index 8ba8a1e579ada..451818e21e8de 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py @@ -58,10 +58,24 @@ class _RFile_Put: def __init__(self, rfile): self._rfile = rfile - def __call__(self, name, obj, title = ""): + def __call__(self, path, obj = None, title = ""): """ Non-templated Put() """ + import ROOT + + if isinstance(path, ROOT.TNamed): + # TNamed-specific overload: arguments are actually Put(namedObj, dirPath). + path, obj = obj, path + fullPath = "" + if isinstance(path, str): + fullPath = f"{path}/" + elif path is not None: + raise TypeError(f"Put(TNamed, dirPath) only accepts a string as the dirPath, not a {type(path)}") + fullPath += obj.GetName() + path = fullPath + title = obj.GetTitle() + objType = type(obj) if isinstance(obj, str): # special case: automatically convert python str to std::string @@ -70,7 +84,7 @@ def __call__(self, name, obj, title = ""): raise TypeError(f"type {objType} is not supported by ROOT I/O") else: className = objType.__cpp_name__ - self._rfile.Put[className](name, obj, title) + self._rfile.Put[className](path, obj, title) def __getitem__(self, template_arg): """ diff --git a/io/io/inc/ROOT/RFile.hxx b/io/io/inc/ROOT/RFile.hxx index bda736b06f45b..ccc5b14eb88c1 100644 --- a/io/io/inc/ROOT/RFile.hxx +++ b/io/io/inc/ROOT/RFile.hxx @@ -8,6 +8,7 @@ #define ROOT7_RFile #include +#include #include #include @@ -276,9 +277,8 @@ class RFile final { void PutUntyped(std::string_view path, const std::type_info &type, const void *obj, std::uint32_t flags, std::string_view title); - /// \see Put template - void PutInternal(std::string_view path, const T &obj, std::uint32_t flags, std::string_view title = "") + void PutInternal(std::string_view path, const T &obj, std::uint32_t flags, std::string_view title) { PutUntyped(path, typeid(T), &obj, flags, title); } @@ -357,7 +357,7 @@ public: return std::unique_ptr(static_cast(obj)); } - /// Puts object `obj` into the file, optionally giving it a title. + /// Puts object `obj` into the file and gives it a title. /// The object will be effectively copied into the file, so any further modifications won't be seen by the object /// inside the file. /// Note that the object is not necessarily written to storage until the RFile is closed or `Flush()` is called. @@ -374,6 +374,11 @@ public: PutInternal(path, obj, /* flags = */ 0, title); } + /// Puts object `namedObj` in the file under the path `dirPath/namedObj.GetName()`, giving it a title equal to + /// `namedObj.GetTitle()`. + /// If `dirPath` is empty (default), the object is put at the top level of the file. + void Put(const class TNamed &namedObj, std::string_view dirPath = ""); + /// Puts an object into the file, overwriting any previously-existing object at that path. /// See Put for more details. /// diff --git a/io/io/src/RFile.cxx b/io/io/src/RFile.cxx index b4c6dc8f282e0..7369e9a2da0aa 100644 --- a/io/io/src/RFile.cxx +++ b/io/io/src/RFile.cxx @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -328,6 +329,12 @@ void *RFile::GetUntyped(std::string_view path, return obj; } +void RFile::Put(const TNamed &namedObj, std::string_view dirPath) +{ + std::string completePath = (dirPath.empty() ? "" : std::string(dirPath) + '/') + namedObj.GetName(); + Put(completePath, namedObj, namedObj.GetTitle()); +} + void RFile::PutUntyped(std::string_view pathSV, const std::type_info &type, const void *obj, std::uint32_t flags, std::string_view title) { diff --git a/io/io/test/rfile.cxx b/io/io/test/rfile.cxx index 60b9eec6fd73f..358b88df92ecf 100644 --- a/io/io/test/rfile.cxx +++ b/io/io/test/rfile.cxx @@ -906,3 +906,28 @@ TEST(RFile, Compression) EXPECT_EQ(file->GetCompressionSettings(), 0); } } + +TEST(RFile, PutTNamed) +{ + FileRaii fileGuard("test_rfile_put_tnamed.root"); + + { + TNamed named1("named1", "title 1"), named2("named2", "title 2"); + auto file = RFile::Recreate(fileGuard.GetPath()); + file->Put(named1); + // Put named2 under a dir + file->Put(named2, "my"); + } + + { + auto file = RFile::Open(fileGuard.GetPath()); + auto named1 = file->Get("named1"); + auto named2 = file->Get("my/named2"); + EXPECT_STREQ(named1->GetTitle(), "title 1"); + EXPECT_STREQ(named2->GetTitle(), "title 2"); + auto key1 = file->GetKeyInfo("named1").value(); + auto key2 = file->GetKeyInfo("my/named2").value(); + EXPECT_EQ(key1.GetTitle(), "title 1"); + EXPECT_EQ(key2.GetTitle(), "title 2"); + } +} diff --git a/io/io/test/rfile.py b/io/io/test/rfile.py index 29a49719263be..7a08576bb9a38 100644 --- a/io/io/test/rfile.py +++ b/io/io/test/rfile.py @@ -152,7 +152,7 @@ def test_getTree(self): fileName = "test_rfile_gettree_py.root" try: - with ROOT.TFile.Open(fileName, "RECREATE") as file: + with ROOT.TFile.Open(fileName, "RECREATE") as _file: tree = ROOT.TTree("tree", "tree") x = array("i", [0]) tree.Branch("myColumn", x, "myColumn/I") @@ -170,6 +170,32 @@ def test_getTree(self): finally: os.remove(fileName) + def test_putTNamed(self): + named1 = ROOT.TNamed("named1", "title 1") + named2 = ROOT.TNamed("named2", "title 2") + + fileName = "test_rfile_tnamed_py.root" + try: + with ROOT.RFile.Recreate(fileName) as file: + file.Put(named1) + # Put named2 under a dir + file.Put(named2, "my") + # If first argument is a TNamed, second argument must be a string (or None) + with self.assertRaises(TypeError): + file.Put(named2, [2, 3]) + + with ROOT.RFile.Open(fileName) as file: + named1 = file.Get("named1") + named2 = file.Get("my/named2") + self.assertEqual(named1.GetTitle(), "title 1") + self.assertEqual(named2.GetTitle(), "title 2") + key1 = file.GetKeyInfo("named1") + key2 = file.GetKeyInfo("my/named2") + self.assertEqual(key1.GetTitle(), "title 1") + self.assertEqual(key2.GetTitle(), "title 2") + finally: + os.remove(fileName) + if __name__ == "__main__": unittest.main() From a2471171521c6eb305a1fc4547ccef765b7627b4 Mon Sep 17 00:00:00 2001 From: silverweed Date: Fri, 23 Oct 2026 09:32:12 +0200 Subject: [PATCH 6/8] [rfile] Replace bool param of Overwrite with a flags bitmask --- io/io/inc/ROOT/RFile.hxx | 21 +++++++++++++-------- io/io/test/rfile.cxx | 4 ++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/io/io/inc/ROOT/RFile.hxx b/io/io/inc/ROOT/RFile.hxx index ccc5b14eb88c1..30a1736ce5b4b 100644 --- a/io/io/inc/ROOT/RFile.hxx +++ b/io/io/inc/ROOT/RFile.hxx @@ -290,9 +290,12 @@ class RFile final { public: enum EListKeyFlags { - kListObjects = 1 << 0, - kListDirs = 1 << 1, - kListRecursive = 1 << 2, + kListObjects = BIT(0), + kListDirs = BIT(1), + kListRecursive = BIT(2), + }; + enum EOverwriteFlags { + kOverwrite_NoNewCycle = BIT(0) }; struct RRecreateOptions { @@ -382,25 +385,27 @@ public: /// Puts an object into the file, overwriting any previously-existing object at that path. /// See Put for more details. /// + /// `overwriteFlags` must contain values from the `EOverwriteFlags` enum. /// If an object already exists at that path,AND it has type `T`, it is kept as a backup cycle - /// unless `createNewCycle` is false. + /// unless `overwriteFlags` contains `kOverwrite_NoNewCycle`. /// If the existing object's type differs from `T`, an exception is thrown. - /// Note that even if `createNewCycle` is false only the *latest* cycle will be deleted. + /// Note that even if no new cycle is created, only the *latest* cycle will be deleted. /// /// \throws ROOT::RException if `path` is already the path of a directory. /// \throws ROOT::RException if the file was opened in read-only mode. template - void Overwrite(std::string_view path, const T &obj, bool createNewCycle = true) + void Overwrite(std::string_view path, const T &obj, std::uint32_t overwriteFlags = 0) { static_assert(!std::is_base_of_v, "Writing TTree with RFile is unsupported."); - Overwrite(path, obj, "", createNewCycle); + Overwrite(path, obj, "", overwriteFlags); } /// Like Overwrite(std::string_view, const T&, bool) but allows giving a title to the object. template - void Overwrite(std::string_view path, const T &obj, std::string_view title, bool createNewCycle = true) + void Overwrite(std::string_view path, const T &obj, std::string_view title, std::uint32_t overwriteFlags = 0) { std::uint32_t flags = kPutFlag_AllowOverwrite; + const bool createNewCycle = !(overwriteFlags & kOverwrite_NoNewCycle); flags |= createNewCycle * kPutFlag_OverwriteKeepCycle; PutInternal(path, obj, flags, title); } diff --git a/io/io/test/rfile.cxx b/io/io/test/rfile.cxx index 358b88df92ecf..410b25b40fa9b 100644 --- a/io/io/test/rfile.cxx +++ b/io/io/test/rfile.cxx @@ -326,7 +326,7 @@ TEST(RFile, PutOverwrite) // Now try overwriting without preserving the existing object std::string s; - file->Overwrite("hist", s, false); + file->Overwrite("hist", s, ROOT::RFile::kOverwrite_NoNewCycle); { // the previous cycle should be gone... auto hist = file->Get("hist;2"); @@ -648,7 +648,7 @@ TEST(RFile, GetAfterOverwriteNoBackup) std::string s = "foo"; file->Put("s", s); s = "bar"; - file->Overwrite("s", s, false); + file->Overwrite("s", s, ROOT::RFile::kOverwrite_NoNewCycle); auto ss = file->Get("s"); EXPECT_EQ(*ss, s); From e711b547aac8d0d318d23a48cbc45a5344decee3 Mon Sep 17 00:00:00 2001 From: silverweed Date: Fri, 23 Oct 2026 10:10:53 +0200 Subject: [PATCH 7/8] [rfile] Fix GetObjectFromKey not considering cycle --- io/io/src/RFile.cxx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/io/io/src/RFile.cxx b/io/io/src/RFile.cxx index 7369e9a2da0aa..c8a9ad683fe54 100644 --- a/io/io/src/RFile.cxx +++ b/io/io/src/RFile.cxx @@ -583,7 +583,8 @@ std::optional RFile::GetKeyInfo(std::string_view path) const void *ROOT::Internal::RFile_GetObjectFromKey(RFile &file, const RKeyInfo &key) { - void *obj = file.GetUntyped(key.GetPath(), key.GetClassName().c_str()); + const auto nameCycle = std::string(key.GetPath()) + ";" + std::to_string(key.GetCycle()); + void *obj = file.GetUntyped(nameCycle, key.GetClassName().c_str()); return obj; } From ffd7a2792ce478dbb70a324fdccae8604e02c506 Mon Sep 17 00:00:00 2001 From: silverweed Date: Fri, 23 Oct 2026 10:10:37 +0200 Subject: [PATCH 8/8] [rfile] add pythonization for Overwrite --- .../python/ROOT/_pythonization/_rfile.py | 40 +++++++++++++++++++ io/io/test/rfile.py | 38 ++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py index 451818e21e8de..8c2e80338e404 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py @@ -93,6 +93,44 @@ def __getitem__(self, template_arg): return self._rfile._OriginalPut[template_arg] +class _RFile_Overwrite: + """ + Allow overwriting objects through the method Overwrite(). + This is pythonized to allow Overwrite() to be called both with and without a template argument. + It also changes Overwrite's bitmask argument with keyword arguments. + """ + + def __init__(self, rfile): + self._rfile = rfile + + def __call__(self, path, obj = None, **kwargs): + """ + Non-templated Overwrite() + """ + + title = kwargs['title'] if 'title' in kwargs else "" + createNewCycle = kwargs['createNewCycle'] if 'createNewCycle' in kwargs else True + + objType = type(obj) + if isinstance(obj, str): + # special case: automatically convert python str to std::string + className = "std::string" + elif not hasattr(objType, "__cpp_name__"): + raise TypeError(f"type {objType} is not supported by ROOT I/O") + else: + className = objType.__cpp_name__ + + from ROOT import RFile + flags = (0 if createNewCycle else 1) * RFile.kOverwrite_NoNewCycle + self._rfile.Overwrite[className](path, obj, title, flags) + + def __getitem__(self, template_arg): + """ + Templated Overwrite() + """ + return self._rfile._OriginalOverwrite[template_arg] + + def _RFileExit(obj, exc_type, exc_val, exc_tb): """ Close the RFile object. @@ -114,6 +152,8 @@ def rfile_open_wrapper(klass, *args): rfile.Get = _RFile_Get(rfile) rfile._OriginalPut = rfile.Put rfile.Put = _RFile_Put(rfile) + rfile._OriginalOverwrite = rfile.Overwrite + rfile.Overwrite = _RFile_Overwrite(rfile) return rfile return rfile_open_wrapper diff --git a/io/io/test/rfile.py b/io/io/test/rfile.py index 7a08576bb9a38..6620e9fdf7d49 100644 --- a/io/io/test/rfile.py +++ b/io/io/test/rfile.py @@ -197,5 +197,43 @@ def test_putTNamed(self): os.remove(fileName) + def test_overwrite(self): + """Objects can be overwritten through the Overwrite method""" + + fileName = "test_rfile_overwrite_py.root" + try: + with ROOT.RFile.Recreate(fileName) as rfile: + hist = ROOT.TH1F("hist", "h1", 100, -10, 10) + hist.FillRandom("gaus", 10) + # We can use Overwrite even if it's the first time we write an object. + rfile.Overwrite("hist", hist, title=hist.GetTitle()) + + hist = ROOT.TH1F("hist", "h2", 200, 0, 10) + hist.FillRandom("gaus", 100) + rfile.Overwrite("hist", hist, title=hist.GetTitle()) + + hist = ROOT.TH1F("hist", "h3", 200, 0, 10) + hist.FillRandom("gaus", 1000) + rfile.Overwrite("hist", hist, title=hist.GetTitle(), createNewCycle=False) + + with ROOT.RFile.Open(fileName) as rfile: + hist = rfile.Get("hist") + self.assertNotEqual(hist, None) + self.assertEqual(hist.GetTitle(), "h3") + self.assertEqual(hist.GetEntries(), 1000) + self.assertTrue(isinstance(hist, ROOT.TH1F)) + + # We should have preserved the backup cycle + hist = rfile.Get("hist;1") + self.assertNotEqual(hist, None) + self.assertEqual(hist.GetTitle(), "h1") + self.assertEqual(hist.GetEntries(), 10) + self.assertTrue(isinstance(hist, ROOT.TH1F)) + + finally: + # os.remove(fileName) + pass + + if __name__ == "__main__": unittest.main()