6 from typing
import Dict, List, Set, Tuple, cast
13 from AthenaConfiguration.iconfTool.models.element
import (
17 from AthenaConfiguration.iconfTool.models.structure
import ComponentsStructure
19 logger = logging.getLogger(
"confTool")
20 logger.setLevel(level=logging.INFO)
21 logger.addHandler(logging.FileHandler(
"confTool-last-run.log", mode=
'w'))
23 componentRenamingDict={}
25 baseParser = argparse.ArgumentParser()
26 baseParser.add_argument(
30 help=
"Include only components matching (regex) this string",
33 baseParser.add_argument(
37 help=
"Exclude components matching (regex) this string",
40 baseParser.add_argument(
42 help=
"Ignore differences in e.g. outputlevel",
46 baseParser.add_argument(
50 help=
"Only list the components selected by the given classname (anchored regular expression) - only used in class listing, for diff or similar use --includeComps"
53 baseParser.add_argument(
57 help=
"Don't list the components excluded by the given classname (anchored regular expression) - only used in class listing, for diff or similar use --excludeComps"
60 baseParser.add_argument(
76 help=
"Ignore properties",
78 baseParser.add_argument(
81 help=
"Pass comps You want to rename as OldName=NewName.",
84 baseParser.add_argument(
86 help=
"Pass the file containing remaps",
89 baseParser.add_argument(
91 help=
"Ignore values that are identical to the c++ defaults. Use it only when when the same release is setup as the one used to generate the config.",
95 baseParser.add_argument(
96 "--ignoreDefaultNamedComps",
97 help=
"""Ignores default handles that have full type specified. That is, if the setting is actually: Tool/A and the default value was just A, the Tool/A is assumed to be default and eliminated.
98 Beware that there is a caveat, the ignored class name may be actually different from the default (there is no way to check that in python).""",
103 baseParser.add_argument(
104 "--shortenDefaultComponents",
105 help=
"Automatically shorten component names that have a default name i.e. ToolX/ToolX to ToolX. It helps comparing Run2 & Run3 configurations where these are handled differently",
109 baseParser.add_argument(
111 help=
"Do not load properties other than those referring to other components",
115 baseParser.add_argument(
117 help=
"Follow to related components up to given recursion depth (3)",
122 baseParser.add_argument(
124 help=
"Enable tool debugging messages",
129 return [item
for elem
in l
for item
in elem]
if l
else []
133 """Updates the dictionary with (potentially) component name -> component type"""
136 s = ast.literal_eval(
str(value))
138 if isinstance(s, list):
144 if isinstance(value,str):
145 slash_startend = value.startswith(
"/")
or value.endswith(
"/")
146 json_dict = value.startswith(
"{")
and value.endswith(
"}")
147 if value.count(
"/")==1
and not parsable
and not slash_startend
and not json_dict:
148 comp = value.split(
"/")
151 dict_to_update[f
'{comp_name}.{comp[1]}'] = comp[0]
152 dict_to_update[f
'{comp[1]}'] = comp[0]
153 logger.debug(
"Parsing %s, found type of %s.%s to be %s", value, comp_name, comp[1], comp[0])
155 logger.debug(
"What is typeless comp? %s", value)
156 if isinstance(value, dict):
157 for v
in value.values():
163 for (comp_name, comp_settings)
in conf.items():
175 def eligible(component):
176 exclude = any(re.match(s, component)
for s
in compsToExclude)
177 if (component
in compsToFollow
or component.removeprefix(
"ToolSvc.")
in compsToFollow)
and not (exclude
or component
in args.ignore):
178 logger.debug(
"Considering this component: %s because some other one depends on it", component)
180 include = any(re.match(s, component)
for s
in compsToInclude)
181 if args.includeComps
and args.excludeComps:
182 return include
and not exclude
183 elif args.includeComps:
185 elif args.excludeComps:
188 for (comp_name, comp_attributes)
in dic.items():
189 if eligible(comp_name):
190 conf[comp_name] = comp_attributes
194 logger.debug(
"Following up for types included in here %s whole set of components to follow %s ", types, compsToFollow)
195 compsToFollow += types.keys()
196 logger.debug(
"Included component %s", comp_name)
198 logger.debug(
"Ignored component %s", comp_name)
204 def remove_irrelevant(val_dict):
206 { key: val
for key, val
in val_dict.items()
if key
not in args.ignore }
207 if isinstance(val_dict, dict)
211 for (key, value)
in dic.items():
212 conf[key] = remove_irrelevant(value)
217 if args.renameCompsFile:
218 with open( args.renameCompsFile,
"r")
as refile:
220 if not (line.startswith(
"#")
or line.isspace() ):
221 compsToRename.append( line.rstrip(
'\n') )
222 componentRenamingDict.update({
224 for old_name, new_name
in [
225 [e.strip()
for e
in element.split(
"=")]
for element
in compsToRename
228 for f,t
in componentRenamingDict.items():
229 logger.info(
"Renaming from: %s to %s", f, t)
231 def rename_comps(comp_name):
232 """Renames component if it is in the dict or, when name fragment is in the dict
233 The later is for cases like: ToolSvc.ToolA.X.Y is renamed to ToolSvc.ToolB.X.Y
235 logger.debug(
"Trying renaming on, %s", comp_name)
236 for k,v
in componentRenamingDict.items():
243 return comp_name.replace(old, f
".{v}.")
246 if comp_name.startswith(old):
247 return comp_name.replace(old, f
"{v}.")
251 if comp_name.endswith(old):
252 return comp_name.replace(old, f
".{k}")
256 for (key, value)
in dic.items():
257 renamed = rename_comps(key)
259 logger.debug(
"Renamed comp %s to %s", key, renamed)
260 conf[renamed] = value
265 def drop_defaults(component_name, val_dict):
267 component_name_last_part = component_name.split(
".")[-1]
268 component_type = known.get(component_name, known.get(component_name_last_part, component_name_last_part))
271 from AthenaConfiguration.ComponentFactory
import CompFactory
272 comp_cls = CompFactory.getComp(component_type)
273 logger.debug(
"Loaded the configuration class %s/%s for defaults elimination", component_type, component_name)
275 logger.debug(
"Could not find the configuration class %s/%s, no defaults for it can be eliminated", component_type, component_name)
279 for k,v
in val_dict.items():
280 if not hasattr(comp_cls,
'_descriptors'):
281 logger.debug(
'No \'_descriptors\' attibute for %s', comp_cls)
283 if k
not in comp_cls._descriptors:
286 default =
str(comp_cls._descriptors[k].default)
289 default.replace(
"StoreGateSvc+",
"") == sv.replace(
"StoreGateSvc+",
"")
or
290 default.replace(
"ConditionStore+",
"") == sv.replace(
"ConditionStore+",
"")):
291 logger.debug(
"Dropped default value \'%s\' of property %s in %s because the default is \'%s\'", sv, k, component_name,
str(default))
292 elif args.ignoreDefaultNamedComps
and isinstance(v, str)
and sv.endswith(f
"/{default}"):
293 logger.debug(
"Dropped speculatively value %s of property %s in %s because the default it ends with %s", sv, k, component_name,
str(default))
296 logger.debug(
"Keep value %s of property %s in %s because it is different from default %s",
str(v),
str(k), component_name,
str(comp_cls._descriptors[k].default))
300 for (comp_name, comp_settings)
in allconf.items():
301 remaining = drop_defaults(comp_name, comp_settings)
302 if len(remaining) != 0:
303 conf[comp_name] = remaining
312 value = ast.literal_eval(
str(value))
316 if isinstance(value, str):
317 svalue = value.split(
"/")
318 if len(svalue) == 2
and svalue[0] == svalue[1]:
319 logger.debug(
"Shortened %s", svalue)
321 if isinstance(value, list):
322 return [shorten(el)
for el
in value]
323 if isinstance(value, dict):
324 return shorten_defaults(value)
328 def shorten_defaults(val_dict):
329 if isinstance(val_dict, dict):
330 return { key: shorten(val)
for key,val
in val_dict.items() }
332 for (key, value)
in dic.items():
333 conf[key] = shorten_defaults(value)
337 """Returns a list of (component,class) if value stores reference to other components
338 value - the value to check
339 compname - full component name
340 conf - complete config dict
343 def _getSvcClass(instance):
344 """Find instance in service lists to get class.
345 Keeps a cache of the service classes in the svcCache default value.
346 That's fine, unless we are dealing with more than one conf in the program.
347 In that case, initialise svcCache to {} and specify in the caller."""
349 props = conf.get(
'ApplicationMgr',{
"":
None})
350 if isinstance(props,dict):
351 for prop,val
in props.items():
354 val = ast.literal_eval(
str(val))
357 if isinstance(val,list):
359 if isinstance(v,str):
362 if svcCache.setdefault(vv[1], vv[0]) != vv[0]:
363 svcCache[vv[1]] =
None
364 return svcCache.get(instance)
367 value = ast.literal_eval(
str(value))
371 if isinstance(value, str):
372 ctype_name = value.split(
'/')
373 cls = ctype_name[0]
if len(ctype_name) == 2
else None
374 instance = ctype_name[-1]
377 if compname
and f
"{compname}.{instance}" in conf:
378 ref = f
"{compname}.{instance}"
379 elif f
"ToolSvc.{instance}" in conf:
380 ref = f
"ToolSvc.{instance}"
381 elif cls
is not None or instance
in conf:
384 cls = _getSvcClass(instance)
388 elif isinstance(value, list):
389 refs = [
isReference(el, compname, conf)
for el
in value]
392 [flattened.extend(el)
for el
in refs
if el]
399 for (name, properties)
in conf.items():
401 if not isinstance(properties, dict):
402 updated[name] = properties
404 for property_name, value
in properties.items():
405 if isReference( value, name, conf)
or property_name ==
'Members':
406 updated[name][property_name] = value
410 """loads config file into a dictionary, supports several modifications of the input switched on via additional arguments
411 Supports reading: Pickled file with the CA or properties & JSON
414 print(
"Debugging info from reading ", fname,
" in ", logger.handlers[0].baseFilename)
415 logger.setLevel(logging.DEBUG)
418 if fname.endswith(
".pkl"):
419 with open(fname,
"rb")
as input_file:
421 cfg = pickle.load(input_file)
422 logger.info(
"... Read %s from %s", cfg.__class__.__name__, fname)
423 from AthenaConfiguration.ComponentAccumulator
import ComponentAccumulator
424 if isinstance(cfg, ComponentAccumulator):
425 props = cfg.gatherProps()
429 for comp, name, value
in jos_props:
430 to_json.setdefault(comp, {})[name] = value
431 to_json[comp][name] = value
433 conf[
'ApplicationMgr'] = props[0]
434 conf[
'MessageSvc'] = props[1]
437 cfg, (collections.defaultdict, dict)
440 conf.update(pickle.load(input_file))
442 elif isinstance(cfg, (collections.Sequence)):
445 logger.info(
"... Read %d items from python pickle file: %s", len(conf), fname)
447 elif fname.endswith(
".json"):
449 def __keepPlainStrings(element):
450 if isinstance(element, str):
452 if isinstance(element, list):
453 return [__keepPlainStrings(x)
for x
in element]
454 if isinstance(element, dict):
456 __keepPlainStrings(key): __keepPlainStrings(value)
457 for key, value
in element.items()
461 with open(fname,
"r")
as input_file:
462 cfg = json.load(input_file, object_hook=__keepPlainStrings)
471 if 'properties' in conf:
472 conf = conf[
'properties']
474 logger.info(
"... Read %d items from json file: %s", len(conf), fname)
477 sys.exit(
"File format not supported.")
480 sys.exit(
"Unable to load %s file" % fname)
482 if args.includeComps
or args.excludeComps
or args.includeClasses
or args.excludeClasses:
483 logger.info(f
"include/exclude comps like {args.includeComps}/{args.excludeComps}")
486 if args.ignoreIrrelevant:
489 if args.renameComps
or args.renameCompsFile:
492 if args.ignoreDefaults:
496 if args.shortenDefaultComponents:
499 if args.skipProperties:
504 def __init__(self, file_path: str, args, checked_elements=
set()) ->
None:
505 self.file_path: str = file_path
506 self.checked_elements: Set[str] = checked_elements
510 logger.info(f
"Loading {self.file_path}")
516 structure = ComponentsStructure(data, self.checked_elements)
529 checked_elements: Set[str],
532 file_path, checked_elements
535 diff_file_path, checked_elements
538 def get_data(self) -> Tuple[ComponentsStructure, ComponentsStructure]:
539 structure = self.main_loader.load_structure()
540 diff_structure = self.diff_loader.load_structure()
542 return structure, diff_structure
544 def equal(self, first: Element, second: Element) -> bool:
546 first.get_name() == second.get_name()
547 and first.x_pos == second.x_pos
552 self, structure: List[Element], diff_structure: List[Element]
555 while i < len(structure)
and j < len(diff_structure):
556 if self.
equal(structure[i], diff_structure[j]):
557 if isinstance(structure[i], GroupingElement):
559 cast(GroupingElement, structure[i]).children,
560 cast(GroupingElement, diff_structure[j]).children,
567 for tmp_j
in range(j, len(diff_structure)):
568 if self.
equal(structure[i], diff_structure[tmp_j]):
569 for marking_j
in range(j, tmp_j):
570 diff_structure[marking_j].mark()
576 for tmp_i
in range(i, len(structure)):
577 if self.
equal(structure[tmp_i], diff_structure[j]):
578 for marking_i
in range(i, tmp_i):
579 structure[marking_i].mark()
584 diff_structure[j].mark()
589 while i < len(structure):
593 while j < len(diff_structure):
594 diff_structure[j].mark()
600 Read differences file
602 full_component_name.property oldvalue=newvalue
604 AlgX.ToolA.SubToolB.SettingZ 45=46
605 It is possible to specify missing values, e.g:
606 AlgX.ToolA.SubToolB.SettingZ 45= means that now the old value should be ignored
607 AlgX.ToolA.SubToolB.SettingZ =46 means that now the new value should be ignored
608 AlgX.ToolA.SubToolB.SettingZ = means that any change of the value should be ignored
611 from collections
import defaultdict
612 differences = defaultdict(dict)
614 with open(fname,
"r")
as f:
616 if line[0] ==
"#" or line ==
"\n":
619 compAndProp, values = line.split(
" ")
620 comp, prop = compAndProp.rsplit(
".", 1)
621 o,n = values.split(
"=")
622 oldval,newval = o
if o
else None, n
if n
else None
624 differences[comp][prop] = (oldval,newval)
626 logger.info(
"... Read %d known differences from file: %s", count, fname)
627 logger.info(
"..... %s",
str(differences))