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().__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
61 def getName( self ):
62 """Get the instance name of the algorithm
63
64 This is for compatibility with the getName() function of Athena
65 configurables.
66 """
67
68 return self.name()
69
70 def getType( self ):
71 """Get the type name of the algorithm
72
73 This is for compatibility with the getType() function of Athena
74 configurables.
75 """
76
77 return self.type()
78
79 def __getattr__( self, name ):
80 """Get a previously set property value from the configuration
81
82 This function allows us to retrieve the value of a property that was
83 already set for the algorithm, to possibly use it in some configuration
84 decisions in the Python code itself.
85
86 Keyword arguments:
87 name -- The name of the property
88 """
89
90 # Short-circuit internal/dunder attribute lookups, which are never
91 # properties (and would otherwise recurse if '_props' is missing):
92 if name.startswith( '_' ):
93 raise AttributeError( name )
94
95 # Fail if the property was not (yet) set:
96 if name not in self._props:
97 raise AttributeError( f"Property '{name}' was not set on "
98 f"'{self.type()}/{self.name()}'" )
99
100 # Return the property value:
101 return self._props[ name ]
102
103 def __setattr__( self, key, value ):
104 """Set an algorithm property on an existing configuration object
105
106 This function allows us to set/override properties on an algorithm
107 configuration object. Allowing for the following syntax:
108
109 alg = ...
110 alg.IntProperty = 66
111 alg.FloatProperty = 3.141592
112 alg.StringProperty = "Foo"
113
114 Keyword arguments:
115 key -- The key/name of the property
116 value -- The value to set for the property
117 """
118
119 # Private variables should be set directly:
120 if key[ 0 ] == '_':
121 return super().__setattr__( key, value )
122
123 # Set the property, and remember its value:
124 super().setPropertyFromString( key, stringPropValue( value ) )
125 self._props[ key ] = copy.deepcopy( value )
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 __str__( self ):
143 """Print the algorithm configuration in a user friendly way
144
145 This is just to help with debugging configurations, allowing
146 the user to get a nice printout of their job configuration.
147 """
148
149 if self.isPublicTool():
150 name = f'Public Tool {self.type()}/{self.name()}'
151 else:
152 name = f'Algorithm {self.type()}/{self.name()}'
153 result = AnaAlgorithmConfig._printHeader( name )
154 result += '\n'
155 for key, value in sorted( self._props.items() ):
156 if isinstance( value, str ):
157 printedValue = f"'{value}'"
158 else:
159 printedValue = value
160 result += f"|- {key}: {indentBy( printedValue, '| ' )}\n"
161 result += AnaAlgorithmConfig._printFooter( name )
162 return result
163
164 def addPrivateTool( self, name, type ):
165 """Create a private tool for the algorithm
166
167 This function is used in 'standalone' mode to declare a private tool
168 for the algorithm, or a private tool for an already declared private
169 tool.
170
171 Can be used like:
172 config.addPrivateTool( 'tool1', 'ToolType1' )
173 config.addPrivateTool( 'tool1.tool2', 'ToolType2' )
174
175 Keyword arguments:
176 name -- The full name of the private tool
177 type -- The C++ type of the private tool
178 """
179
180 # And now set up the Python object that will take care of setting
181 # properties on this tool.
182
183 # Tokenize the tool's name. In case it is a subtool of a tool, or
184 # something possibly even deeper.
185 toolNames = name.split( '.' )
186
187 # Look up the component that we need to set up the private tool on.
188 component = self
189 for tname in toolNames[ 0 : -1 ]:
190 component = getattr( component, tname )
191
192 # Check that the component doesn't have such a (tool) property yet.
193 if hasattr( component, toolNames[ -1 ] ):
194 raise RuntimeError( f"Tool with name '{name}' already exists" )
195
196 # Now set up a smart object as a property on that component.
197 component._props[ toolNames[ -1 ] ] = PrivateToolConfig( self, name,
198 type )
199
200 # Finally, tell the C++ code what to do.
201 self.createPrivateTool( name, type ).ignore()
202
203 def addPrivateToolInArray( self, name, type ):
204 """Create a private tool in an array for the algorithm
205
206 This function is used in 'standalone' mode to declare a
207 private tool in a tool array for the algorithm, or a private
208 tool in a tool array for an already declared private tool.
209
210 Can be used like:
211 tool = config.addPrivateToolInArray( 'tool1', 'ToolType1' )
212 tool = config.addPrivateToolInArray( 'tool1.tool2', 'ToolType2' )
213
214 Keyword arguments:
215 name -- The full name of the private tool
216 type -- The C++ type of the private tool
217 """
218
219 # And now set up the Python object that will take care of setting
220 # properties on this tool.
221
222 # Tokenize the tool's name. In case it is a subtool of a tool, or
223 # something possibly even deeper.
224 toolNames = name.split( '.' )
225
226 # Look up the component that we need to set up the private tool on.
227 component = self
228 for tname in toolNames[ 0 : -1 ]:
229 component = getattr( component, tname )
230
231 # Finally, tell the C++ code what to do.
232 actualName = self.createPrivateToolInArray( name, type )
233
234 # Tokenize the actual tool's name. In case it is a subtool of
235 # a tool, or something possibly even deeper.
236 actualToolNames = actualName.split( '.' )
237
238 # Now set up a smart object as a property on that component.
239 config = PrivateToolConfig( self, actualName, type )
240 component._props[ actualToolNames[ -1 ] ] = config
241 return config
242
243 @staticmethod
244 def _printHeader( title ):
245 """Produce a nice header when printing the configuration
246
247 This function is used for printing the header of both algorithms
248 and tools.
249
250 Keyword arguments:
251 indentString -- String used as indentation
252 title -- The title of the algorithm/tool
253 """
254
255 preLength = AnaAlgorithmConfig.printHeaderPre
256 postLength = AnaAlgorithmConfig.printHeaderWidth - 3 - preLength - \
257 len( title )
258 return f"/{preLength * '*'} {title} {postLength * '*'}"
259
260 @staticmethod
261 def _printFooter( title ):
262 """Produce a nice footer when printing the configuration
263
264 This function is used for printing the footer 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 - 12 - preLength - \
274 len( title )
275 return f"\\{preLength * '-'} (End of {title}) {postLength * '-'}"
276
277
279 """Standalone Private Tool Configuration
280
281 This class is used to mimic the behaviour of Athena tool configurable
282 classes. To be able to set the properties of private tools used by
283 dual-use algorithms in a way that's valid for both Athena and EventLoop.
284 """
285
286 def __init__( self, algorithm, prefix, type ):
287 """Constructor for an private tool configuration object
288 """
289
290 self._algorithm = algorithm
291 self._prefix = prefix
292 self._type = type
293 self._props = {}
294
295 def __getattr__( self, name ):
296 """Get a previously set property value from the configuration
297
298 This function allows us to retrieve the value of a tool property that
299 was already set for an algorithm's private tool, to possibly use it in
300 some configuration decisions in the Python code itself.
301
302 Keyword arguments:
303 name -- The name of the property
304 """
305
306 # Short-circuit internal/dunder attribute lookups, which are never
307 # properties (and would otherwise recurse if '_props' is missing):
308 if name.startswith( '_' ):
309 raise AttributeError( name )
310
311 # Fail if the property was not (yet) set:
312 if name not in self._props:
313 raise AttributeError(
314 f'Property "{name}" was not set on '
315 f'"{self._algorithm.type()}/{self._algorithm.name()}.{self._prefix}"' )
316
317 # Return the property value:
318 return self._props[ name ]
319
320 def __setattr__( self, key, value ):
321 """Set a tool property on an existing configuration object
322
323 This function allows us to set/override properties on a private tool
324 of an algorithm configuration object. Allowing for the following syntax:
325
326 alg = ...
327 alg.Tool.IntProperty = 66
328 alg.Tool.FloatProperty = 3.141592
329 alg.Tool.StringProperty = "Foo"
330
331 Keyword arguments:
332 key -- The key/name of the property
333 value -- The value to set for the property
334 """
335
336 # Private variables should be set directly:
337 if key[ 0 ] == '_':
338 return super().__setattr__( key, value )
339
340 # Construct the full name, used in the C++ code:
341 fullName = self._prefix + "." + key
342
343 # Set the property, and remember its value:
344 self._algorithm.setPropertyFromString( fullName,
345 stringPropValue( value ) )
346 self._props[ key ] = copy.deepcopy( value )
347
348 def __str__( self ):
349 """Print the private tool configuration in a user friendly way
350
351 This is just to help with debugging configurations, allowing
352 the user to get a nice printout of their job configuration.
353 """
354
355 name = f'Private Tool {self._type}/{self._prefix}'
356 result = ' \n'
357 result += AnaAlgorithmConfig._printHeader( name )
358 result += '\n'
359 for key, value in sorted( self._props.items() ):
360 if isinstance( value, str ):
361 printedValue = f"'{value}'"
362 else:
363 printedValue = value
364 result += f"|- {key}: {indentBy( printedValue, '| ' )}\n"
365 result += AnaAlgorithmConfig._printFooter( name )
366 return result
367
368
369def stringPropValue( value ):
370 """Helper function producing a string property value"""
371
372 stringValue = str( value )
373 if isinstance( value, bool ):
374 stringValue = str( int( value ) )
375 return stringValue
376
377
378def indentBy( propValue, indent ):
379 """Helper function used in the configuration printout"""
380
381 stringValue = str( propValue )
382 result = ""
383 for stringLine in stringValue.split( '\n' ):
384 if len( result ):
385 result += "\n" + indent
386 result += stringLine
387 return result
388
389
390#
391# Declare some unit tests for the code
392#
393
394
395class TestAlgTypeAndName( unittest.TestCase ):
396
397
400 config1 = AnaAlgorithmConfig( "TypeName" )
401 self.assertEqual( config1.type(), "TypeName" )
402 self.assertEqual( config1.name(), "TypeName" )
403 config2 = AnaAlgorithmConfig( "NS::SomeType" )
404 self.assertEqual( config2.type(), "NS::SomeType" )
405 self.assertEqual( config2.name(), "NS::SomeType" )
406
407
409 def test_typeandname( self ):
410 config1 = AnaAlgorithmConfig( "TypeName/InstanceName" )
411 self.assertEqual( config1.type(), "TypeName" )
412 self.assertEqual( config1.name(), "InstanceName" )
413 config2 = AnaAlgorithmConfig( "NS::SomeType/Instance" )
414 self.assertEqual( config2.type(), "NS::SomeType" )
415 self.assertEqual( config2.name(), "Instance" )
416
417
418class TestAlgProperties( unittest.TestCase ):
419
420
421 def setUp( self ):
422 self.config = AnaAlgorithmConfig( "Type/Name" )
423
424
425 def test_propaccess( self ):
426 self.config.Prop1 = "Value1"
427 self.config.Prop2 = [ "Value2" ]
428 self.assertEqual( self.config.Prop1, "Value1" )
429 self.assertEqual( self.config.Prop2, [ "Value2" ] )
430 self.assertNotEqual( self.config.Prop1, "Foo" )
431 self.assertNotEqual( self.config.Prop2, "Value2" )
432
433
435 with self.assertRaises( AttributeError ):
436 value = self.config.Prop3
437
438
439class TestAlgPrivateTool( unittest.TestCase ):
440
441
442 def setUp( self ):
443 self.config = AnaAlgorithmConfig( "AlgType/AlgName" )
444
445
446 def test_privatetool( self ):
447 self.config.addPrivateTool( "Tool1", "ToolType1" )
448 self.config.Tool1.Prop1 = "Value1"
449 self.config.Tool1.Prop2 = [ 1, 2, 3 ]
450 self.assertEqual( self.config.Tool1.Prop1, "Value1" )
451 self.assertEqual( self.config.Tool1.Prop2, [ 1, 2, 3 ] )
452
453
455 tool = self.config.addPrivateToolInArray( "Tool1", "ToolType1" )
456 tool.Prop1 = "Value1"
457 tool.Prop2 = [ 1, 2, 3 ]
458 self.assertEqual( tool.Prop1, "Value1" )
459 self.assertEqual( tool.Prop2, [ 1, 2, 3 ] )
460
461
463 self.config.addPrivateTool( "Tool1", "ToolType1" )
464 self.config.addPrivateTool( "Tool1.Tool2", "ToolType2" )
465 self.config.Tool1.Tool2.Prop3 = "Foo"
466 self.config.Tool1.Tool2.Prop4 = [ "Bar" ]
467 self.assertEqual( self.config.Tool1.Tool2.Prop3, "Foo" )
468 self.assertEqual( self.config.Tool1.Tool2.Prop4, [ "Bar" ] )
469
470
472 self.config.addPrivateTool( "Tool1", "ToolType1" )
473 with self.assertRaises( AttributeError ):
474 value = self.config.Tool1.BadProp
475 self.config.addPrivateTool( "Tool1.Tool2", "ToolType2" )
476 with self.assertRaises( AttributeError ):
477 value = self.config.Tool1.Tool2.BadProp
478
479
481 with self.assertRaises( AttributeError ):
482 self.config.addPrivateTool( "BadTool.Tool4", "BadToolType" )
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.