Belle II Software  release-08-01-10
_constwrapper.py
1 #!/usr/bin/env python3
2 
3 
10 
11 """
12 Modify the PyDBObj and PyDBArray classes to return read only objects to prevent
13 accidental changes to the conditions data.
14 
15 This module does not contain any public functions or variables, it just
16 modifies the ROOT interfaces for PyDBObj and PyDBArray to only return read only
17 objects.
18 """
19 
20 # we need to patch PyDBObj so lets import the ROOT cppyy backend.
21 import cppyy
22 
23 # However importing ROOT.kIsConstMethod and kIsStatic is a bad idea here since
24 # it triggers final setup of ROOT and thus starts a gui thread (probably).
25 # So we take them as literal values from TDictionary.h. We do have a unit test
26 # to make sure they don't change silently though.
27 
28 
29 _ROOT_kIsPublic = 0x00000200
30 
31 _ROOT_kIsStatic = 0x00004000
32 
33 _ROOT_kIsConstMethod = 0x10000000
34 
35 
36 # copy and enhanced version of ROOT.pythonization
37 def pythonization(lazy=True, namespace=""):
38  """
39  Pythonizor decorator to be used in pythonization modules for pythonizations.
40  These pythonizations functions are invoked upon usage of the class.
41  Parameters
42  ----------
43  lazy : boolean
44  If lazy is true, the class is pythonized upon first usage, otherwise
45  upon import of the ROOT module.
46  """
47  def pythonization_impl(fn):
48  """
49  The real decorator. This structure is adopted to deal with parameters
50  fn : function
51  Function that implements some pythonization.
52  The function must accept two parameters: the class
53  to be pythonized and the name of that class.
54  """
55  if lazy:
56  cppyy.py.add_pythonization(fn, namespace)
57  else:
58  fn()
59  return pythonization_impl
60 
61 
62 def _make_tobject_const(obj):
63  """
64  Make a TObject const: Disallow setting any attributes and calling any
65  methods which are not declared as const. This affects any reference to this
66  object in python and stays permanent. Once called this particular instance
67  will be readonly everywhere and will remain like this.
68 
69  This is done by replacing all setter functions with function objects that
70  just raise an attribute error when called. The class will be still the same
71  """
72  # nothing to do if None
73  if obj is None:
74  return obj
75 
76  try:
77 
78  non_const = [m.GetName() for m in obj.Class().GetListOfMethods() if (m.Property() & _ROOT_kIsPublic)
79  and not (m.Property() & (_ROOT_kIsConstMethod | _ROOT_kIsStatic))]
80  except AttributeError:
81  raise ValueError("Object does not have a valid dictionary: %r" % obj)
82 
83  # Override all setters to just raise an exception
84  for name in non_const:
85  def __proxy(*args, **argk):
86  """raise attribute error when called"""
87  raise AttributeError(f"{obj} is readonly and method '{name}' is not const")
88 
89  setattr(obj, name, __proxy)
90 
91  # and return the modified object
92  return obj
93 
94 
95 def _PyDBArray__iter__(self):
96  """Provide iteration over Belle2::PyDBArray. Needs to be done here since we
97  want to make all returned classes read only"""
98  for i in range(len(self)):
99  yield self[i]
100 
101 
102 @pythonization(namespace="Belle2")
103 def _pythonize(klass, name):
104  """Adjust the python interface of some Py* classes"""
105  if not name.startswith("Py"):
106  return
107 
108  if name == "PyDBObj":
109  # now replace the PyDBObj getter with one that returns non-modifiable objects.
110  # This is how root does it in ROOT.py so let's keep that
111  klass.obj = lambda self: _make_tobject_const(self._obj())
112  # and allow to use it most of the time without calling obj() like the ->
113  # indirection in C++
114  klass.__getattr__ = lambda self, name: getattr(self.obj(), name)
115  # and make sure that we can iterate over the items in the class
116  # pointed to if it allows iteration
117  klass.__iter__ = lambda self: iter(self.obj())
118  elif name == "PyDBArray":
119  # also make item access in DBArray readonly
120  klass.__getitem__ = lambda self, i: _make_tobject_const(self._get(i))
121  # and supply an iterator
122  klass.__iter__ = _PyDBArray__iter__
123  elif name == "PyStoreObj":
124  # make sure that we can iterate over the items in the class
125  # pointed to if it allows iteration
126  klass.__iter__ = lambda self: iter(self.obj())