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