Belle II Software development
_constwrapper.py
1#!/usr/bin/env python3
2
3
10
11"""
12Modify the PyDBObj and PyDBArray classes to return read only objects to prevent
13accidental changes to the conditions data.
14Also modify the functions fillArray and readArray of the PyStoreArray class to
15ensure a safe and simple usage of those function.
16
17This module does not contain any public functions or variables, it just
18modifies the ROOT interfaces for PyDBObj and PyDBArray to only return read only
19objects.
20"""
21
22# we need to patch PyDBObj so lets import the ROOT cppyy backend.
23import cppyy
24
25# However importing ROOT.kIsConstMethod and kIsStatic is a bad idea here since
26# it triggers final setup of ROOT and thus starts a gui thread (probably).
27# So we take them as literal values from TDictionary.h. We do have a unit test
28# to make sure they don't change silently though.
29
30
31_ROOT_kIsPublic = 0x00000200
32
33_ROOT_kIsStatic = 0x00004000
34
35_ROOT_kIsConstMethod = 0x10000000
36
37
38# copy and enhanced version of ROOT.pythonization
39def pythonization(lazy=True, namespace=""):
40 """
41 Pythonizor decorator to be used in pythonization modules for pythonizations.
42 These pythonizations functions are invoked upon usage of the class.
43 Parameters
44 ----------
45 lazy : boolean
46 If lazy is true, the class is pythonized upon first usage, otherwise
47 upon import of the ROOT module.
48 """
49 def pythonization_impl(fn):
50 """
51 The real decorator. This structure is adopted to deal with parameters
52 fn : function
53 Function that implements some pythonization.
54 The function must accept two parameters: the class
55 to be pythonized and the name of that class.
56 """
57 if lazy:
58 cppyy.py.add_pythonization(fn, namespace)
59 else:
60 fn()
61 return pythonization_impl
62
63
64def _make_tobject_const(obj):
65 """
66 Make a TObject const: Disallow setting any attributes and calling any
67 methods which are not declared as const. This affects any reference to this
68 object in python and stays permanent. Once called this particular instance
69 will be readonly everywhere and will remain like this.
70
71 This is done by replacing all setter functions with function objects that
72 just raise an attribute error when called. The class will be still the same
73 """
74 # nothing to do if None
75 if obj is None:
76 return obj
77
78 try:
79
80 non_const = [m.GetName() for m in obj.Class().GetListOfMethods() if (m.Property() & _ROOT_kIsPublic)
81 and not (m.Property() & (_ROOT_kIsConstMethod | _ROOT_kIsStatic))]
82 except AttributeError:
83 raise ValueError(f"Object does not have a valid dictionary: {obj!r}")
84
85 # Override all setters to just raise an exception
86 for name in non_const:
87 def __proxy(*args, **argk):
88 """raise attribute error when called"""
89 raise AttributeError(f"{obj} is readonly and method '{name}' is not const")
90
91 setattr(obj, name, __proxy)
92
93 # and return the modified object
94 return obj
95
96
97def _PyDBArray__iter__(self):
98 """Provide iteration over Belle2::PyDBArray. Needs to be done here since we
99 want to make all returned classes read only"""
100 for i in range(len(self)):
101 yield self[i]
102
103
104numpy_to_cpp = {"int16": "short *",
105 "uint16": "unsigned short *",
106 "int32": "int *",
107 "uint32": "unsigned int *",
108 "int64": "long *",
109 "uint64": "unsigned long *",
110 "float32": "float *",
111 "float64": "double *",
112 "float96": "long double *"}
113
114cpp_to_numpy = {"short": "short",
115 "unsigned short": "ushort",
116 "int": "intc",
117 "unsigned int": "uintc",
118 "long": "long",
119 "unsigned long": "ulong",
120 "float": "single",
121 "double": "double",
122 "long double": "longdouble",
123 "Belle2::VxdID": "ushort"}
124
125
127 '''
128 Class that throws an exception when a specific constructor is not found.
129 The error message will contain the signature of the wanted constructor,
130 as well as the signature of the available constructors.
131 '''
132 def __init__(self, members, list_constructors, obj_name):
133 '''
134 Parameters:
135 -----------
136 members: dictionary
137 Contains the name of the parameters of the wanted constructor and their types
138
139 list_constructors: list of dictionaries
140 Contains all the available constructors, with the names of their parameters
141 and their types
142
143 obj_name: string
144 Name of the class of which the constructor is wanted
145 '''
146
147 self.message = "No corresponding constructor found. Looking for signature: \n"
148
149 self.members = members
150
151 self.list_constructors = list_constructors
152
153 self.name = obj_name
154 self.message = self.message + self.name + "("
155 self.message = (self.message +
156 ", ".join([" ".join(i) for i in list(zip(self.members.values(), self.members.keys()))]) +
157 ")\n Available constructors:\n")
158 for lis in self.list_constructors:
159 self.message = (self.message +
160 self.name + "(" + ", ".join([" ".join(i) for i in list(zip(lis.values(), lis.keys()))]) + ")\n")
161 super().__init__(self.message)
162
163
164def _wrap_fill_array(func):
165
166 def fill_array(pyStoreArray, **kwargs):
167 import numpy as np
168
169 list_constructors = []
170
171 obj_class = pyStoreArray.getClass()
172 obj_classname = obj_class.GetName()
173
174 for meth in obj_class.GetListOfMethods():
175 if meth.GetName() == obj_classname.split(":")[-1]: # found one constructor
176 d = {}
177
178 for ar in meth.GetListOfMethodArgs():
179 d[ar.GetName()] = ar.GetTypeName()
180 list_constructors.append(d)
181
182 # Check if this is the right constructor
183 if d.keys() == kwargs.keys():
184 break
185
186 else:
187 m_d = {list(kwargs.keys())[i]: numpy_to_cpp[type(list(kwargs.values())[i]
188 [0]).__name__].split("*")[0] for i in range(len(kwargs.keys()))}
189 raise ConstructorNotFoundError(m_d, list_constructors, obj_classname)
190
191 arr_types = []
192 for k in kwargs.keys():
193 if kwargs[k][0].dtype != np.dtype(cpp_to_numpy[d[k]]):
194 try:
195 kwargs[k] = kwargs[k].astype(cpp_to_numpy[d[k]])
196 except ValueError:
197 raise ValueError((f"Impossible to convert type of input arrays ({type(kwargs[k][0]).__name__})" +
198 f" into the type of the corresponding class member '{k}' ({np.dtype(cpp_to_numpy[d[k]])})"))
199 arr_types.append(numpy_to_cpp[type(kwargs[k][0]).__name__])
200
201 l_arr = list(kwargs.values())
202 if not all(len(l_arr[0]) == len(arr) for arr in l_arr[1:]):
203 raise ValueError("The lengths of the passed arrays are not the same")
204
205 length = len(l_arr[0])
206
207 func[(obj_classname, *arr_types)](pyStoreArray, length, *[kwargs[k] for k in d.keys()])
208
209 return fill_array
210
211
212def _wrap_read_array(func):
213
214 def read_array(pyStoreArray):
215 import numpy as np
216 kwargs = {}
217
218 obj_class = pyStoreArray.getClass()
219 obj_classname = obj_class.GetName()
220
221 fillValuesMethod = obj_class.GetMethodAny("fillValues")
222 if not fillValuesMethod:
223 raise ReferenceError(f"The method fillValues is not implemented for the class {obj_classname}")
224
225 for ar in fillValuesMethod.GetListOfMethodArgs():
226 kwargs.setdefault(ar.GetName(), np.zeros(len(pyStoreArray), dtype=cpp_to_numpy[ar.GetFullTypeName().split("*")[0]]))
227
228 l_types = [numpy_to_cpp[kwargs[v].dtype.name] for v in kwargs.keys()]
229
230 func[(obj_classname, *l_types)](pyStoreArray, *kwargs.values())
231
232 return kwargs
233
234 return read_array
235
236
237@pythonization(namespace="Belle2")
238def _pythonize(klass, name):
239 """Adjust the python interface of some Py* classes"""
240 if not name.startswith("Py"):
241 return
242
243 if name == "PyDBObj":
244 # now replace the PyDBObj getter with one that returns non-modifiable objects.
245 # This is how root does it in ROOT.py so let's keep that
246 klass.obj = lambda self: _make_tobject_const(self._obj())
247 # and allow to use it most of the time without calling obj() like the ->
248 # indirection in C++
249 klass.__getattr__ = lambda self, name: getattr(self.obj(), name)
250 # and make sure that we can iterate over the items in the class
251 # pointed to if it allows iteration
252 klass.__iter__ = lambda self: iter(self.obj())
253 elif name == "PyDBArray":
254 # also make item access in DBArray readonly
255 klass.__getitem__ = lambda self, i: _make_tobject_const(self._get(i))
256 # and supply an iterator
257 klass.__iter__ = _PyDBArray__iter__
258 elif name == "PyStoreObj":
259 # make sure that we can iterate over the items in the class
260 # pointed to if it allows iteration
261 klass.__iter__ = lambda self: iter(self.obj())
262 elif name == "PyStoreArray":
263 klass.fillArray = _wrap_fill_array(klass.fillArray)
264 klass.readArray = _wrap_read_array(klass.readArray)
__init__(self, members, list_constructors, obj_name)
str message
Member contatining the final message of the exception.