Skip to content

Commit fcff6da

Browse files
Grigori RybkineGrigori Rybkine
authored andcommitted
[ntuple][python][ATLAS experiment] Re-Implement context management protocol for RNTupleReader/Writer
bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rntuple.py: add __enter__ method - returns self (an instance of RNTupleReader/RNTupleWriter), __exit__ method - calls RNTupleReader/RNTupleWriter destructor (if not destructed yet). tree/ntuple/test/ntuple_basics.py: update tests
1 parent a9cc13f commit fcff6da

2 files changed

Lines changed: 67 additions & 118 deletions

File tree

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_rntuple.py

Lines changed: 21 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -124,51 +124,6 @@ def pythonize_RNTupleModel(klass):
124124
klass.GetDefaultEntry = _RNTupleModel_GetDefaultEntry
125125

126126
klass.MakeField = MethodTemplateGetter(klass.MakeField, _RNTupleModel_MakeField)
127-
128-
129-
# Wrapper class used for RNTupleReader and RNTupleWriter.
130-
# It deletes the underlying smart pointer on context manager exit and ensures that the inner object becomes
131-
# inaccessible by raising an error every time an attribute of the object is accessed.
132-
# It also raises an error if `with` statements using the same object are nested.
133-
# This is a generic class and can in principle be used with any class that needs this behavior.
134-
class RNTupleContextWrapper:
135-
def __init__(self, inner, pretty_name, on_ctx_enter = None, on_ctx_exit = None):
136-
self._inner = inner
137-
self._pretty_name = pretty_name
138-
self._closed = False
139-
self._in_context = False
140-
self._on_ctx_enter = on_ctx_enter
141-
self._on_ctx_exit = on_ctx_exit
142-
143-
def __getattribute__(self, name):
144-
if name.startswith('_'):
145-
return super().__getattribute__(name)
146-
147-
if super().__getattribute__("_closed"):
148-
raise RuntimeError(
149-
f"cannot access {super().__getattribute__('_pretty_name')} after the `with` statement is exited"
150-
)
151-
return super().__getattribute__("_inner").__getattribute__(name)
152-
153-
def __enter__(self, *args):
154-
if self._on_ctx_enter:
155-
self._on_ctx_enter(self._inner)
156-
if self._closed:
157-
raise RuntimeError(f"cannot reuse {self._pretty_name} in multiple `with` statements")
158-
if self._in_context:
159-
raise RuntimeError(f"cannot nest `with` statements using the same {self._pretty_name}")
160-
161-
self._in_context = True
162-
return self
163-
164-
def __exit__(self, *args):
165-
assert self._in_context and not self._closed
166-
if self._on_ctx_exit:
167-
self._on_ctx_exit(self._inner)
168-
self._in_context = False
169-
self._closed = True
170-
self._inner.__smartptr__().reset()
171-
return False
172127

173128

174129
def _RNTupleReader_Open(maybe_model, *args):
@@ -177,7 +132,7 @@ def _RNTupleReader_Open(maybe_model, *args):
177132
maybe_model = maybe_model.Clone()
178133
import ROOT
179134

180-
return RNTupleContextWrapper(ROOT.RNTupleReader._Open(maybe_model, *args), "RNTupleReader")
135+
return ROOT.RNTupleReader._Open(maybe_model, *args)
181136

182137

183138
def _RNTupleReader_LoadEntry(self, *args):
@@ -186,6 +141,18 @@ def _RNTupleReader_LoadEntry(self, *args):
186141
return self._LoadEntry(*args)
187142

188143

144+
def _RNTupleReaderWriter___enter__(self):
145+
"""Context management protocol. Returns self (an instance of RNTupleReader/RNTupleWriter)."""
146+
if not self.__smartptr__():
147+
raise ValueError(f"I/O operation on destructed {type(self).__name__!r}.")
148+
return self
149+
150+
151+
def _RNTupleReaderWriter___exit__(self, *args):
152+
"""Context management protocol. Calls RNTupleReader/RNTupleWriter destructor (if not destructed yet)."""
153+
self.__smartptr__().reset()
154+
155+
189156
@pythonization("RNTupleReader", ns="ROOT")
190157
def pythonize_RNTupleReader(klass):
191158
klass._Open = klass.Open
@@ -194,13 +161,16 @@ def pythonize_RNTupleReader(klass):
194161
klass._LoadEntry = klass.LoadEntry
195162
klass.LoadEntry = _RNTupleReader_LoadEntry
196163

164+
klass.__enter__ = _RNTupleReaderWriter___enter__
165+
klass.__exit__ = _RNTupleReaderWriter___exit__
166+
197167

