Belle II Software prerelease-10-00-00a
datastoreprinter.py
1#!/usr/bin/env python3
2
3
10
11from ROOT import Belle2, kIsPublic, kIsStatic, TVector3, TLorentzVector
12from ROOT.Belle2 import Const
13from ROOT.Math import XYZVector
14from basf2 import Module, B2FATAL
15
16
17def get_public_members(classname):
18 """
19 Return a list of public, non-static member functions for a given classname.
20 The class must exist in the Belle2 namespace and have a ROOT dictionary
21 """
22 if not hasattr(Belle2, classname):
23 return []
24 tclass = getattr(Belle2, classname).Class()
25 members = {e.GetName() for e in tclass.GetListOfMethods()
26 if (e.Property() & kIsPublic) and not (e.Property() & kIsStatic)}
27
28 # filter known members from ClassDef and constructor/destructor
29 # Remove some Dictionary members
30 removed = {
31 "CheckTObjectHashConsistency", "Class", "Class_Name", "Class_Version",
32 "DeclFileLine", "DeclFileName", "Dictionary", "ImplFileLine",
33 "ImplFileName", "IsA", "ShowMembers", "Streamer", "StreamerNVirtual",
34 "operator!=", "operator=", "operator==",
35 # we don't need constructor and destructor either
36 classname, f"~{classname}",
37 }
38 members -= removed
39 return list(sorted(members))
40
41
43 """
44 Class to print contents of a StoreObjPtr or StoreArray.
45
46 This class is intended to print the contents of dataobjects to the standard
47 output to monitor changes to the contents among versions.
48
49 For example:
50
51 >>> printer = DataStorePrinter("MCParticle", ["getVertex"], {"hasStatus": [1, 2, 4]})
52 >>> printer.print()
53
54 will loop over all MCParticle instances in the MCParticles StoreArray and
55 print something like ::
56
57 MCParticle#0
58 getVertex(): (0,0,0)
59 hasStatus(1): True
60 hasStatus(2): False
61 hasStatus(4): False
62
63 for each MCparticle
64 """
65
66 def __init__(self, name, simple, withArgument=None, array=True):
67 """
68 Initialize
69
70 Args:
71 name (str): class name of the DataStore object
72 simple (list): list of member names to print which do not need any additional
73 arguments
74 withArgument (dict or None): dictionary of member names and a list of
75 all argument combinations to call them.
76 array (bool): if True we print a StoreArray, otherwise a single StoreObjPtr
77 """
78
79 self.name = name
80
81 self.array = array
82
84
85 # add the simple members to the list of members to call with empty
86 # arguments
87 for member in simple:
88 self.object_members.append((member, [], None, None))
89
90 # and add the members with Argument
91 if withArgument:
92 for member, arguments in withArgument.items():
93 for args in arguments:
94 # convert args to a list
95 if not isinstance(args, list) or isinstance(args, tuple):
96 args = [args]
97 # and add (name,args,display,callback) tuple
98 self.object_members.append((member, args, None, None))
99
100 # sort them by member name to have fixed order: python sort is
101 # guaranteed to be stable so different calls to the same member will
102 # remain in same relative order
103 self.object_members.sort(key=lambda x: x[0])
104
105 def add_member(self, name, arguments=None, print_callback=None, display=None):
106 """
107 Add an additional member to be printed.
108
109 Args:
110 name (str): name of the member
111 arguments (list or callable): arguments to pass to the member when calling it
112 If this is a callable object then the function is called with
113 the object as first argument and the member name to be tested as
114 second argument. The function is supposed to return the list of
115 arguments to pass to the member when calling. Possible return
116 values for the callable are:
117
118 * a `list` of arguments to be passed to the member. An empty
119 `list` means to call the member with no arguments.
120 * a `tuple` of `lists <list>` to call the member once for each
121 list of arguments in the tuple
122 * `None` or an empty tuple to not call the member at all
123 print_callback (function or None): if not None a function to print
124 the result of the member call. The function will be called with
125 the arguments (name, arguments, result) and should print the
126 result on stdout without any additional information.
127 display (str or None): display string to use when printing member call
128 info instead of function name + arguments. If it is not given
129 the default output will be ``{membername}({arguments}):``
130 followed by the result.
131 """
132 if arguments is None:
133 arguments = []
134 self.object_members.append((name, arguments, print_callback, display))
135 # return self for method chaining
136 return self
137
138 def print(self):
139 """Print all the objects currently existing"""
140 if self.array:
141 data = Belle2.PyStoreArray(self.name + "s")
142 if not data.isValid():
143 print(f"No data for {self.name}")
144 else:
145 for i, obj in enumerate(data):
146 self._printObj(obj, i)
147 else:
148 obj = Belle2.PyStoreObj(self.name)
149 if not obj.isValid():
150 print(f"No data for {self.name}")
151 else:
152 if obj:
153 self._printObj(obj.obj())
154
155 def print_untested(self):
156 """Print all the public member functions we will not test"""
157 members = get_public_members(self.name)
158 tested = {e[0] for e in self.object_members}
159 for member in members:
160 if member in tested:
161 continue
162 print(f"Untested method {self.name}::{member}")
163
164 def _printObj(self, obj, index=None):
165 """Print all defined members for each object with given index.
166 If we print a StoreObjPtr then index is None and this function is called
167 once. If we print StoreArrays this function is called once for each
168 entry in the array with index set to the position in the array
169 """
170 # print array index? If yes then add it to the output
171 if index is not None:
172 index = f"#{int(index)}"
173 else:
174 index = ""
175
176 print(f"{self.name}{index}:")
177
178 # loop over all defined member/argument combinations
179 # and print "member(arguments): result" for each
180 for name, arguments, callback, display in self.object_members:
181 # if arguments is callable it means we need to call it to determine
182 # the arguments
183 if callable(arguments):
184 all_args = arguments(obj, name)
185 # None means we don't called the member this time
186 if all_args is None:
187 continue
188 # list is one set of arguments, tuple(list) is n set of
189 # arguments. So convert list into tuple of length 1 to call
190 # member once
191 if isinstance(all_args, list):
192 all_args = (all_args,)
193 else:
194 # if arguments is not callable we assume it's one set of
195 # arguments
196 all_args = (arguments,)
197
198 for args in all_args:
199 result = getattr(obj, name)(*args)
200 # display can be used to override what is printed for the member. If
201 # so, use it here
202 if display is not None:
203 print(" " + display + ": ", end="")
204 else:
205 # otherwise just print name and arguments
206 print(f" {name}({','.join(map(str, args))}): ", end="")
207 # if a callback is set the callback is used to print the result
208 if callback is not None:
209 print("", end="", flush=True)
210 callback(name, args, result)
211 # otherwise use default function
212 else:
213 self._printResult(result)
214
215 def _printResult(self, result, depth=0, weight=None):
216 """ Print the result of calling a certain member.
217 As we want the test to be independent of memory we have to be a bit careful
218 how to not just print() but check whether the object is maybe a pointer to another
219 DataStore object or if it is a TObject with implements Print().
220 Also, call recursively std::pair
221
222 Args:
223 result: object to print
224 depth (int): depth for recursive printing, controls the level of indent
225 weight (float or None): weight to print in addition to object, only used for
226 relations
227 """
228 # are we in recursion? if so indent the output
229 if depth:
230 print(" " * (depth + 1), end="")
231
232 if weight is not None:
233 weight = f" (weight: {weight:.6g})"
234 else:
235 weight = ""
236
237 # is it another RelationsObject? print array name and index
238 if hasattr(result, "getArrayName") and hasattr(result, "getArrayIndex"):
239 if not result:
240 print(f"-> NULL{weight}")
241 else:
242 print(f"-> {result.getArrayName()}#{int(result.getArrayIndex())}{weight}")
243 # special case for TMatrix like types to make them more space efficient
244 elif hasattr(result, "GetNrows") and hasattr(result, "GetNcols"):
245 print(weight, end="")
246 for row in range(result.GetNrows()):
247 print("\n" + " " * (depth + 2), end="")
248 for col in range(result.GetNcols()):
249 print(f"{result(row, col):13.6e} ", end="")
250
251 print()
252 # or is it a TVector3 or TLorentzVector?
253 elif isinstance(result, TVector3):
254 print("(" + ",".join(f"{result[i]:.6g}" for i in range(3)) + ")")
255 elif isinstance(result, XYZVector):
256 print(f"({result.X():.6g},{result.Y():.6g},{result.Z():.6g})")
257 elif isinstance(result, TLorentzVector):
258 print("(" + ",".join(f"{result[i]:.6g}" for i in range(4)) + ")")
259 # or, does it look like a std::pair?
260 elif hasattr(result, "first") and hasattr(result, "second"):
261 print(f"pair{weight}")
262 self._printResult(result.first, depth + 1)
263 self._printResult(result.second, depth + 1)
264 # or, could it be a std::vector like container? But ROOT might wrap a std::string so if it has npos assume it's a string
265 elif (hasattr(result, "size") and hasattr(result, "begin")
266 and hasattr(result, "end")) and not hasattr(result, "npos") \
267 and not isinstance(result, Const.DetectorSet):
268 print(f"size({int(result.size())}){weight}")
269 # if it is a RelationVector we also want to print the weights. So
270 # check whether we have weights and pass them to the _printResult
271 weight_getter = getattr(result, "weight", None)
272 weight = None
273 # loop over all elements and print the elements with one level more
274 # indentation
275 for i, e in enumerate(result):
276 if weight_getter is not None:
277 weight = weight_getter(i)
278 self._printResult(e, depth + 1, weight=weight)
279 # print floats with 6 valid digits
280 elif isinstance(result, float):
281 print(f"{result:.6g}{weight}")
282 # print char as int
283 elif isinstance(result, str) and len(result) == 1:
284 print(ord(result), weight, sep="")
285 # ok, in any case we can just print it
286 else:
287 print(result, weight, sep="")
288
289
290# ok, now we just need a small class to call all the printer objects for each
291# event
292class PrintObjectsModule(Module):
293
294 """Call all DataStorePrinter objects in for each event"""
295
296 def __init__(self, objects_to_print, print_untested=False):
297 """
298 Initialize
299
300 Args:
301 objects_to_print (list): list of object to print
302 """
303
304 self.objects_to_print = objects_to_print
305
306 self.print_untested = print_untested
307 super().__init__()
308
309 def initialize(self):
310 """Print all untested members if requested"""
311 if not self.print_untested:
312 return
313
314 for printer in self.objects_to_print:
315 printer.print_untested()
316
317 def event(self):
318 """print the contents of the mdst mdst_dataobjects"""
319 try:
320 for printer in self.objects_to_print:
321 printer.print()
322 except Exception as e:
323 B2FATAL("Error in datastore printer: ", e)
A (simplified) python wrapper for StoreArray.
a (simplified) python wrapper for StoreObjPtr.
Definition PyStoreObj.h:67
__init__(self, name, simple, withArgument=None, array=True)
array
if True we print a StoreArray, otherwise a single StoreObjPtr
_printResult(self, result, depth=0, weight=None)
name
class name of the datastore object
add_member(self, name, arguments=None, print_callback=None, display=None)
list object_members
list of object members to call and print their results
__init__(self, objects_to_print, print_untested=False)