From 792f4fd66a4b8a5392f866fb4122977b0fcbe7b7 Mon Sep 17 00:00:00 2001 From: silverweed Date: Mon, 20 Jul 2026 14:06:26 +0200 Subject: [PATCH 1/9] [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..0c55d2441132c 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 8abeefdab791a445d186722c1605cf99ed2ea11a Mon Sep 17 00:00:00 2001 From: silverweed Date: Tue, 21 Jul 2026 16:36:17 +0200 Subject: [PATCH 2/9] [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 d775a526784438659ed67089ba9a8747584b2dff Mon Sep 17 00:00:00 2001 From: silverweed Date: Tue, 21 Jul 2026 16:37:36 +0200 Subject: [PATCH 3/9] [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 0c55d2441132c..f8d142f3bebf5 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 0a43a6c191d0c885be8b73a02b8f654b34ea4d0d Mon Sep 17 00:00:00 2001 From: silverweed Date: Tue, 21 Jul 2026 17:06:28 +0200 Subject: [PATCH 4/9] [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 bb36ebfb66720..bc3a5259501a9 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py @@ -120,7 +120,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() """ @@ -132,7 +132,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 f8d142f3bebf5..7aa85c0ed08be 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 31eef07f7f886fa849d1101c1dd8dd728ae81c60 Mon Sep 17 00:00:00 2001 From: silverweed Date: Fri, 23 Oct 2026 09:32:12 +0200 Subject: [PATCH 5/9] [rfile] Replace bool param of Overwrite with a flags bitmask --- io/io/inc/ROOT/RFile.hxx | 22 ++++++++++++++-------- io/io/test/rfile.cxx | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/io/io/inc/ROOT/RFile.hxx b/io/io/inc/ROOT/RFile.hxx index 7aa85c0ed08be..09bbb74fdc450 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 @@ -290,9 +291,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 { @@ -377,25 +381,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 60b9eec6fd73f..e6af75e586a6a 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 8b6e9a36ab6d765e3aa62f9cdb127dcbe6b851b0 Mon Sep 17 00:00:00 2001 From: silverweed Date: Fri, 23 Oct 2026 10:10:53 +0200 Subject: [PATCH 6/9] [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 b4c6dc8f282e0..d7ad9217dbfa7 100644 --- a/io/io/src/RFile.cxx +++ b/io/io/src/RFile.cxx @@ -576,7 +576,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 8aacd5b724370c2d3a33b065715e8852f1c5bc6b Mon Sep 17 00:00:00 2001 From: silverweed Date: Fri, 23 Oct 2026 10:10:37 +0200 Subject: [PATCH 7/9] [rfile] add pythonization for Overwrite --- .../python/ROOT/_pythonization/_rfile.py | 44 +++++++++++++++++++ io/io/test/rfile.py | 38 ++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py index bc3a5259501a9..5e2158038bb73 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rfile.py @@ -31,6 +31,13 @@ file.Overwrite("myHisto", myOtherHisto) \endcode +When overwriting, you may specify a title and whether to keep a backup cycle of a potentially-overwritten object. +Note that Overwrite will work just like Put if there is no object to overwrite. +\code{.py} +# Overwrite object without preserving it as a backup cycle +file.Overwrite("myHisto", myOtherHisto, "my new histo", createNewCycle = False) +\endcode + You can iterate the metadata of all objects inside an RFile using the ListKeys method: \code{.py} for key in file.ListKeys(): @@ -141,6 +148,41 @@ 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, title = "", *, createNewCycle = True): + """ + Non-templated Overwrite() + """ + + 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. @@ -162,6 +204,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 29a49719263be..68d6faa52cdcc 100644 --- a/io/io/test/rfile.py +++ b/io/io/test/rfile.py @@ -171,5 +171,43 @@ def test_getTree(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() From f12ee3eeb0512ecc6b84169ca08de532f8e250c2 Mon Sep 17 00:00:00 2001 From: silverweed Date: Tue, 27 Oct 2026 10:05:54 +0100 Subject: [PATCH 8/9] [rfile] exclude faulty python test on mac 15 It seems that on mac15 x86 assertRaises doesn't work consistently. We disable the test on that platform for now. --- io/io/test/rfile.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/io/io/test/rfile.py b/io/io/test/rfile.py index 68d6faa52cdcc..778ab0170841f 100644 --- a/io/io/test/rfile.py +++ b/io/io/test/rfile.py @@ -5,6 +5,11 @@ import ROOT +def platform_supports_assertRaises(): + # TODO: re-enable it on Windows once the exception handling is fixed + # TODO: remove mac 15 exception once we stop supporting it + return platform.system() != "Windows" and (platform.system() != "Darwin" or not platform.mac_ver()[0].startswith('15')) + class RFileTests(unittest.TestCase): def test_open_for_reading(self): """A RFile can read a ROOT file created by TFile""" @@ -24,8 +29,7 @@ def test_open_for_reading(self): self.assertEqual(rfile.Get[ROOT.TH1F]("hist"), None) self.assertNotEqual(rfile.Get[ROOT.TH1]("hist"), None) - if not platform.system() == "Windows": - # TODO: re-enable it on Windows once the exception handling is fixed + if platform_supports_assertRaises(): with self.assertRaises(ROOT.RException): # This should fail because the file was opened as read-only rfile.Put("foo", hist) @@ -41,8 +45,7 @@ def test_writing_reading(self): hist = ROOT.TH1D("hist", "", 100, -10, 10) hist.FillRandom("gaus", 10) rfile.Put("hist", hist) - if not platform.system() == "Windows": - # TODO: re-enable it on Windows once the exception handling is fixed + if platform_supports_assertRaises(): with self.assertRaises(ROOT.RException): rfile.Put("hist/2", hist) @@ -129,12 +132,14 @@ def test_putUnsupportedType(self): try: with ROOT.RFile.Recreate(fileName) as rfile: # Storing integers is unsupported - with self.assertRaises(TypeError): - rfile.Put("foo", 2) + if platform_supports_assertRaises(): + with self.assertRaises(TypeError): + rfile.Put("foo", 2) # Storing lists without an explicit template is unsupported - with self.assertRaises(TypeError): - rfile.Put("bar", [2, 3]) + if platform_supports_assertRaises(): + with self.assertRaises(TypeError): + rfile.Put("bar", [2, 3]) # Storing lists with an explicit template is supported rfile.Put["std::vector"]("bar", [2, 3]) From e5ee553e818177c39ffab21dd0a401bdf12df1fb Mon Sep 17 00:00:00 2001 From: silverweed Date: Tue, 27 Oct 2026 16:19:13 +0100 Subject: [PATCH 9/9] [rfile] exclude a dependent python rfile test when dataframe is off --- tutorials/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tutorials/CMakeLists.txt b/tutorials/CMakeLists.txt index 932b3de90b8ea..2ec7daf493ea6 100644 --- a/tutorials/CMakeLists.txt +++ b/tutorials/CMakeLists.txt @@ -156,6 +156,7 @@ if (NOT dataframe) list(APPEND dataframe_veto io/ntuple/ntpl008_import.C) # RFile tutorials depending on RDataFrame list(APPEND dataframe_veto io/rfile/rfile003_ttree.C) + list(APPEND dataframe_veto io/rfile/rfile003_ttree.py) endif() if(NOT sqlite)