198168
def _RNTupleWriter_Append(model, *args):
199169
# In Python, the user cannot create REntries directly from a model, so we can safely clone it and avoid destructively passing the user argument.
200170
model = model.Clone()
201171
import ROOT
202172

203-
return RNTupleContextWrapper(ROOT.RNTupleWriter._Append(model, *args), "RNTupleWriter", on_ctx_exit = _RNTupleWriter_exit)
173+
return ROOT.RNTupleWriter._Append(model, *args)
204174

205175

206176
def _RNTupleWriter_Recreate(model_or_fields, *args):
@@ -209,7 +179,7 @@ def _RNTupleWriter_Recreate(model_or_fields, *args):
209179
model_or_fields = model_or_fields.Clone()
210180
import ROOT
211181

212-
return RNTupleContextWrapper(ROOT.RNTupleWriter._Recreate(model_or_fields, *args), "RNTupleWriter", on_ctx_exit = _RNTupleWriter_exit)
182+
return ROOT.RNTupleWriter._Recreate(model_or_fields, *args)
213183

214184

215185
def _RNTupleWriter_Fill(self, *args):
@@ -218,10 +188,6 @@ def _RNTupleWriter_Fill(self, *args):
218188
return self._Fill(*args)
219189

220190

221-
def _RNTupleWriter_exit(self):
222-
self.CommitDataset()
223-
224-
225191
@pythonization("RNTupleWriter", ns="ROOT")
226192
def pythonize_RNTupleWriter(klass):
227193
klass._Append = klass.Append
@@ -231,3 +197,6 @@ def pythonize_RNTupleWriter(klass):
231197

232198
klass._Fill = klass.Fill
233199
klass.Fill = _RNTupleWriter_Fill
200+
201+
klass.__enter__ = _RNTupleReaderWriter___enter__
202+
klass.__exit__ = _RNTupleReaderWriter___exit__

tree/ntuple/test/ntuple_basics.py

Lines changed: 46 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -14,33 +14,37 @@ def test_write_read(self):
1414
model.MakeField["int"]("f")
1515
model.MakeField["std::string"]("mystr")
1616

17+
nentries = 2
1718
with ROOT.RNTupleWriter.Recreate(model, "ntpl", "test_ntuple_py_write_read.root") as writer:
1819
entry = writer.CreateEntry()
19-
entry["f"] = 42
20-
entry["mystr"] = "string stored in RNTuple"
21-
writer.Fill(entry)
20+
for i in range(nentries):
21+
entry["f"] = i
22+
entry["mystr"] = f"{i} string stored in RNTuple"
23+
writer.Fill(entry)
2224
# The model should not have been destroyed (a clone has been used).
2325
self.assertFalse(model.IsFrozen())
2426

25-
# Accessing the writer after the context manager is an error
26-
with self.assertRaisesRegex(RuntimeError, "cannot access RNTupleWriter after"):
27+
# Upon exiting the context, the writer is destructed
28+
with self.assertRaisesRegex(ReferenceError, "attempt to access a null-pointer"):
2729
writer.GetNEntries()
2830

2931
with ROOT.RNTupleReader.Open("ntpl", "test_ntuple_py_write_read.root") as reader:
30-
self.assertEqual(reader.GetNEntries(), 1)
32+
self.assertEqual(reader.GetNEntries(), nentries)
3133
entry = reader.CreateEntry()
32-
reader.LoadEntry(0, entry)
33-
self.assertEqual(entry["f"], 42)
34-
self.assertEqual(entry["mystr"], "string stored in RNTuple")
35-
36-
# Entry values are still accessible after the reader is gone
37-
self.assertEqual(entry["f"], 42)
38-
self.assertEqual(entry["mystr"], "string stored in RNTuple")
39-
40-
# Accessing the reader after the context manager is an error
41-
with self.assertRaisesRegex(RuntimeError, "cannot access RNTupleReader after"):
34+
for i in reader:
35+
reader.LoadEntry(i, entry)
36+
with self.subTest(i=i):
37+
self.assertEqual(entry["f"], i)
38+
self.assertEqual(entry["mystr"], f"{i} string stored in RNTuple")
39+
40+
# Upon exiting the context, the reader is destructed
41+
with self.assertRaisesRegex(ReferenceError, "attempt to access a null-pointer"):
4242
reader.GetNEntries()
4343

44+
# Last entry values are still accessible after the reader is destructed
45+
self.assertEqual(entry["f"], nentries - 1)
46+
self.assertEqual(entry["mystr"], f"{nentries - 1} string stored in RNTuple")
47+
4448
def test_write_fields(self):
4549
"""Can create writer with on-the-fly model"""
4650

@@ -73,14 +77,15 @@ def test_append_open(self):
7377
self.assertFalse(model.IsFrozen())
7478

