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 global componentRenamingDict
223 componentRenamingDict.update({
225 for old_name, new_name
in [
226 [e.strip()
for e
in element.split(
"=")]
for element
in compsToRename
229 for f,t
in componentRenamingDict.items():
230 logger.info(
"Renaming from: %s to %s", f, t)
232 def rename_comps(comp_name):
233 """Renames component if it is in the dict or, when name fragment is in the dict
234 The later is for cases like: ToolSvc.ToolA.X.Y is renamed to ToolSvc.ToolB.X.Y
236 logger.debug(
"Trying renaming on, %s", comp_name)
237 for k,v
in componentRenamingDict.items():
244 return comp_name.replace(old, f
".{v}.")
247 if comp_name.startswith(old):
248 return comp_name.replace(old, f
"{v}.")
252 if comp_name.endswith(old):
253 return comp_name.replace(old, f
".{k}")
257 for (key, value)
in dic.items():
258 renamed = rename_comps(key)
260 logger.debug(
"Renamed comp %s to %s", key, renamed)
261 conf[renamed] = value
266 def drop_defaults(component_name, val_dict):
268 component_name_last_part = component_name.split(
".")[-1]
269 component_type = known.get(component_name, known.get(component_name_last_part, component_name_last_part))
272 from AthenaConfiguration.ComponentFactory
import CompFactory
273 comp_cls = CompFactory.getComp(component_type)
274 logger.debug(
"Loaded the configuration class %s/%s for defaults elimination", component_type, component_name)
276 logger.debug(
"Could not find the configuration class %s/%s, no defaults for it can be eliminated", component_type, component_name)
280 for k,v
in val_dict.items():
281 if not hasattr(comp_cls,
'_descriptors'):
282 logger.debug(
'No \'_descriptors\' attibute for %s', comp_cls)
284 if k
not in comp_cls._descriptors:
287 default =
str(comp_cls._descriptors[k].default)
290 default.replace(
"StoreGateSvc+",
"") == sv.replace(
"StoreGateSvc+",
"")
or
291 default.replace(
"ConditionStore+",
"") == sv.replace(
"ConditionStore+",
"")):
292 logger.debug(
"Dropped default value \'%s\' of property %s in %s because the default is \'%s\'", sv, k, component_name,
str(default))
293 elif args.ignoreDefaultNamedComps
and isinstance(v, str)
and sv.endswith(f
"/{default}"):
294 logger.debug(
"Dropped speculatively value %s of property %s in %s because the default it ends with %s", sv, k, component_name,
str(default))
297 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))
301 for (comp_name, comp_settings)
in allconf.items():
302 remaining = drop_defaults(comp_name, comp_settings)
303 if len(remaining) != 0:
304 conf[comp_name] = remaining
313 value = ast.literal_eval(
str(value))
317 if isinstance(value, str):
318 svalue = value.split(
"/")
319 if len(svalue) == 2
and svalue[0] == svalue[1]:
320 logger.debug(
"Shortened %s", svalue)
322 if isinstance(value, list):
323 return [shorten(el)
for el
in value]
324 if isinstance(value, dict):
325 return shorten_defaults(value)
329 def shorten_defaults(val_dict):
330 if isinstance(val_dict, dict):
331 return { key: shorten(val)
for key,val
in val_dict.items() }
333 for (key, value)
in dic.items():
334 conf[key] = shorten_defaults(value)
338 """Returns a list of (component,class) if value stores reference to other components
339 value - the value to check
340 compname - full component name
341 conf - complete config dict
344 def _getSvcClass(instance):
345 """Find instance in service lists to get class.
346 Keeps a cache of the service classes in the svcCache default value.
347 That's fine, unless we are dealing with more than one conf in the program.
348 In that case, initialise svcCache to {} and specify in the caller."""
350 props = conf.get(
'ApplicationMgr',{
"":
None})
351 if isinstance(props,dict):
352 for prop,val
in props.items():
355 val = ast.literal_eval(
str(val))
358 if isinstance(val,list):
360 if isinstance(v,str):
363 if svcCache.setdefault(vv[1], vv[0]) != vv[0]:
364 svcCache[vv[1]] =
None
365 return svcCache.get(instance)
368 value = ast.literal_eval(
str(value))
372 if isinstance(value, str):
373 ctype_name = value.split(
'/')
374 cls = ctype_name[0]
if len(ctype_name) == 2
else None
375 instance = ctype_name[-1]
378 if compname
and f
"{compname}.{instance}" in conf:
379 ref = f
"{compname}.{instance}"
380 elif f
"ToolSvc.{instance}" in conf:
381 ref = f
"ToolSvc.{instance}"
382 elif cls
is not None or instance
in conf:
385 cls = _getSvcClass(instance)
389 elif isinstance(value, list):
390 refs = [
isReference(el, compname, conf)
for el
in value]
393 [flattened.extend(el)
for el
in refs
if el]
400 for (name, properties)
in conf.items():
402 if not isinstance(properties, dict):
403 updated[name] = properties
405 for property_name, value
in properties.items():
406 if isReference( value, name, conf)
or property_name ==
'Members':
407 updated[name][property_name] = value
411 """loads config file into a dictionary, supports several modifications of the input switched on via additional arguments
412 Supports reading: Pickled file with the CA or properties & JSON
415 print(
"Debugging info from reading ", fname,
" in ", logger.handlers[0].baseFilename)
416 logger.setLevel(logging.DEBUG)
419 if fname.endswith(
".pkl"):
420 with open(fname,
"rb")
as input_file:
422 cfg = pickle.load(input_file)
423 logger.info(
"... Read %s from %s", cfg.__class__.__name__, fname)
424 from AthenaConfiguration.ComponentAccumulator
import ComponentAccumulator
425 if isinstance(cfg, ComponentAccumulator):
426 props = cfg.gatherProps()
430 for comp, name, value
in jos_props:
431 to_json.setdefault(comp, {})[name] = value
432 to_json[comp][name] = value
434 conf[
'ApplicationMgr'] = props[0]
435 conf[
'MessageSvc'] = props[1]
438 cfg, (collections.defaultdict, dict)
441 conf.update(pickle.load(input_file))
443 elif isinstance(cfg, (collections.Sequence)):
446 logger.info(
"... Read %d items from python pickle file: %s", len(conf), fname)
448 elif fname.endswith(
".json"):
450 def __keepPlainStrings(element):
451 if isinstance(element, str):
453 if isinstance(element, list):
454 return [__keepPlainStrings(x)
for x
in element]
455 if isinstance(element, dict):
457 __keepPlainStrings(key): __keepPlainStrings(value)
458 for key, value
in element.items()
462 with open(fname,
"r")
as input_file:
463 cfg = json.load(input_file, object_hook=__keepPlainStrings)
472 if 'properties' in conf:
473 conf = conf[
'properties']
475 logger.info(
"... Read %d items from json file: %s", len(conf), fname)
478 sys.exit(
"File format not supported.")
481 sys.exit(
"Unable to load %s file" % fname)
483 if args.includeComps
or args.excludeComps
or args.includeClasses
or args.excludeClasses:
484 logger.info(f
"include/exclude comps like {args.includeComps}/{args.excludeComps}")
487 if args.ignoreIrrelevant:
490 if args.renameComps
or args.renameCompsFile:
493 if args.ignoreDefaults:
497 if args.shortenDefaultComponents:
500 if args.skipProperties:
505 def __init__(self, file_path: str, args, checked_elements=
set()) ->
None:
506 self.file_path: str = file_path
507 self.checked_elements: Set[str] = checked_elements
511 logger.info(f
"Loading {self.file_path}")
517 structure = ComponentsStructure(data, self.checked_elements)
530 checked_elements: Set[str],
533 file_path, checked_elements
536 diff_file_path, checked_elements
539 def get_data(self) -> Tuple[ComponentsStructure, ComponentsStructure]:
540 structure = self.main_loader.load_structure()
541 diff_structure = self.diff_loader.load_structure()
543 return structure, diff_structure
545 def equal(self, first: Element, second: Element) -> bool:
547 first.get_name() == second.get_name()
548 and first.x_pos == second.x_pos
553 self, structure: List[Element], diff_structure: List[Element]
556 while i < len(structure)
and j < len(diff_structure):
557 if self.
equal(structure[i], diff_structure[j]):
558 if isinstance(structure[i], GroupingElement):
560 cast(GroupingElement, structure[i]).children,
561 cast(GroupingElement, diff_structure[j]).children,
568 for tmp_j
in range(j, len(diff_structure)):
569 if self.
equal(structure[i], diff_structure[tmp_j]):
570 for marking_j
in range(j, tmp_j):
571 diff_structure[marking_j].mark()
577 for tmp_i
in range(i, len(structure)):
578 if self.
equal(structure[tmp_i], diff_structure[j]):
579 for marking_i
in range(i, tmp_i):
580 structure[marking_i].mark()
585 diff_structure[j].mark()
590 while i < len(structure):
594 while j < len(diff_structure):
595 diff_structure[j].mark()
601 Read differences file
603 full_component_name.property oldvalue=newvalue
605 AlgX.ToolA.SubToolB.SettingZ 45=46
606 It is possible to specify missing values, e.g:
607 AlgX.ToolA.SubToolB.SettingZ 45= means that now the old value should be ignored
608 AlgX.ToolA.SubToolB.SettingZ =46 means that now the new value should be ignored
609 AlgX.ToolA.SubToolB.SettingZ = means that any change of the value should be ignored
612 from collections
import defaultdict
613 differences = defaultdict(dict)
615 with open(fname,
"r")
as f:
617 if line[0] ==
"#" or line ==
"\n":
620 compAndProp, values = line.split(
" ")
621 comp, prop = compAndProp.rsplit(
".", 1)
622 o,n = values.split(
"=")
623 oldval,newval = o
if o
else None, n
if n
else None
625 differences[comp][prop] = (oldval,newval)
627 logger.info(
"... Read %d known differences from file: %s", count, fname)
628 logger.info(
"..... %s",
str(differences))