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