ATLAS Offline Software
Loading...
Searching...
No Matches
loaders.py
Go to the documentation of this file.
1# Copyright (C) 2002-2025 CERN for the benefit of the ATLAS collaboration
2
3import pickle
4import ast
5import logging
6from typing import Dict, List, Set, Tuple, cast
7import collections
8import json
9import sys
10import re
11import argparse
12
13from AthenaConfiguration.iconfTool.models.element import (
14 Element,
15 GroupingElement,
16)
17from AthenaConfiguration.iconfTool.models.structure import ComponentsStructure
18
19logger = logging.getLogger("confTool")
20logger.setLevel(level=logging.INFO)
21logger.addHandler(logging.FileHandler("confTool-last-run.log", mode='w'))
22
23componentRenamingDict={}
24
25baseParser = argparse.ArgumentParser()
26baseParser.add_argument(
27 "--includeComps",
28 nargs="*",
29 default=[],
30 help="Include only components matching (regex) this string",
31 action="append",
32)
33baseParser.add_argument(
34 "--excludeComps",
35 nargs="*",
36 default=[],
37 help="Exclude components matching (regex) this string",
38 action="append",
39)
40baseParser.add_argument(
41 "--ignoreIrrelevant",
42 help="Ignore differences in e.g. outputlevel",
43 action="store_true",
44)
45
46baseParser.add_argument(
47 "--includeClasses",
48 action='append',
49 default=[],
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"
51 )
52
53baseParser.add_argument(
54 "--excludeClasses",
55 action='append',
56 default=[],
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"
58)
59
60baseParser.add_argument(
61 "--ignore",
62 action="append",
63 default= [
64 "StoreGateSvc",
65 "OutputLevel",
66 "ExtraInputs",
67 "ExtraOutputs",
68 "DetStore",
69 "EvtStore",
70 "EventStore",
71 "NeededResources",
72 "GeoModelSvc",
73 "MetaDataStore",
74 "wallTimeOffset" # perfmon Svc property, is timestamp of stating the job, different by construction
75 ],
76 help="Ignore properties",
77)
78baseParser.add_argument(
79 "--renameComps",
80 nargs="*",
81 help="Pass comps You want to rename as OldName=NewName.",
82 action="append",
83)
84baseParser.add_argument(
85 "--renameCompsFile",
86 help="Pass the file containing remaps",
87)
88
89baseParser.add_argument(
90 "--ignoreDefaults",
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.",
92 action="store_true"
93)
94
95baseParser.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).""",
99 action="store_true"
100)
101
102
103baseParser.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",
106 action="store_true",
107)
108
109baseParser.add_argument(
110 "--skipProperties",
111 help="Do not load properties other than those referring to other components",
112 action="store_true",
113)
114
115baseParser.add_argument(
116 "--follow",
117 help="Follow to related components up to given recursion depth (3)",
118 type=int,
119 default=3
120)
121
122baseParser.add_argument(
123 "--debug",
124 help="Enable tool debugging messages",
125 action="store_true",
126)
127
129 return [item for elem in l for item in elem] if l else []
130
131
132def types_in_properties(comp_name, value, dict_to_update):
133 """Updates the dictionary with (potentially) component name -> component type"""
134 parsable = False
135 try:
136 s = ast.literal_eval(str(value))
137 parsable = True
138 if isinstance(s, list):
139 for el in s:
140 types_in_properties(comp_name, el, dict_to_update)
141 except Exception:
142 pass
143 # Exclude non-strings, or strings that look like paths rather than type/name pairs
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("/")
149 if len(comp) == 2:
150 # Record with and without parent
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])
154 else:
155 logger.debug("What is typeless comp? %s", value)
156 if isinstance(value, dict):
157 for v in value.values():
158 types_in_properties(comp_name, v, dict_to_update)
159
160
162 name_to_type = {}
163 for (comp_name, comp_settings) in conf.items():
164 types_in_properties(comp_name, comp_settings, name_to_type)
165 return name_to_type
166
167
168def excludeIncludeComps(dic, args, depth, compsToFollow=[]) -> Dict:
169 conf = {}
170 if depth == 0:
171 return conf
172 compsToInclude = __flatten_list(args.includeComps)
173 compsToExclude = __flatten_list(args.excludeComps)
174
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)
179 return True
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:
184 return include
185 elif args.excludeComps:
186 return not exclude
187
188 for (comp_name, comp_attributes) in dic.items():
189 if eligible(comp_name):
190 conf[comp_name] = comp_attributes
191 if depth > 0:
192 types = {}
193 types_in_properties(comp_attributes, types, compsToFollow)
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)
197 else:
198 logger.debug("Ignored component %s", comp_name)
199 if depth > 0:
200 conf.update(excludeIncludeComps(dic, args, depth-1, compsToFollow))
201 return conf
202
203def ignoreIrrelevant(dic, args) -> Dict:
204 def remove_irrelevant(val_dict):
205 return (
206 { key: val for key, val in val_dict.items() if key not in args.ignore }
207 if isinstance(val_dict, dict)
208 else val_dict
209 )
210 conf = {}
211 for (key, value) in dic.items():
212 conf[key] = remove_irrelevant(value)
213 return conf
214
215def renameComps(dic, args) -> Dict:
216 compsToRename = __flatten_list(args.renameComps)
217 if args.renameCompsFile:
218 with open( args.renameCompsFile, "r") as refile:
219 for line in refile:
220 if not (line.startswith("#") or line.isspace() ):
221 compsToRename.append( line.rstrip('\n') )
222 componentRenamingDict.update({
223 old_name: new_name
224 for old_name, new_name in [
225 [e.strip() for e in element.split("=")] for element in compsToRename
226 ]
227 })
228 for f,t in componentRenamingDict.items():
229 logger.info("Renaming from: %s to %s", f, t)
230
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
234 """
235 logger.debug("Trying renaming on, %s", comp_name)
236 for k,v in componentRenamingDict.items():
237 if k == comp_name:
238# logger.debug("Renamed comp %s to %s", k, v)
239 return v
240
241 old = f".{k}."
242 if old in comp_name:
243 return comp_name.replace(old, f".{v}.")
244
245 old = f"{k}."
246 if comp_name.startswith(old):
247 return comp_name.replace(old, f"{v}.")
248
249
250 old = f".{k}"
251 if comp_name.endswith(old):
252 return comp_name.replace(old, f".{k}")
253 return comp_name
254
255 conf = {}
256 for (key, value) in dic.items():
257 renamed = rename_comps(key)
258 if renamed != key:
259 logger.debug("Renamed comp %s to %s", key, renamed)
260 conf[renamed] = value
261 return conf
262
263def ignoreDefaults(allconf, args, known) -> Dict:
264 conf = {}
265 def drop_defaults(component_name, val_dict):
266 # try picking the name from the dict, if missing use last part of the name, if that fails use the component_name (heuristic)
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))
269 comp_cls = None
270 try:
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)
274 except Exception:
275 logger.debug("Could not find the configuration class %s/%s, no defaults for it can be eliminated", component_type, component_name)
276 return val_dict
277 c = {}
278
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)
282 continue
283 if k not in comp_cls._descriptors: # property not in descriptors (for instance, removed from component now)
284 c[k] = v
285 else:
286 default = str(comp_cls._descriptors[k].default)
287 sv = str(v)
288 if (default == sv or
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))
294 else:
295 c[k] = v
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))
297 return c
298
299 # collect types for all components (we look for A/B or lost of A/B strings)
300 for (comp_name, comp_settings) in allconf.items():
301 remaining = drop_defaults(comp_name, comp_settings)
302 if len(remaining) != 0: # ignore components that have only default settings
303 conf[comp_name] = remaining
304 return conf
305
306def shortenDefaultComponents(dic, args) -> Dict:
307 conf = {}
308 def shorten(val):
309 value = val
310 # the value can possibly be a serialized object (like a list)
311 try:
312 value = ast.literal_eval(str(value))
313 except Exception:
314 pass
315
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)
320 return svalue[0]
321 if isinstance(value, list):
322 return [shorten(el) for el in value]
323 if isinstance(value, dict):
324 return shorten_defaults(value)
325
326 return value
327
328 def shorten_defaults(val_dict):
329 if isinstance(val_dict, dict):
330 return { key: shorten(val) for key,val in val_dict.items() }
331
332 for (key, value) in dic.items():
333 conf[key] = shorten_defaults(value)
334 return conf
335
336def isReference(value, compname, conf, svcCache={}) -> list:
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
341 """
342
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."""
348 if not svcCache: # only scan ApplicationMgr once
349 props = conf.get('ApplicationMgr',{"":None})
350 if isinstance(props,dict):
351 for prop,val in props.items():
352 if 'Svc' in prop:
353 try:
354 val = ast.literal_eval(str(val))
355 except Exception:
356 pass
357 if isinstance(val,list):
358 for v in val:
359 if isinstance(v,str):
360 vv = v.split('/')
361 if len(vv) == 2:
362 if svcCache.setdefault(vv[1], vv[0]) != vv[0]:
363 svcCache[vv[1]] = None # fail if same instance, different class
364 return svcCache.get(instance)
365
366 try:
367 value = ast.literal_eval(str(value))
368 except Exception:
369 pass
370
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]
375 ref = None
376 if instance:
377 if compname and f"{compname}.{instance}" in conf: # private tool
378 ref = f"{compname}.{instance}"
379 elif f"ToolSvc.{instance}" in conf: # public tool
380 ref = f"ToolSvc.{instance}"
381 elif cls is not None or instance in conf: # service or other component
382 ref = instance
383 if cls is None:
384 cls = _getSvcClass(instance)
385 if ref is not None:
386 return [(ref, cls)]
387
388 elif isinstance(value, list):
389 refs = [isReference(el, compname, conf) for el in value]
390 if any(refs):
391 flattened = []
392 [flattened.extend(el) for el in refs if el]
393 return flattened
394 return []
395
396
397def skipProperties(conf, args) -> Dict:
398 updated = {}
399 for (name, properties) in conf.items():
400 updated[name] = {}
401 if not isinstance(properties, dict): # keep it
402 updated[name] = properties
403 else:
404 for property_name, value in properties.items():
405 if isReference( value, name, conf) or property_name == 'Members': # later for sequences structure
406 updated[name][property_name] = value
407 return updated
408
409def loadConfigFile(fname, args) -> Dict:
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
412 """
413 if args.debug:
414 print("Debugging info from reading ", fname, " in ", logger.handlers[0].baseFilename)
415 logger.setLevel(logging.DEBUG)
416
417 conf = {}
418 if fname.endswith(".pkl"):
419 with open(fname, "rb") as input_file:
420 # determine if there is a old or new configuration pickled
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): # new configuration
425 props = cfg.gatherProps()
426 # to make json compatible with old configuration
427 jos_props = props[2]
428 to_json = {}
429 for comp, name, value in jos_props:
430 to_json.setdefault(comp, {})[name] = value
431 to_json[comp][name] = value
432 conf.update(to_json)
433 conf['ApplicationMgr'] = props[0]
434 conf['MessageSvc'] = props[1]
435
436 elif isinstance(
437 cfg, (collections.defaultdict, dict)
438 ): # old configuration
439 conf.update(cfg)
440 conf.update(pickle.load(input_file)) # special services
441 # FIXME: there's a third pickle object with python components
442 elif isinstance(cfg, (collections.Sequence)):
443 for c in cfg:
444 conf.update(c)
445 logger.info("... Read %d items from python pickle file: %s", len(conf), fname)
446
447 elif fname.endswith(".json"):
448
449 def __keepPlainStrings(element):
450 if isinstance(element, str):
451 return str(element)
452 if isinstance(element, list):
453 return [__keepPlainStrings(x) for x in element]
454 if isinstance(element, dict):
455 return {
456 __keepPlainStrings(key): __keepPlainStrings(value)
457 for key, value in element.items()
458 }
459 return element
460
461 with open(fname, "r") as input_file:
462 cfg = json.load(input_file, object_hook=__keepPlainStrings)
463 for c in cfg:
464 conf.update(cfg)
465
466 # For compatibility with HLTJobOptions json, which produces structure:
467 # {
468 # "filetype": "joboptions",
469 # "properties": { the thing we are interested in}
470 # }
471 if 'properties' in conf:
472 conf = conf['properties']
473
474 logger.info("... Read %d items from json file: %s", len(conf), fname)
475
476 else:
477 sys.exit("File format not supported.")
478
479 if conf is None:
480 sys.exit("Unable to load %s file" % fname)
481
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}")
484 conf = excludeIncludeComps(conf, args, args.follow)
485
486 if args.ignoreIrrelevant:
487 conf = ignoreIrrelevant(conf, args)
488
489 if args.renameComps or args.renameCompsFile:
490 conf = renameComps(conf, args)
491
492 if args.ignoreDefaults:
493 known_types = collect_types(conf)
494 conf = ignoreDefaults(conf, args, known_types)
495
496 if args.shortenDefaultComponents:
497 conf = shortenDefaultComponents(conf, args)
498
499 if args.skipProperties:
500 conf = skipProperties(conf, args)
501 return conf
502
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
507 self.args = args
508
509 def _load_file_data(self) -> Dict:
510 logger.info(f"Loading {self.file_path}")
511 return loadConfigFile(self.file_path, self.args)
512
513
514 def load_structure(self) -> ComponentsStructure:
515 data = self._load_file_data()
516 structure = ComponentsStructure(data, self.checked_elements)
517 structure.generate()
518 return structure
519
520 def get_data(self) -> ComponentsStructure:
521 return self.load_structure()
522
523
526 self,
527 file_path: str,
528 diff_file_path: str,
529 checked_elements: Set[str],
530 ) -> None:
531 self.main_loader: ComponentsFileLoader = ComponentsFileLoader(
532 file_path, checked_elements
533 )
534 self.diff_loader: ComponentsFileLoader = ComponentsFileLoader(
535 diff_file_path, checked_elements
536 )
537
538 def get_data(self) -> Tuple[ComponentsStructure, ComponentsStructure]:
539 structure = self.main_loader.load_structure()
540 diff_structure = self.diff_loader.load_structure()
541 self.mark_differences(structure.get_list(), diff_structure.get_list())
542 return structure, diff_structure
543
544 def equal(self, first: Element, second: Element) -> bool:
545 return (
546 first.get_name() == second.get_name()
547 and first.x_pos == second.x_pos
548 and type(first) is type(second)
549 )
550
552 self, structure: List[Element], diff_structure: List[Element]
553 ) -> None:
554 i, j = 0, 0
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):
558 self.mark_differences(
559 cast(GroupingElement, structure[i]).children,
560 cast(GroupingElement, diff_structure[j]).children,
561 )
562 i += 1
563 j += 1
564 continue
565
566 # Find equal element in diff structure
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()
571 j = tmp_j
572 break
573 else:
574 # Not found equal element in diff structure
575 # Find equal element in first structure
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()
580 i = tmp_i
581 break
582 else:
583 structure[i].mark()
584 diff_structure[j].mark()
585 i += 1
586 j += 1
587
588 # Mark remaining elements in both structures
589 while i < len(structure):
590 structure[i].mark()
591 i += 1
592
593 while j < len(diff_structure):
594 diff_structure[j].mark()
595 j += 1
596
597
598def loadDifferencesFile(fname) -> Dict:
599 """
600 Read differences file
601 Format:
602 full_component_name.property oldvalue=newvalue
603 example:
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
609
610 """
611 from collections import defaultdict
612 differences = defaultdict(dict)
613 count=0
614 with open(fname, "r") as f:
615 for line in f:
616 if line[0] == "#" or line == "\n":
617 continue
618 line = line.strip()
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
623
624 differences[comp][prop] = (oldval,newval)
625 count+=1
626 logger.info("... Read %d known differences from file: %s", count, fname)
627 logger.info("..... %s", str(differences))
628
629 return differences
630
void print(char *figname, TCanvas *c1)
bool equal(self, Element first, Element second)
Definition loaders.py:544
None mark_differences(self, List[Element] structure, List[Element] diff_structure)
Definition loaders.py:553
None __init__(self, str file_path, str diff_file_path, Set[str] checked_elements)
Definition loaders.py:530
Tuple[ComponentsStructure, ComponentsStructure] get_data(self)
Definition loaders.py:538
None __init__(self, str file_path, args, checked_elements=set())
Definition loaders.py:504
STL class.
Dict loadConfigFile(fname, args)
Definition loaders.py:409
Dict shortenDefaultComponents(dic, args)
Definition loaders.py:306
types_in_properties(comp_name, value, dict_to_update)
Definition loaders.py:132
Dict renameComps(dic, args)
Definition loaders.py:215
Dict excludeIncludeComps(dic, args, depth, compsToFollow=[])
Definition loaders.py:168
Dict ignoreIrrelevant(dic, args)
Definition loaders.py:203
Dict skipProperties(conf, args)
Definition loaders.py:397
Dict ignoreDefaults(allconf, args, known)
Definition loaders.py:263
list isReference(value, compname, conf, svcCache={})
Definition loaders.py:336