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.
14
15This module does not contain any public functions or variables, it just
16modifies the ROOT interfaces for PyDBObj and PyDBArray to only return read only
17objects.
18"""
19
20# we need to patch PyDBObj so lets import the ROOT cppyy backend.
21import 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
37def 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 upon import of the ROOT module.
45 """
46 def pythonization_impl(fn):
47 """
48 The real decorator. This structure is adopted to deal with parameters
49 fn : function
50 Function that implements some pythonization.
51 The function must accept two parameters: the class
52 to be pythonized and the name of that class.
53 """
54 if lazy:
55 cppyy.py.add_pythonization(fn, namespace)
56 else:
57 fn()
58 return pythonization_impl
59
60
61def _make_tobject_const(obj):
62 """
63 Make a TObject const: Disallow setting any attributes and calling any
64 methods which are not declared as const. This affects any reference to this
65 object in python and stays permanent. Once called this particular instance
66 will be readonly everywhere and will remain like this.
67
68 This is done by replacing all setter functions with function objects that
69 just raise an attribute error when called. The class will be still the same
70 """
71 # nothing to do if None
72 if obj is None:
73 return obj
74
75 try:
76
77 non_const = [m.GetName() for m in obj.Class().GetListOfMethods() if (m.Property() & _ROOT_kIsPublic)
78 and not (m.Property() & (_ROOT_kIsConstMethod | _ROOT_kIsStatic))]
79 except AttributeError:
80 raise ValueError(f"Object does not have a valid dictionary: {obj!r}")
81
82 # Override all setters to just raise an exception
83 for name in non_const:
84 def __proxy(*args, **argk):
85 """raise attribute error when called"""
86 raise AttributeError(f"{obj} is readonly and method '{name}' is not const")
87
88 setattr(obj, name, __proxy)
89
90 # and return the modified object
91 return obj
92
93
94def _PyDBArray__iter__(self):
95 """Provide iteration over Belle2::PyDBArray. Needs to be done here since we
96 want to make all returned classes read only"""
97 for i in range(len(self)):
98 yield self[i]
99
100
101@pythonization(namespace="Belle2")
102def _pythonize(klass, name):
103 """Adjust the python interface of some Py* classes"""
104 if not name.startswith("Py"):
105 return
106
107 if name == "PyDBObj":
108 # now replace the PyDBObj getter with one that returns non-modifiable objects.
109 # This is how root does it in ROOT.py so let's keep that
110 klass.obj = lambda self: _make_tobject_const(self._obj())
111 # and allow to use it most of the time without calling obj() like the ->
112 # indirection in C++
113 klass.__getattr__ = lambda self, name: getattr(self.obj(), name)
114 # and make sure that we can iterate over the items in the class
115 # pointed to if it allows iteration
116 klass.__iter__ = lambda self: iter(self.obj())
117 elif name == "PyDBArray":
118 # also make item access in DBArray readonly
119 klass.__getitem__ = lambda self, i: _make_tobject_const(self._get(i))
120 # and supply an iterator
121 klass.__iter__ = _PyDBArray__iter__
122 elif name == "PyStoreObj":
123 # make sure that we can iterate over the items in the class
124 # pointed to if it allows iteration
125 klass.__iter__ = lambda self: iter(self.obj())
126