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