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