ATLAS Offline Software
Loading...
Searching...
No Matches
PropertyProxy.py
Go to the documentation of this file.
1# Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
2
3# File: AthenaCommon/python/PropertyProxy.py
4# Author: Wim Lavrijsen (WLavrijsen@lbl.gov)
5# Author: Martin Woudstra (Martin.Woudstra@cern.ch)
6
7import os, weakref, copy
8from GaudiKernel.GaudiHandles import GaudiHandle, GaudiHandleArray
9from GaudiKernel.DataHandle import DataHandle
10
11# dictionary with configurable class : python module entries
12from AthenaCommon import ConfigurableDb
13
14
15__version__ = '2.0.0'
16__author__ = 'Wim Lavrijsen (WLavrijsen@lbl.gov), Martin Woudstra (Martin.Woudstra@cern.ch)'
17
18__all__ = [ 'PropertyProxy',
19 'GaudiHandlePropertyProxy',
20 'GaudiHandleArrayPropertyProxy' ]
21
22
23from AthenaCommon.Logging import logging
24log = logging.getLogger( 'PropertyProxy' )
25
26
27def derives_from( derived, base ):
28 """A string version of isinstance().
29 <derived> is either an object instance, or a type
30 <base> is a string containing the name of the base class (or <derived> class)"""
31 if not isinstance( derived, type ):
32 derived = type(derived)
33
34 if derived.__name__ == base:
35 return True
36
37 for b in derived.__bases__:
38 if derives_from( b,base ):
39 return True
40
41 return False
42
43
44def _isCompatible( tp, value, context = "" ):
45 # Note: this function is only called for builtin types. Lists of
46 # configurables (most likely GaudiHandleArrays) don't get here, since
47 # their PropertyProxy types are special cases (see below). Thus, a
48 # compatibility check that relies on conversion (which will always fail
49 # for configurables) is acceptable.
50
51 if ( tp == str or type(value) is str ) and not isinstance( value, tp ):
52 # special case, insist on exact match for str (no conversions allowed)
53 raise ValueError( "received an instance of %s, but %s expected, context: %s" % (type(value),tp, context) )
54 elif ( tp == int ) and type(value) is float:
55 # special case, insist on strict match for integer types
56 raise ValueError( "received an instance of %s, but %s expected, context: %s" % (type(value),tp, context) )
57 else:
58 # all other types: accept if conversion allowed
59 try:
60 dummy = tp( value )
61 except ValueError as verr:
62 if isinstance( value, tp ):
63 # this is a conversion mismatch, but an "ok" type (i.e. the type can
64 # not be copied/constructed from itself), which is a typical case
65 # for configurables being added to an ordinary list (as opposed to
66 # a GaudiHandleArray), where the actual type of the list is unknown
67 raise ValueError( "received non-convertible type %s, but builtin type expected, reason: %s context: %s" % (type(value), str(verr), context) )
68 else:
69 # this is a complete miss and the error message will be clear
70 raise ValueError( "received an instance of %s, but %s expected, reason: %s, context: %s" % (type(value),tp, str(verr), context) )
71 except TypeError as terr:
72 raise TypeError( "received an instance of %s, but %s expected, reason: %s, context: %s" % (type(value),tp, str(terr), context) )
73 return dummy # in case of e.g. classes with __int__, __iter__, etc. implemented
74
75
77 def __init__( self, descr, docString=None, default=None ):
78 self.history = weakref.WeakKeyDictionary()
79 self.descr = descr
80 if docString:
81 self.__doc__ = docString
82 if default is not None:
83 self.default = default
84
85 def setDefault( self, value ):
86 self.__default = value
87
88 def getDefault( self ):
89 return self.__default
90
91 default = property( getDefault, setDefault )
92
93 def fullPropertyName( self, obj ):
94 return (obj.getJobOptName() or obj.getName()) + '.' + self.descr.__name__
95
96 def __get__( self, obj, type = None ):
97 value = None
98 try:
99 value = self.descr.__get__( obj, type )
100 except AttributeError:
101 # special case for list and set: allow default to work with on +=;
102 # unfortunately, that means that direct access also succeeds, even
103 # as the property wasn't set yet ... (TODO: ideas??)
104 if self.__default.__class__ == list or self.__default.__class__ == set:
105 self.descr.__set__( obj, self.__default.__class__(self.__default) )
106 value = self.descr.__get__( obj, type ) # no history
107 else:
108 raise
109
110 if value is None:
111 value = self.descr.__get__( obj, type )
112
113 # locked configurables return copies to prevent modifications
114 if obj.isLocked():
115 import builtins
116 if builtins.type( value ) is dict:
117 from ctypes import pythonapi, py_object
118 from _ctypes import PyObj_FromPtr
119 PyDictProxy_New = pythonapi.PyDictProxy_New
120 PyDictProxy_New.argtypes = (py_object,)
121 PyDictProxy_New.rettype = py_object
122 value = PyObj_FromPtr( PyDictProxy_New( value ) )
123 elif builtins.type( value ) is list:
124 value = tuple( value ) # point being that tuple is read-only
125 else:
126 import copy
127 value = copy.copy( value )
128
129 return value
130
131 def __set__( self, obj, value ):
132 # locked configurables can no longer be changed
133 if obj.isLocked():
134 raise RuntimeError(
135 'can not change property "%s" of locked configurable "%s"' %
136 (self.descr.__name__, obj.getJobOptName()) )
137
138 # check value/property compatibility if possible
139 proptype = None
140 if hasattr( self, 'default' ):
141 proptype = type(self.default)
142 elif obj in self.history:
143 proptype = type( self.history[ obj ][ 0 ] )
144
145 # check if type known; allow special initializer for typed instances
146 if proptype and not isinstance(proptype, type(None)):
147 # check value itself
148 value = _isCompatible( proptype, value, self.fullPropertyName(obj) )
149
150 # check elements in case of list
151 if proptype == tuple or proptype == list:
152 try:
153 oldvec = self.descr.__get__( obj, type )
154 if oldvec:
155 tpo = type(oldvec[0])
156 for v in value:
157 _isCompatible( tpo, v, self.fullPropertyName(obj) )
158 except AttributeError:
159 # value not yet set
160 pass
161
162 # allow a property to be set if we're in non-default mode, or if it
163 # simply hasn't been set before
164 if not obj._isInSetDefaults() or obj not in self.history:
165 # by convention, 'None' for default is used to designate objects setting
166 if hasattr( self, 'default' ) and self.default is None:
167 obj.__iadd__( value, self.descr ) # to establish hierarchy
168 else:
169 self.descr.__set__( obj, value )
170 self.history.setdefault( obj, [] ).append( value )
171
172 def __delete__( self, obj ):
173 if obj in self.history:
174 del self.history[ obj ]
175 self.descr.__delete__( obj )
176
177
178
180 """A class with some utilities for GaudiHandles and GaudiHandleArrays"""
181
182 def __init__(self, descr, docString, default, handleType, allowedType ):
183 """<descr>: the real property in the object instance (from __slots__)
184 <docString>: the documentation string of this property
185 <default>: default value from C++ (via python generated by genconf)
186 <handleType>: real python handle type (e.g. PublicToolHandle, PrivateToolHandle, ...)
187 <allowedType>: allowed instance type for default
188 """
189 # check that default is of allowed type for this proxy
190 if not isinstance(default,allowedType):
191 raise TypeError( "%s: %s default: %r is not a %s" % \
192 ( descr.__name__, self.__class__.__name__, default, allowedType.__name__ ) )
193 PropertyProxy.__init__( self, descr, docString, default )
194 self._handleType = handleType
195 self._confTypeName = 'Configurable' + handleType.componentType
196# print "%s: %r (%s)" % (self.__class__.__name__,self._handleType,self._confTypeName)
197
198
199 def __get__( self, obj, type = None ):
200 try:
201 return self.descr.__get__( obj, type )
202 except AttributeError:
203 # Get default
204 try:
205 default = obj.__class__.getDefaultProperty( self.descr.__name__ )
206 # if printing, just show the toolhandle. No need to auto-retrieve default configurable instance
207 if obj.isPrinting(): return default
208 default = self.convertDefaultToBeSet( obj, default )
209 # special case if default is auto-retrieved configurable instance
210 if self.isConfig(default):
211 if obj.isLocked():
212 # return a locked copy to produce error in case of setting any property
213 # rather then changing the value of the default
214 default = copy.deepcopy(default)
215 default.lock()
216 return default
217 else:
218 # Set to default configurable instance to allow MyTool.MySubTool.MyProperty = MyValue
219 self.__set__( obj, default )
220 elif isinstance(default,GaudiHandleArray):
221 if obj.isLocked():
222 # return a locked copy to produce error in case of setting any property
223 # rather then changing the value of the default
224 default = copy.deepcopy(default)
225 for c in default:
226 if self.isConfig(c):
227 c.lock()
228 return default
229 else:
230 # Set array to allow to add to default with syntax: MyTool.MyArray.append()
231 self.__set__( obj, default )
232 else:
233 return default
234 except AttributeError as e:
235 import traceback
236 traceback.print_exc()
237 # change type of exception to avoid false error message
238 raise RuntimeError("AttributeError(%s)" % e.args)
239
240 return self.descr.__get__( obj, type )
241
242
243 def __set__( self, obj, value ):
244 # locked configurables can no longer be changed
245 if obj.isLocked():
246 raise RuntimeError(
247 'can not change property "%s" of locked configurable "%s"' %
248 (self.descr.__name__, obj.getJobOptName()) )
249
250 # allow a property to be set if we're in non-default mode, or if it
251 # simply hasn't been set before
252 if not obj._isInSetDefaults() or obj not in self.history:
253 value = self.convertValueToBeSet( obj, value )
254 # assign the value
255 self.descr.__set__( obj, value )
256 log.verbose( "Setting %s = %r", self.fullPropertyName( obj ), value )
257 self.history.setdefault( obj, [] ).append( value )
258
259
260 def isHandle(self,value):
261 """Check if <value> is a handle of the correct type"""
262 return isinstance(value,self._handleType)
263
264
265 def isConfig(self,value):
266 """Check if <value> is a configurable of the correct type"""
267 return derives_from(value,self._confTypeName)
268
269
270 def getDefaultConfigurable(self,typeAndName,requester):
271 """Return the configurable instance corresponding to the toolhandle if possible.
272 Otherwise return None"""
273 # find the module
274 typeAndNameTuple = typeAndName.split('/')
275 confType = typeAndNameTuple[0]
276 confClass = ConfigurableDb.getConfigurable(confType)
277 # check the type of the configurable
278 if not derives_from(confClass,self._confTypeName):
279 log.error( "%s: Configurable %s is not a %s",
280 requester, confType, self._confTypeName )
281 return None
282 try:
283 confName = typeAndNameTuple[1]
284 except IndexError:
285 return confClass() # use default name
286 else:
287 return confClass(confName)
288
289
290 def convertDefaultToBeSet( self, obj, default ):
291 # turn string into handle
292 isString = type(default) is str
293 if not isString and self.isConfig(default):
294# print self.fullPropertyName(obj) + ": Setting default configurable: %r" % default
295 return default
296 elif isString or self.isHandle(default):
297 if isString:
298 # convert string into handle
299 typeAndName = default
300 default = self._handleType( typeAndName )
301 else:
302 typeAndName = default.typeAndName
303 if not self._handleType.isPublic and typeAndName:
304 # Find corresponding default configurable of private handles
305 try:
306 conf = self.getDefaultConfigurable(typeAndName, self.fullPropertyName(obj))
307# print self.fullPropertyName(obj) + ": Setting default private configurable (from default handle): %r" % conf
308 except AttributeError as e:
309 # change type of exception to avoid false error message
310 raise RuntimeError(*e.args)
311 if conf is None:
312 raise RuntimeError( "%s: Default configurable for class %s not found in ConfigurableDb.CfgDb" % \
313 (self.fullPropertyName(obj),default.getType() ) )
314 return conf
315 else: # not a config, not a handle, not a string
316 raise TypeError( "%s: default value %r is not of type %s or %s" % \
317 (self.fullPropertyName(obj),default,self._confTypeName,self._handleType.__name__) )
318
319 return default
320
321 def convertValueToBeSet( self, obj, value ):
322 if value is None: value = ''
323 isString = type(value) is str
324 if isString:
325 # create an new handle
326 return self._handleType(value)
327 elif self.isHandle(value):
328 # make a copy of the handle
329 return self._handleType(value.toStringProperty())
330 elif self.isConfig(value):
331 if self._handleType.isPublic:
332 # A public tool must be registered to ToolSvc before assigning it
333 if derives_from(value,'ConfigurableAlgTool'):
334 if not value.isInToolSvc():
335 suggestion = 'You may need to add jobOptions lines something like:' + os.linesep + \
336 'from AthenaCommon.AppMgr import ToolSvc' + os.linesep + \
337 'ToolSvc += '
338 if value.getName() == value.getType(): # using default name
339 suggestion += '%s()' % value.__class__.__name__
340 else: # using user-defined name
341 suggestion += '%s(%r)' % (value.__class__.__name__,value.getName())
342 raise RuntimeError( self.fullPropertyName(obj) +
343 ': Public tool %s is not yet in ToolSvc. %s' %
344 (value.getJobOptName(),suggestion) )
345 # make it read-only
346 return self._handleType(value.toStringProperty())
347 elif value.hasParent( obj.getJobOptName() ):
348 # is already a child, keep as-is
349 return value
350 else:
351 # make a copy of the configurable
352 return obj.copyChildAndSetParent( value, obj.getJobOptName() )
353 else:
354 raise TypeError( "Property %s value %r is not a %s nor a %s nor a string" % \
355 (self.fullPropertyName(obj),value,self._confTypeName,self._handleType.__name__) )
356
357 return value
358
359
361 def __init__( self, descr, docString, default ):
362 GaudiHandlePropertyProxyBase.__init__( self, descr, docString, default, type(default), GaudiHandle )
363
364
366 def __init__( self, descr, docString, default ):
367 """<descr>: the real property in the object instance (from __slots__)
368 <confTypeName>: string indicating the (base) class of allowed Configurables to be assigned.
369 <handleType>: real python handle type (e.g. PublicToolHandle, PrivateToolHandle, ...)
370 """
371 GaudiHandlePropertyProxyBase.__init__( self, descr, docString, default, type(default).handleType, GaudiHandleArray )
372 self.arrayType = type(default)
373
374
375 def checkType( self, obj, value ):
376 if not isinstance( value, list ) and not isinstance( value, self.arrayType ):
377 raise TypeError( "%s: Value %r is not a list nor a %s" % \
378 ( self.fullPropertyName(obj), value, self.arrayType.__name__ ) )
379
380
381 def convertDefaultToBeSet( self, obj, default ):
382 self.checkType( obj, default )
383 newDefault = self.arrayType()
384 for d in default:
385 cd = GaudiHandlePropertyProxyBase.convertDefaultToBeSet( self, obj, d )
386 if cd: newDefault.append( cd )
387
388 return newDefault
389
390
391 def convertValueToBeSet( self, obj, value ):
392 self.checkType( obj, value )
393 newValue = self.arrayType()
394 for v in value:
395 cv = GaudiHandlePropertyProxyBase.convertValueToBeSet( self, obj, v )
396 if cv: newValue.append( cv )
397
398 return newValue
399
400
402 def __init__(self, descr, docString, default):
403 PropertyProxy.__init__(self, descr, docString, default)
404
405 def __get__(self, obj, type=None):
406 try:
407 return self.descr.__get__(obj, type)
408 except AttributeError:
409 # Get default
410 try:
411 default = obj.__class__.getDefaultProperty(self.descr.__name__)
412 default = self.convertValueToBeSet(obj, default)
413 if default:
414 self.__set__(obj, default)
415 except AttributeError as e:
416 # change type of exception to avoid false error message
417 raise RuntimeError(*e.args)
418
419 return self.descr.__get__(obj, type)
420
421 def __set__(self, obj, value):
422 if not obj._isInSetDefaults() or obj not in self.history:
423 value = self.convertValueToBeSet(obj, value)
424 # assign the value
425 self.descr.__set__(obj, value)
426 log.debug("Setting %s = %r", self.fullPropertyName(obj), value)
427 self.history.setdefault(obj, []).append(value)
428
429 def convertValueToBeSet(self, obj, value):
430 if value is None:
431 value = ''
432
433 mode = obj.__class__.getDefaultProperty(self.descr.__name__).mode()
434 _type = obj.__class__.getDefaultProperty(self.descr.__name__).type()
435 if type(value) is str:
436 return DataHandle(value, mode, _type)
437 elif isinstance(value, DataHandle):
438 return DataHandle(value.__str__(), mode, _type)
439 else:
440 raise ValueError("received an instance of %s, but %s expected" %
441 (type(value), 'str or DataHandle'))
442
443
444def PropertyProxyFactory( descr, doc, default ):
445# print "PropertyProxyFactory( %s, %r )" % (descr.__name__,default)
446 if isinstance(default,GaudiHandleArray):
447 return GaudiHandleArrayPropertyProxy( descr, doc, default )
448
449 if isinstance(default,GaudiHandle):
450 return GaudiHandlePropertyProxy( descr, doc, default )
451
452 if isinstance(default,DataHandle):
453 return DataHandlePropertyProxy( descr, doc, default )
454
455 return PropertyProxy( descr, doc, default )
an iterator over instances of a given type in StoreGateSvc.
Definition DataHandle.h:43
__init__(self, descr, docString, default)
getDefaultConfigurable(self, typeAndName, requester)
__init__(self, descr, docString, default, handleType, allowedType)
__init__(self, descr, docString, default)
__init__(self, descr, docString=None, default=None)
PropertyProxyFactory(descr, doc, default)
derives_from(derived, base)
_isCompatible(tp, value, context="")