ATLAS Offline Software
Loading...
Searching...
No Matches
PythonConfig.py
Go to the documentation of this file.
1# Copyright (C) 2002-2021 CERN for the benefit of the ATLAS collaboration
2
3# Import(s):
4import ROOT
5import unittest
6import copy
7
8class PythonConfig( ROOT.EL.PythonConfigBase ):
9 """Standalone Analysis Component Configuration
10
11 This class is used to describe the configuration of an analysis component
12 (a C++ class inheriting from asg::AsgComponentConfig) in Python. It behaves
13 similar to an Athena configurable, but is implemented in a much simpler
14 way.
15
16 An example of using it in configuring an EventLoop job could look like:
17
18 job = ROOT.EL.Job()
19 ...
20 from AnaAlgorithm.PythonConfig import PythonConfig
21 alg = PythonConfig( "EL::UnitTestAlg2/TestAlg",
22 property = 1.23 )
23 alg.setComponentType( "AnaAlgorithm" )
24 alg.string_property = "Foo"
25 job.algsAdd( alg )
26
27 Note that the python code doesn't know what properties can actually be set
28 on any given C++ algorithm. Any mistake made in the Python configuration
29 (apart from syntax errors) is only discovered while initialising the
30 analysis job.
31 """
32
33 # Class/static variable(s):
34 printHeaderWidth = 80
35 printHeaderPre = 3
36
37 def __init__( self, typeAndName, **kwargs ):
38 """Constructor for an algorithm configuration object
39
40 Keyword arguments:
41 typeAndName -- The type/instance name of the algorithm
42
43 Note that you can pass (initial) properties to the constructor like:
44
45 alg = PythonConfig( "EL::UnitTestAlg2/TestAlg",
46 property = 1.23 )
47 """
48
49 # Call the base class's constructor. Use the default constructor instead
50 # of the one receiving the type and name, to avoid ROOT-10872.
51 super( PythonConfig, self ).__init__()
52 self.setTypeAndName( typeAndName )
53
54 # Initialise the properties of the algorihm:
55 self._props = {}
56
57 # Set the properties on the object:
58 for key, value in kwargs.items():
59 self.setPropertyFromString( key, stringPropValue( value ) )
60 self._props[ key ] = copy.deepcopy( value )
61 pass
62
63 pass
64
65 def getName( self ):
66 """Get the instance name of the algorithm
67
68 This is for compatibility with the getName() function of Athena
69 configurables.
70 """
71
72 return self.name()
73
74 def getType( self ):
75 """Get the type name of the algorithm
76
77 This is for compatibility with the getType() function of Athena
78 configurables.
79 """
80
81 return self.type()
82
83 def addSelfToJob( self, job ):
84 """add a copy of this config to the EventLoop job object
85
86 Keyword arguments:
87 job -- The job object to add ourself to
88 """
89 job.algsAdd( self )
90 pass
91
92 def __getattr__( self, name ):
93 """Get a previously set property value from the configuration
94
95 This function allows us to retrieve the value of a property that was
96 already set for the algorithm, to possibly use it in some configuration
97 decisions in the Python code itself.
98
99 Keyword arguments:
100 name -- The name of the property
101 """
102
103 # Fail if the property was not (yet) set:
104 if not name in self._props:
105 raise AttributeError( 'Property \'%s\' was not set on \'%s/%s\'' %
106 ( name, self.type(), self.name() ) )
107
108 # Return the property value:
109 return self._props[ name ]
110
111 def __setattr__( self, key, value ):
112 """Set an algorithm property on an existing configuration object
113
114 This function allows us to set/override properties on an algorithm
115 configuration object. Allowing for the following syntax:
116
117 alg = ...
118 alg.IntProperty = 66
119 alg.FloatProperty = 3.141592
120 alg.StringProperty = "Foo"
121
122 Keyword arguments:
123 key -- The key/name of the property
124 value -- The value to set for the property
125 """
126
127 # Private variables should be set directly:
128 if key[ 0 ] == '_':
129 return super( PythonConfig, self ).__setattr__( key, value )
130
131 # Set the property, and remember its value:
132 super( PythonConfig,
133 self ).setPropertyFromString( key, stringPropValue( value ) )
134 self._props[ key ] = copy.deepcopy( value )
135 pass
136
137 def __eq__( self, other ):
138 """Check for equality with another object
139
140 The implementation of this is very simple. We only check that the type
141 and the name of the algorithms would match.
142 """
143
144 # First check that the other object is also an PythonConfig one:
145 if not isinstance( other, PythonConfig ):
146 return False
147
148 # Now check whether the type and the name of the algorithms agree:
149 return ( ( self.type() == other.type() ) and
150 ( self.name() == other.name() ) )
151
152 def __ne__( self, other ):
153 """Check for an inequality with another object
154
155 This is just defined to make the '!=' operator of Python behave
156 consistently with the '==' operator for such objects.
157 """
158 return not self.__eq__( other )
159
160 def __str__( self ):
161 """Print the algorithm configuration in a user friendly way
162
163 This is just to help with debugging configurations, allowing
164 the user to get a nice printout of their job configuration.
165 """
166
167 name = 'PythonConfig %s/%s/%s' % ( self.componentType(),self.type(), self.name() )
168 result = PythonConfig._printHeader( name )
169 result += '\n'
170 for key, value in sorted( self._props.items() ):
171 if isinstance( value, str ):
172 printedValue = "'%s'" % value
173 else:
174 printedValue = value
175 pass
176 result += "|- %s: %s\n" % ( key, indentBy( printedValue, "| " ) )
177 pass
178 result += PythonConfig._printFooter( name )
179 return result
180
181 def addPrivateTool( self, name, type ):
182 """Create a private tool for the algorithm
183
184 This function is used in 'standalone' mode to declare a private tool
185 for the algorithm, or a private tool for an already declared private
186 tool.
187
188 Can be used like:
189 config.addPrivateTool( 'tool1', 'ToolType1' )
190 config.addPrivateTool( 'tool1.tool2', 'ToolType2' )
191
192 Keyword arguments:
193 name -- The full name of the private tool
194 type -- The C++ type of the private tool
195 """
196
197 # And now set up the Python object that will take care of setting
198 # properties on this tool.
199
200 # Tokenize the tool's name. In case it is a subtool of a tool, or
201 # something possibly even deeper.
202 toolNames = name.split( '.' )
203
204 # Look up the component that we need to set up the private tool on.
205 component = self
206 for tname in toolNames[ 0 : -1 ]:
207 component = getattr( component, tname )
208 pass
209
210 # Check that the component doesn't have such a (tool) property yet.
211 if hasattr( component, toolNames[ -1 ] ):
212 raise RuntimeError( "Tool with name '%s' already exists" % name )
213 pass
214
215 # Now set up a smart object as a property on that component.
216 component._props[ toolNames[ -1 ] ] = PrivateToolConfig( self, name,
217 type )
218
219 # Finally, tell the C++ code what to do.
220 self.createPrivateTool( name, type ).ignore()
221
222 pass
223
224 def addPrivateToolInArray( self, name, type ):
225 """Create a private tool in an array for the algorithm
226
227 This function is used in 'standalone' mode to declare a
228 private tool in a tool array for the algorithm, or a private
229 tool in a tool array for an already declared private tool.
230
231 Can be used like:
232 tool = config.addPrivateToolInArray( 'tool1', 'ToolType1' )
233 tool = config.addPrivateToolInArray( 'tool1.tool2', 'ToolType2' )
234
235 Keyword arguments:
236 name -- The full name of the private tool
237 type -- The C++ type of the private tool
238 """
239
240 # And now set up the Python object that will take care of setting
241 # properties on this tool.
242
243 # Tokenize the tool's name. In case it is a subtool of a tool, or
244 # something possibly even deeper.
245 toolNames = name.split( '.' )
246
247 # Look up the component that we need to set up the private tool on.
248 component = self
249 for tname in toolNames[ 0 : -1 ]:
250 component = getattr( component, tname )
251 pass
252
253 # Finally, tell the C++ code what to do.
254 actualName = self.createPrivateToolInArray( name, type )
255
256 # Tokenize the actual tool's name. In case it is a subtool of
257 # a tool, or something possibly even deeper.
258 actualToolNames = actualName.split( '.' )
259
260 # Now set up a smart object as a property on that component.
261 config = PrivateToolConfig( self, actualName, type )
262 component._props[ actualToolNames[ -1 ] ] = config
263 return config
264
265 @staticmethod
266 def _printHeader( title ):
267 """Produce a nice header when printing the configuration
268
269 This function is used for printing the header of both algorithms
270 and tools.
271
272 Keyword arguments:
273 indentString -- String used as indentation
274 title -- The title of the algorithm/tool
275 """
276
277 preLength = PythonConfig.printHeaderPre
278 postLength = PythonConfig.printHeaderWidth - 3 - preLength - \
279 len( title )
280 return '/%s %s %s' % ( preLength * '*', title, postLength * '*' )
281
282 @staticmethod
283 def _printFooter( title ):
284 """Produce a nice footer when printing the configuration
285
286 This function is used for printing the footer of both algorithms
287 and tools.
288
289 Keyword arguments:
290 indentString -- String used as indentation
291 title -- The title of the algorithm/tool
292 """
293
294 preLength = PythonConfig.printHeaderPre
295 postLength = PythonConfig.printHeaderWidth - 12 - preLength - \
296 len( title )
297 return '\\%s (End of %s) %s' % ( preLength * '-', title,
298 postLength * '-' )
299
300 pass
301
302
304 """Standalone Private Tool Configuration
305
306 This class is used to mimic the behaviour of Athena tool configurable
307 classes. To be able to set the properties of private tools used by
308 dual-use algorithms in a way that's valid for both Athena and EventLoop.
309 """
310
311 def __init__( self, algorithm, prefix, type ):
312 """Constructor for an private tool configuration object
313 """
314
315 self._algorithm = algorithm
316 self._prefix = prefix
317 self._type = type
318 self._props = {}
319
320 pass
321
322 def __getattr__( self, name ):
323 """Get a previously set property value from the configuration
324
325 This function allows us to retrieve the value of a tool property that
326 was already set for an algorithm's private tool, to possibly use it in
327 some configuration decisions in the Python code itself.
328
329 Keyword arguments:
330 name -- The name of the property
331 """
332
333 # Fail if the property was not (yet) set:
334 if not name in self._props:
335 raise AttributeError( 'Property "%s" was not set on "%s/%s.%s"' %
336 ( name, self._algorithm.type(),
337 self._algorithm.name(), self._prefix ) )
338
339 # Return the property value:
340 return self._props[ name ]
341
342 def __setattr__( self, key, value ):
343 """Set a tool property on an existing configuration object
344
345 This function allows us to set/override properties on a private tool
346 of an algorithm configuration object. Allowing for the following syntax:
347
348 alg = ...
349 alg.Tool.IntProperty = 66
350 alg.Tool.FloatProperty = 3.141592
351 alg.Tool.StringProperty = "Foo"
352
353 Keyword arguments:
354 key -- The key/name of the property
355 value -- The value to set for the property
356 """
357
358 # Private variables should be set directly:
359 if key[ 0 ] == '_':
360 return super( PrivateToolConfig, self ).__setattr__( key, value )
361
362 # Construct the full name, used in the C++ code:
363 fullName = self._prefix + "." + key
364
365 # Set the property, and remember its value:
366 self._algorithm.setPropertyFromString( fullName,
367 stringPropValue( value ) )
368 self._props[ key ] = copy.deepcopy( value )
369 pass
370
371 def __str__( self ):
372 """Print the private tool configuration in a user friendly way
373
374 This is just to help with debugging configurations, allowing
375 the user to get a nice printout of their job configuration.
376 """
377
378 name = 'Private Tool %s/%s' % ( self._type, self._prefix )
379 result = ' \n'
380 result += PythonConfig._printHeader( name )
381 result += '\n'
382 for key, value in sorted( self._props.items() ):
383 if isinstance( value, str ):
384 printedValue = "'%s'" % value
385 else:
386 printedValue = value
387 pass
388 result += "|- %s: %s\n" % ( key, indentBy( printedValue, "| " ) )
389 pass
390 result += PythonConfig._printFooter( name )
391 return result
392
393 pass
394
395
396def stringPropValue( value ):
397 """Helper function producing a string property value"""
398
399 stringValue = str( value )
400 if isinstance( value, bool ):
401 stringValue = str( int( value ) )
402 pass
403 return stringValue
404
405
406def indentBy( propValue, indent ):
407 """Helper function used in the configuration printout"""
408
409 stringValue = str( propValue )
410 result = ""
411 for stringLine in stringValue.split( '\n' ):
412 if len( result ):
413 result += "\n" + indent
414 pass
415 result += stringLine
416 pass
417 return result
418
419
420#
421# Declare some unit tests for the code
422#
423
424
425class TestAlgTypeAndName( unittest.TestCase ):
426
427
430 config1 = PythonConfig( "TypeName" )
431 self.assertEqual( config1.type(), "TypeName" )
432 self.assertEqual( config1.name(), "TypeName" )
433 config2 = PythonConfig( "NS::SomeType" )
434 self.assertEqual( config2.type(), "NS::SomeType" )
435 self.assertEqual( config2.name(), "NS::SomeType" )
436 pass
437
438
440 def test_typeandname( self ):
441 config1 = PythonConfig( "TypeName/InstanceName" )
442 self.assertEqual( config1.type(), "TypeName" )
443 self.assertEqual( config1.name(), "InstanceName" )
444 config2 = PythonConfig( "NS::SomeType/Instance" )
445 self.assertEqual( config2.type(), "NS::SomeType" )
446 self.assertEqual( config2.name(), "Instance" )
447 pass
448
449
450class TestAlgProperties( unittest.TestCase ):
451
452
453 def setUp( self ):
454 self.config = PythonConfig( "Type/Name" )
455 pass
456
457
458 def test_propaccess( self ):
459 self.config.Prop1 = "Value1"
460 self.config.Prop2 = [ "Value2" ]
461 self.assertEqual( self.config.Prop1, "Value1" )
462 self.assertEqual( self.config.Prop2, [ "Value2" ] )
463 self.assertNotEqual( self.config.Prop1, "Foo" )
464 self.assertNotEqual( self.config.Prop2, "Value2" )
465 pass
466
467
469 with self.assertRaises( AttributeError ):
470 value = self.config.Prop3
471 pass
472 pass
473
474
475class TestAlgPrivateTool( unittest.TestCase ):
476
477
478 def setUp( self ):
479 self.config = PythonConfig( "AlgType/AlgName" )
480 pass
481
482
483 def test_privatetool( self ):
484 self.config.addPrivateTool( "Tool1", "ToolType1" )
485 self.config.Tool1.Prop1 = "Value1"
486 self.config.Tool1.Prop2 = [ 1, 2, 3 ]
487 self.assertEqual( self.config.Tool1.Prop1, "Value1" )
488 self.assertEqual( self.config.Tool1.Prop2, [ 1, 2, 3 ] )
489 pass
490
491
493 tool = self.config.addPrivateToolInArray( "Tool1", "ToolType1" )
494 tool.Prop1 = "Value1"
495 tool.Prop2 = [ 1, 2, 3 ]
496 self.assertEqual( tool.Prop1, "Value1" )
497 self.assertEqual( tool.Prop2, [ 1, 2, 3 ] )
498 pass
499
500
502 self.config.addPrivateTool( "Tool1", "ToolType1" )
503 self.config.addPrivateTool( "Tool1.Tool2", "ToolType2" )
504 self.config.Tool1.Tool2.Prop3 = "Foo"
505 self.config.Tool1.Tool2.Prop4 = [ "Bar" ]
506 self.assertEqual( self.config.Tool1.Tool2.Prop3, "Foo" )
507 self.assertEqual( self.config.Tool1.Tool2.Prop4, [ "Bar" ] )
508 pass
509
510
512 self.config.addPrivateTool( "Tool1", "ToolType1" )
513 with self.assertRaises( AttributeError ):
514 value = self.config.Tool1.BadProp
515 pass
516 self.config.addPrivateTool( "Tool1.Tool2", "ToolType2" )
517 with self.assertRaises( AttributeError ):
518 value = self.config.Tool1.Tool2.BadProp
519 pass
520 pass
521
522
524 with self.assertRaises( AttributeError ):
525 self.config.addPrivateTool( "BadTool.Tool4", "BadToolType" )
526 pass
527 pass
__init__(self, algorithm, prefix, type)
addPrivateToolInArray(self, name, type)
__init__(self, typeAndName, **kwargs)
Test case for using private tools.
test_nonexistentprop(self)
Test that unset properties on the tools can't be used.
test_privatetool(self)
Test setting up and using one private tool.
setUp(self)
Set up the main algorithm object to test.
test_nonexistenttool(self)
Test that private tools can't be set up on not-yet-declared tools.
test_privatetoolarray(self)
Test setting up and using one private tool.
test_privatetoolofprivatetool(self)
Test setting up and using a private tool of a private tool.
Test case for the algorithm property handling.
test_propaccess(self)
Test that properties that got set, can be read back.
setUp(self)
Common setup for the tests.
test_nonexistentprop(self)
Test that an unset property can't be accessed.
Test case for the algorithm type/name handling.
test_typeandname(self)
Test that specifying the type and name separately in the same string works as expected.
test_singletypename(self)
Test that the type and name are set correctly when using a single argument.
indentBy(propValue, indent)