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