5from functools
import wraps
8from AnaAlgorithm.Logging
import logging
9logCPAlgCfgBlock = logging.getLogger(
'CPAlgCfgBlock')
11from AnalysisAlgorithmsConfig.ConfigAccumulator
import DataType, ExpertModeWarning
15 """check whether the sample being run passes a"""
16 """possible DSID filter on the block"""
17 if len(filterList) == 0:
19 for dsid_filter
in filterList:
21 if any(char
in str(dsid_filter)
for char
in "^$*+?.()|[]{}\\"):
22 pattern = re.compile(dsid_filter)
23 if pattern.match(str(config.dsid())):
27 if str(dsid_filter) == str(config.dsid()):
32 """this wrapper ensures that the 'instanceName' of the various """
33 """config blocks is cleaned up of any non-alphanumeric characters """
34 """that may arise from using 'selectionName' in the naming."""
36 def wrapper(*args, **kwargs):
38 orig_name = func(*args, **kwargs)
50 """this meta class enforces the application of 'alphanumeric_block_names()' """
51 """to 'instanceName()' and will be used in the main ConfigBlock class in order """
52 """to propagate this rule also to all derived classes (the individual config blocks."""
55 if 'instanceName' in dct
and callable(dct[
'instanceName']):
57 return super().
__new__(cls, name, bases, dct)
60 """the information for a single option on a configuration block"""
62 def __init__ (self, type=None, info='', noneAction='ignore', required=False,
63 default=None, meta=None) :
74 """Class encoding a blocks dependence on other blocks."""
90 return f
'ConfigBlockDependency(blockName="{self.blockName}", required={self.required})'
94 """the base class for classes implementing individual blocks of
97 A configuration block is a sequence of one or more algorithms that
98 should always be scheduled together, e.g. the muon four momentum
99 corrections could be a single block, muon selection could then be
100 another block. The blocks themselves generally have their own
101 configuration options/properties specific to the block, and will
102 perform a dynamic configuration based on those options as well as
105 The actual configuration of the algorithms in the block will
106 depend on what other blocks are scheduled before and afterwards,
107 most importantly some algorithms will introduce shallow copies
108 that subsequent algorithms will need to run on, and some
109 algorithms will add selection decorations that subquent algorithms
110 should use as preselections.
112 The algorithms get created in a multi-step process (that may be
113 extended in the future): As a first step each block retrieves
114 references to the containers it uses (essentially marking its spot
115 in the processing chain) and also registering any shallow copies
116 that will be made. In the second/last step each block then
117 creates the fully configured algorithms.
119 One goal is that when the algorithms get created they will have
120 their final configuration and there needs to be no
121 meta-configuration data attached to the algorithms, essentially an
122 inversion of the approach in AnaAlgSequence in which the
123 algorithms got created first with associated meta-configuration
124 and then get modified in susequent configuration steps.
126 For now this is mostly an empty base class, but another goal of
127 this approach is to make it easier to build another configuration
128 layer on top of this one, and this class will likely be extended
129 and get data members at that point.
131 The child class needs to implement the method `makeAlgs` which is
132 given a single `ConfigAccumulator` type argument. This is meant to
133 create the sequence of algorithms that this block configures. This
134 is currently (28 Jul 2025) called twice and should do the same thing
135 during both calls, but the plan is to change that to a single call.
137 The child class should also implement the method `getInstanceName`
138 which should return a string that is used to distinguish between
139 multiple instances of the same block. This is used to append the
140 instance name to the names of all algorithms created by this block,
141 and may in the future also be used to distinguish between multiple
142 instances of the block.
154 self.
addOption(
'groupName',
'', type=str,
155 info=(
'Used to specify this block when setting an'
156 ' option at an arbitrary location.'))
157 self.
addOption(
'skipOnData',
False, type=bool,
158 info=(
'User option to prevent the block from running'
159 ' on data. This only affects blocks that are'
160 ' intended to run on data.'))
161 self.
addOption(
'skipOnMC',
False, type=bool,
162 info=(
'User option to prevent the block from running'
163 ' on MC. This only affects blocks that are'
164 ' intended to run on MC.'))
165 self.
addOption(
'skipWithSystematics',
False, type=bool,
166 info=(
'User option to prevent the block from running with systematics.'))
167 self.
addOption(
'onlyForDSIDs', [], type=list,
168 info=(
'Used to specify which MC DSIDs to allow this'
169 ' block to run on. Each element of the list'
170 ' can be a full DSID (e.g. 410470), or a regex'
171 ' (e.g. 410.* to select all 410xxx DSIDs, or'
172 ' ^(?!410) to veto them). An empty list means no'
173 ' DSID restriction.'))
174 self.
addOption(
'propertyOverrides', {}, type=dict,
175 info=(
'EXPERT USE ONLY: A dictionary of properties to'
176 ' override at the end of configuration. This should'
178 ' {"algName.toolName.propertyName": value, ...},'
179 ' without any automatically applied postfixes for'
180 ' the algorithm name. THIS IS MEANT TO BE EXPERT'
181 ' USAGE ONLY. Properties that need to be set by'
182 ' the user should be declared as options on the'
183 ' block itself. EXPERT USE ONLY!'),
187 if cls
not in ConfigBlock.instance_counts:
188 ConfigBlock.instance_counts[cls] = 0
191 stack = inspect.stack()
192 for frame_info
in stack:
194 parent_cls = frame_info.frame.f_locals.get(
'self',
None)
195 if parent_cls
is None or not isinstance(parent_cls, ConfigBlock):
197 if frame_info.function ==
"makeConfig":
198 ConfigBlock.instance_counts[cls] += 1
211 """get the factory name for this block
213 This is mostly to give a reliable means of identifying the type
214 of block we have in error messages. This is meant to be
215 automatically set by the factory based on the requested block
216 name, but there are a number of fallbacks. It is best not to
217 assume a specific format, this is mostly meant to be used as an
218 identifier in output messages.
226 return self.__class__.__name__
229 """set the factory name for this block
231 This is meant to be called automatically by the factory based on
232 the requested block name. If you are creating a block without a factory,
233 you can call this method to set the factory name manually.
238 """Get the name of the instance
240 The name of the instance is used to distinguish between multiple
241 instances of the same block. Most importantly, this will be
242 appended to the names of all algorithms created by this block.
243 This defaults to an empty string, but block implementations
244 should override it with an appropriate name based on identifying
245 options set on this instance. A typical example would be the
246 name of the (main) container, plus potentially the selection or
249 Ideally all blocks should override this method, but for backward
250 compatibility (28 Jul 25) it defaults to an empty string.
256 whether this block should be used for the given configuration
258 This is used by `ConfigSequence` to determine whether this block
259 should be included in the configuration.
261 if self.skipWithSystematics
and not config.noSystematics():
263 if self.skipOnData
and config.dataType()
is DataType.Data:
265 if self.skipOnMC
and config.dataType()
is not DataType.Data:
273 Apply any configuration overrides specified in the block's
274 `propertyOverrides` option. This is meant to be called at the
275 end of the configuration process, after all algorithms have been
276 created and configured.
278 for key, value
in self.propertyOverrides.items():
280 parts = key.split(
'.')
282 raise Exception(f
"Invalid override key format: {key}")
283 alg = config.getAlgorithm(parts[0])
285 raise Exception(f
"Algorithm {parts[0]} not found in config for override: {key}")
286 for name
in parts[1:-1]:
288 if hasattr(alg, name):
289 alg = getattr(alg, name)
291 raise Exception(f
"Tool {name} not found for override: {key}")
294 alg.__setattr__(parts[-1], value)
298 Add a dependency for the block. Dependency is corresponds to the
299 blockName of another block. If required is True, will throw an
300 error if dependency is not present; otherwise will move this
301 block after the required block. If required is False, will do
302 nothing if required block is not present; otherwise, it will
303 move block after required block.
307 self.
addOption(
'ignoreDependencies', [], type=list,
308 info=
'List of dependencies defined in the ConfigBlock to ignore.')
312 """Return True if there is a dependency."""
316 """Return the list of dependencies. """
320 type, info='', noneAction='ignore', required=False,
321 expertMode=None, meta=None) :
322 """declare the given option on the configuration block
324 This should only be called in the constructor of the
327 NOTE: The backend to option handling is slated to be replaced
328 at some point. This particular function should essentially
329 stay the same, but some behavior may change.
332 raise KeyError (f
'duplicate option: {name}')
333 if type
not in [str, bool, int, float, list, dict,
None] :
334 raise TypeError (f
'unknown option type: {type}')
335 noneActions = [
'error',
'set',
'ignore']
336 if noneAction
not in noneActions :
337 raise ValueError (f
'invalid noneAction: {noneAction} [allowed values: {noneActions}]')
340 if expertMode
is not None:
341 if expertMode
is True:
344 elif not isinstance(expertMode, list):
345 raise TypeError (f
'expertMode must be a list, got {type(expertMode)}')
351 if not isinstance(meta, dict):
352 raise TypeError(f
'meta must be a dictionary, got {type(meta)}')
354 unknown =
set(meta) - {
'choices',
'role'}
356 raise ValueError(f
'meta received unknown keys: {unknown}')
358 if 'choices' in meta:
359 choices = meta[
'choices']
360 if (
not isinstance(choices, tuple)
362 or not isinstance(choices[0], list)
363 or not all(isinstance(choice, str)
for choice
in choices[0])
364 or (choices[1]
is not None and not isinstance(choices[1], int))
366 raise TypeError(
"meta['choices'] must be a (list[str], int | None) tuple")
370 if role
not in {
'container',
'containerRef',
'selection',
'region'}:
375 raise ValueError(f
"meta['role'] must be one of 'container', 'containerRef', 'selection', 'region', got '{role}'")
377 setattr (self, name, defaultValue)
379 noneAction=noneAction, required=required,
380 default=defaultValue, meta=meta)
384 """set the given option on the configuration block
386 NOTE: The backend to option handling is slated to be replaced
387 at some point. This particular function should essentially
388 stay the same, but some behavior may change.
392 raise KeyError (f
'unknown option "{name}" in block "{self.__class__.__name__}"')
393 noneAction = self.
_options[name].noneAction
394 if value
is not None or noneAction ==
'set' :
398 if optType
is float
and type(value)
is int:
400 if optType
is not None and optType !=
type(value):
401 raise ValueError(f
'{name} for block {self.__class__.__name__} should '
402 f
'be of type {optType} not {type(value)}')
403 setattr (self, name, value)
404 elif noneAction ==
'ignore' :
406 elif noneAction ==
'error' :
407 raise ValueError (f
'passed None for setting option {name} with noneAction=error')
411 """Returns config option value, if present; otherwise return None"""
413 return getattr(self, name)
417 """Return a copy of the options associated with the block"""
423 Prints options and their values
425 def printWrap(text, width=60, indent=" "):
426 wrapper = textwrap.TextWrapper(width=width, initial_indent=indent,
427 subsequent_indent=indent)
428 for line
in wrapper.wrap(text=text):
429 logCPAlgCfgBlock.info(line)
433 logCPAlgCfgBlock.info(indent + f
"\033[4m{opt}\033[0m: {self.getOptionValue(opt)}")
434 logCPAlgCfgBlock.info(indent*2 + f
"\033[4mtype\033[0m: {vals.type}")
435 logCPAlgCfgBlock.info(indent*2 + f
"\033[4mdefault\033[0m: {vals.default}")
436 logCPAlgCfgBlock.info(indent*2 + f
"\033[4mrequired\033[0m: {vals.required}")
437 logCPAlgCfgBlock.info(indent*2 + f
"\033[4mnoneAction\033[0m: {vals.noneAction}")
438 printWrap(f
"\033[4minfo\033[0m: {vals.info}", indent=indent*2)
440 logCPAlgCfgBlock.info(indent + f
"{ opt}: {self.getOptionValue(opt)}")
444 """whether the configuration block has the given option
446 WARNING: The backend to option handling is slated to be
447 replaced at some point. This particular function may change
448 behavior, interface or be removed/replaced entirely.
455 Implementation of == operator. Used for seaching configSeque.
456 E.g. if blockName in configSeq:
468 return ConfigBlock.instance_counts.get(cls, 0)
472 Check whether value matches an expert mode rule.
474 - A literal (compared with ==)
475 - A callable predicate (called with value)
476 - A special marker string (common callable)
481 if isinstance(rule, str):
482 if rule ==
"nonemptystring":
483 return isinstance(value, str)
and value !=
""
484 if rule ==
"nonemptylist":
485 return isinstance(value, list)
and value != []
486 if rule ==
"positiveint":
487 return isinstance(value, int)
and value > 0
494 Check if any settings require expert mode and validate accordingly.
495 If any setting is set to a value that requires expert mode but we're
496 not in expert mode, raise an error.
500 default_value = self.
_options[option_name].default
502 if expert_rule
is True:
504 if current_value != default_value:
506 f
"Block '{self.factoryName()}' option '{option_name}' "
507 f
"set to '{current_value}' (default '{default_value}'), "
508 f
"requires expert mode.",
509 ExpertModeWarning, stacklevel=2
513 for ev
in expert_rule:
516 f
"Block '{self.factoryName()}' option '{option_name}' "
517 f
"set to expert-only value '{current_value}'. "
518 f
"Requires expert mode.",
519 ExpertModeWarning, stacklevel=2
__init__(self, blockName, required=True)
__init__(self, type=None, info='', noneAction='ignore', required=False, default=None, meta=None)
checkExpertSettings(self, config)
applyConfigOverrides(self, config)
addOption(self, name, defaultValue, *, type, info='', noneAction='ignore', required=False, expertMode=None, meta=None)
getOptionValue(self, name)
isUsedForConfig(self, config)
_is_expert_value(self, rule, value)
printOptions(self, verbose=False, width=60, indent=" ")
setOptionValue(self, name, value)
addDependency(self, dependencyName, required=True)
setFactoryName(self, name)
std::string replace(std::string s, const std::string &s2, const std::string &s3)
alphanumeric_block_name(func)
filter_dsids(filterList, config)