7579
with ROOT.TFile.Open("test_ntuple_py_append.root") as f:
76-
reader = ROOT.RNTupleReader.Open(f["ntpl"])
77-
self.assertEqual(reader.GetNEntries(), 1)
78-
entry = reader.CreateEntry()
79-
reader.LoadEntry(0, entry)
80-
self.assertEqual(entry["f"], 42)
80+
with ROOT.RNTupleReader.Open(f["ntpl"]) as reader:
81+
self.assertEqual(reader.GetNEntries(), 1)
82+
entry = reader.CreateEntry()
83+
reader.LoadEntry(0, entry)
84+
self.assertEqual(entry["f"], 42)
8185

82-
# Entry values are still accessible after the reader is gone
83-
self.assertEqual(entry["f"], 42)
86+
with self.subTest(repr(reader)):
87+
self.assertFalse(reader, "RNTupleReader destructed")
88+
self.assertEqual(entry["f"], 42, "Entry values still accessible")
8489

8590
def test_read_model(self):
8691
"""Can impose a model when reading."""
@@ -100,8 +105,7 @@ def test_read_model(self):
100105
entry = reader.CreateEntry()
101106
if not platform.system() == "Windows":
102107
# TODO: re-enable it on Windows once the exception handling is fixed
103-
with self.assertRaises(Exception):
104-
# Field f2 does not exist in imposed model
108+
with self.assertRaises(ROOT.RException, msg="Field f2 does not exist in imposed model"):
105109
entry["f2"] = 42
106110

107111
def test_forbid_writing_wrong_type(self):
@@ -117,8 +121,12 @@ class WrongClass: ...
117121
with self.assertRaises(TypeError):
118122
entry["mystr"] = WrongClass()
119123

120-
def test_nested_ctxmanager(self):
121-
"""Nesting context managers of the same object is an error"""
124+
def test_singleuse_ctxmanager(self):
125+
"""RNTupleReader/RNTupleWriter context managers are single use context managers.
126+
127+
Upon exiting the context, they are destructed.
128+
They are not reentrant - cannot be used in nested 'with' statements,
129+
or are not reusable - cannot be used multiple times."""
122130

123131
try:
124132
fileName = "test_ntuple_nested_ctxmanager_py.root"
@@ -127,49 +135,21 @@ def test_nested_ctxmanager(self):
127135
writer = ROOT.RNTupleWriter.Recreate(model, "ntpl", fileName)
128136
with writer as w1:
129137
entry1 = w1.CreateEntry()
130-
with self.assertRaisesRegex(RuntimeError, "cannot nest `with`"):
131-
with writer as w2:
132-
entry2 = w2.CreateEntry()
133-
entry1["f"] = 2
134-
entry2["f"] = 4
135-
w2.Fill(entry2)
136-
w1.Fill(entry1)
137-
138-
reader = ROOT.RNTupleReader.Open("ntpl", fileName)
139-
with reader as r1:
140-
with self.assertRaisesRegex(RuntimeError, "cannot nest `with`"):
141-
with reader as r2:
142-
print(r2.GetNEntries())
143-
print(r1.GetNEntries())
144-
145-
finally:
146-
import os
147-
os.remove(fileName)
148-
149-
def test_weird_ctxmanager(self):
150-
"""Using an existing object with a context manager"""
151-
152-
try:
153-
fileName = "test_ntuple_weird_ctxmanager_py.root"
154-
model = ROOT.RNTupleModel.Create()
155-
model.MakeField["int"]("f")
156-
writer = ROOT.RNTupleWriter.Recreate(model, "ntpl", fileName)
157-
entry = writer.CreateEntry()
158-
with writer as w1:
159-
w1.Fill(entry)
160-
161-
with self.assertRaisesRegex(RuntimeError, "after the `with` statement"):
162-
writer.Fill(entry)
138+
entry1["f"] = 2
139+
with writer as w2:
140+
entry2 = w2.CreateEntry()
141+
entry2["f"] = 4
142+
w2.Fill(entry2)
143+
with self.assertRaisesRegex((ReferenceError, TypeError), "attempt to access a null-pointer"):
144+
w1.Fill(entry1)
163145

164146
reader = ROOT.RNTupleReader.Open("ntpl", fileName)
165147
with reader as r1:
166-
with self.assertRaisesRegex(RuntimeError, "cannot nest `with`"):
167-
with reader as r2:
168-
print(r2.GetNEntries())
169-
print(r1.GetNEntries())
148+
with reader as r2:
149+
print(r2.GetNEntries())
150+
with self.assertRaisesRegex(ReferenceError, "attempt to access a null-pointer"):
151+
print(r1.GetNEntries())
170152

171153
finally:
172154
import os
173155
os.remove(fileName)
174-
175-

0 commit comments

Comments
 (0)