ATLAS Offline Software
confTool.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 #
4 # Copyright (C) 2002-2024 CERN for the benefit of the ATLAS collaboration
5 #
6 
7 import ast
8 import json
9 import pickle
10 import re
11 import sys
12 
13 from AthenaConfiguration.iconfTool.models.loaders import loadConfigFile, baseParser, componentRenamingDict, loadDifferencesFile, isReference
14 class fullColor:
15  reset="\033[0m"
16  difference="\033[91m"
17  knowndifference="\033[35m"
18  first="\033[92m"
19  property="\033[94m"
20  second="\033[35m"
21  component="\33[92m"
22  value="\33[91m"
23 
24 class noColor:
25  reset=""
26  difference=""
27  knowndifference=""
28  first=""
29  property=""
30  second=""
31  component=""
32  value=""
33 
34 
35 def parse_args():
36  parser = baseParser
37  parser.add_argument(
38  "-q", "--quiet", action="store_true", help="Don't print command arguments"
39  )
40  parser.add_argument(
41  "-p", "--printConf", action="store_true", help="Prints entire configuration"
42  )
43  parser.add_argument(
44  "--printComps", action="store_true", help="Prints only the components"
45  )
46  parser.add_argument(
47  "--diff", dest="diff", action="store_true", help="Diffs two files"
48  )
49  parser.add_argument("--toJSON", action="store_true", help="Convert pickle to JSON file")
50  parser.add_argument("--toPickle", action="store_true", help="Convert JSON to pickle file")
51 
52  parser.add_argument("file", nargs="+", help="Files to work with")
53  parser.add_argument(
54  "--ignoreMissing",
55  help="Don't report components existing in only one of the two configurations",
56  action="store_true",
57  )
58  parser.add_argument(
59  "--ignoreOrder",
60  metavar="PROP",
61  action="append",
62  default=[],
63  help="Ignore order for sequence properties matching regex",
64  )
65  parser.add_argument(
66  "--allComponentPrint",
67  help="Print all component if there are differences in any of its properties",
68  action="store_true",
69  )
70  parser.add_argument(
71  "--printIdenticalComponents",
72  help="Print all components even, if there are no differences.",
73  action="store_true",
74  )
75  parser.add_argument(
76  "--printIdenticalPerParameter",
77  help="Print all parameters in component with difference even, if there are no differences.",
78  action="store_true",
79  )
80  parser.add_argument("--knownDifferencesFile",
81  help="Ignore differences enlisted in file (to be used only with diffing)")
82 
83  parser.add_argument("--color",
84  help="Use colored output even for file output (useful when piping to less -R to to HTML conversion",
85  action="store_true")
86 
87  parser.add_argument("-s", "--structured",
88  action='append',
89  default=[],
90  help="Print only a single component, in a structured manner (reflecting components parent children)")
91 
92  parser.add_argument("--includeClassesSub",
93  action='append',
94  default=[],
95  help="Below the top-level, also include the components selected by the given classname (anchored regular expression)")
96 
97  parser.add_argument("--includeSequences",
98  help="Include sequences in the structured printout",
99  action="store_true")
100 
101  parser.add_argument("--classes",
102  action="store_true",
103  help="Only show class names")
104 
105  parser.add_argument("--uniqueClasses",
106  action="store_true",
107  help="Only show unique classes")
108 
109  parser.add_argument("--showComponentName",
110  help="Show component name with --classes",
111  action="store_true")
112 
113  parser.add_argument("--maxDepth",
114  help="Maximum depth for structured printout",
115  type=int,
116  default=None)
117 
118  args = parser.parse_args()
119  if not args.quiet:
120  print("Run with arguments:")
121  print( "confTool.py", " ".join(sys.argv[1:]))
122 
123  return main(args)
124 
125 knownDifferences={}
126 
127 def main(args):
128  exit_code = 0
129  if not args.quiet and args.ignoreIrrelevant:
130  print(f"Properties to ignore: {args.ignore}")
131  color = fullColor()
132  if not sys.stdout.isatty() and not args.color: #Remove colors when writing to a file unless forced
133  color = noColor()
134  if args.printComps:
135  for fileName in args.file:
136  conf = loadConfigFile(fileName, args)
137  _printComps(conf)
138 
139  if args.printConf:
140  for fileName in args.file:
141  conf = loadConfigFile(fileName, args)
142  _print(conf, color)
143 
144  if args.structured:
145  for fileName in args.file:
146  conf = loadConfigFile(fileName, args)
147  _structuredPrint(conf, args.structured, args)
148 
149 
150  if args.toJSON:
151  if len(args.file) != 1:
152  sys.exit(
153  "ERROR, can convert single file at a time, got: %s" % args.file
154  )
155  conf = loadConfigFile(args.file[0], args)
156  oFileName = args.file[0].replace(".pkl", ".json")
157  with open(oFileName, "w") as oFile:
158  json.dump(conf, oFile, indent=4, sort_keys=True, ensure_ascii=True)
159  print("Wrote " + args.file[0] + " to " + oFileName)
160 
161 
162  if args.toPickle:
163  if len(args.file) != 1:
164  sys.exit(
165  "ERROR, can convert single file at a time, got: %s" % args.file
166  )
167  conf = loadConfigFile(args.file[0], args)
168  oFileName = args.file[0].replace(".json", ".pkl")
169  with open(oFileName, "wb") as oFile:
170  for item in conf:
171  pickle.dump(item, oFile)
172  print("Wrote " + args.file[0] + " to " + oFileName)
173 
174  if args.diff:
175  if len(args.file) != 2:
176  sys.exit(
177  "ERROR, can diff exactly two files at a time, got: %s"
178  % args.file
179  )
180  # renaming only applied on the "reference/1st" file
181  configRef = loadConfigFile(args.file[0], args)
182  args.renameComps=None
183  args.renameCompsFile=None
184  configChk = loadConfigFile(args.file[1], args)
185  global knownDifferences
186  if args.knownDifferencesFile:
187  knownDifferences = loadDifferencesFile(args.knownDifferencesFile)
188  exit_code = _compareConfig(configRef, configChk, args, color) != 0
189 
190  return exit_code
191 
192 
193 def _print(conf, color):
194  for k, settings in conf.items():
195  print(f"{color.component}{k}{color.reset}")
196  if isinstance(settings, dict):
197  for prop,val in settings.items():
198  print(f" {color.property}{prop} = {color.value}{val}")
199  else:
200  print(settings)
201 
202 def _printComps(conf):
203  for k, item in conf.items():
204  if isinstance(item, dict):
205  print(k)
206 
207 def _structuredPrint(conf, start, args):
208  showClasses = args.classes or args.uniqueClasses
209  def _oneCompPrint(d, comp, cl, done={}, depth=0, indent = "") -> list:
210  show = ((not showClasses or cl is not None) and
211  (depth>0 and any([re.match(f"({regex})$",cl) for regex in args.includeClassesSub])) or
212  ((not args.includeClasses or any([re.match(f"({regex})$",cl) for regex in args.includeClasses])) and
213  not any([re.match(f"({regex})$",cl) for regex in args.excludeClasses]) and
214  not any([re.match(f"({regex})$",comp) for regex in args.excludeComponents])))
215  if show:
216  if not args.uniqueClasses:
217  if args.classes:
218  cc = cl if (not args.showComponentName) else f"{cl}/{comp}"
219  print(f"{indent}{cc}")
220  else:
221  print(f"{indent}{comp}")
222  newindent = indent + " - "
223  depth += 1
224  elif args.includeSequences and ((len(d) == 1 and "Members" in d) or (comp == "ApplicationMgr" and "TopAlg" in d)):
225  newindent = indent
226  else:
227  return []
228  if showClasses and args.maxDepth is not None and depth > args.maxDepth:
229  return []
230 
231  sub = []
232  for prop,val in sorted(d.items()):
233  if not showClasses and show:
234  print(f"{indent} {prop} = {val}")
235  if args.maxDepth is not None and depth > args.maxDepth:
236  continue
237  if not args.includeSequences and (prop == "Members" or prop == "TopAlg"):
238  continue
239  for ref,c in isReference(val, comp, conf):
240  if showClasses and not c:
241  continue
242  if ref in conf and ref not in done:
243  r = _oneCompPrint(conf[ref], ref, c or ref, {**done, ref:True}, depth, newindent)
244  if args.uniqueClasses:
245  sub += r
246  if show:
247  return [[cl, comp, sub]]
248  else:
249  return sub
250 
251  sub = []
252  for comp in start:
253  for ref,cl in isReference(start, None, conf) or [(None,None)]:
254  if ref and ref in conf:
255  settings = conf.get(ref)
256  if isinstance(settings, dict):
257  r = _oneCompPrint(settings, ref, cl or ref)
258  sub += r
259  elif not showClasses:
260  print(settings)
261  else:
262  print(f"{comp} is absent in the config, unfortunately the name has to be exact")
263 
264  if not args.uniqueClasses:
265  return
266 
267  def _nocomp(top):
268  """copy structure without the component names, so equality tests for the same class names."""
269  new = []
270  for cl,_,sub in top:
271  r = _nocomp(sub)
272  new.append([cl,r])
273  return new
274 
275  from collections import defaultdict
276  cls = defaultdict(list)
277  def _oneCompPrune(top):
278  """Duplicate classes (with identical structure below) are listed, without substructure
279  - or not listed at all if they already showed up at this level.
280  Classes with the same name are grouped next to each other and shown with their component name."""
281 
282  # group classes with the same name together
283  ord = defaultdict(list)
284  for cl,comp,sub in top:
285  ord[cl].append([cl,comp,sub])
286 
287  # remove duplicate class structures
288  new = []
289  dupcls = defaultdict(int)
290  for o in ord.values():
291  for cl,comp,sub in o:
292  comp = comp.split('.')[-1]
293  nsub = _nocomp(sub)
294  if cl in cls and nsub in cls[cl]:
295  if cl in dupcls:
296  continue
297  r = []
298  else:
299  cls[cl].append(nsub)
300  r = _oneCompPrune(sub)
301  dupcls[cl] += 1
302  new.append([cl,comp,r])
303 
304  # only show component for duplicated class names
305  if not args.showComponentName:
306  for e in new:
307  if dupcls[e[0]] < 2:
308  e[1] = None
309  return new
310 
311  def _structPrint(top, indent = ""):
312  for cl,comp,sub in top:
313  cc = f"{cl}/{comp}" if args.showComponentName else \
314  f"{cl} ({comp})" if comp is not None else cl
315  print(f"{indent}{cc}")
316  if sub:
317  _structPrint(sub, indent + " - ")
318 
319  _structPrint(_oneCompPrune(sub))
320 
321 
322 def _compareConfig(configRef, configChk, args, color):
323  # Find superset of all components:
324  allComps = list(set(configRef.keys()) | set(configChk.keys()))
325  allComps.sort()
326 
327  print("Step 1: reference file #components:", len(configRef))
328  print("Step 2: file to check #components:", len(configChk))
329  print("Legend:")
330  print(f"{color.difference}Differences in components {color.first}Settings in 1st file {color.second}Settings in 2nd file{color.reset}")
331 
332  componentReverseRenamig = {v: k for k, v in componentRenamingDict.items()} # need mapping from new name to old when renaming
333  def _componentDescription(comp_name):
334  return (comp_name+ " renamed from " + componentReverseRenamig[comp_name]) if comp_name in componentReverseRenamig else comp_name
335 
336  countDifferent = 0
337  for component in allComps:
338  if component not in configRef:
339  if not args.ignoreMissing:
340  print(
341  f"\n{color.second} Component ",
342  _componentDescription(component),
343  f"{color.reset} only in 2nd file {color.reset} \n",
344  )
345  continue
346 
347  if component not in configChk:
348  if not args.ignoreMissing:
349  print(
350  f"\n{color.first} Component",
351  _componentDescription(component),
352  f"{color.reset}only in 1st file {color.reset} \n",
353  )
354  continue
355 
356  refValue = configRef[component]
357  chkValue = configChk[component]
358 
359  if chkValue == refValue:
360  if args.printIdenticalComponents:
361  print("Component", _componentDescription(component), "identical")
362  else:
363  print(f"{color.difference}Component", _componentDescription(component), f"may differ{color.reset}")
364  if not args.allComponentPrint:
365  countDifferent = _compareComponent(refValue, chkValue, "\t", args, component, "", color)
366  if countDifferent == 0:
367  print(" but all are suppressed by renaming/known differences/...")
368  else:
369  print(f" {color.difference} {countDifferent} relevant differences{color.reset}")
370  else:
371  print(
372  f"\t{color.first}Ref{color.reset}\t",
373  sorted(configRef[component].items(), key=lambda kv: kv[0]),
374  )
375  print(
376  f"\t{color.second}Chk{color.reset}\t",
377  sorted(configChk[component].items(), key=lambda kv: kv[0]),
378  )
379  return countDifferent
380 
381 
383  """Ensure all numeric values are of the same type (int or float)"""
384  if any(isinstance(val, float) for val in values):
385  return tuple(float(val) for val in values)
386  elif all(isinstance(val, int) for val in values):
387  return tuple(int(val) for val in values)
388  else:
389  return values
390 
391 
392 def _knownDifference(comp, prop, chkVal, refVal):
393  if comp in knownDifferences:
394  if prop in knownDifferences[comp]:
395  acceptedDifference = knownDifferences[comp][prop]
396  if acceptedDifference == (None,None):
397  return True
398  if acceptedDifference[0] is None:
399  return chkVal == acceptedDifference[1]
400  if acceptedDifference[1] is None:
401  return refVal == acceptedDifference[0]
402  else:
403  return refVal == acceptedDifference[0] and chkVal == acceptedDifference[1]
404  return False
405 
407  """ Rename values in reference as long as they are hashable (and in renamingDict)
408  It is a bit of heuristics that is invoved, the assumption is that the property has value of the form A/B if it is name of the compoemnt or it is a just single string"""
409  refList = refVal if isinstance(refVal, list) else [refVal]
410  updatedRef = []
411  for v in refList:
412  if isinstance(v, str):
413  if "/" in v and len(v.split("/")) == 2:
414  compType,compName = v.split("/")
415  newName = componentRenamingDict.get(compName, compName)
416  updatedRef.append( f"{compType}/{newName}" )
417  else:
418  updatedRef.append( componentRenamingDict.get(v, v) )
419  else:
420  updatedRef.append(v)
421  return updatedRef if isinstance(refVal, list) else updatedRef[0]
422 
423 def _compareComponent(compRef, compChk, prefix, args, component, propname, color):
424  countDifferent=0
425  if isinstance(compRef, dict):
426  allProps = list(set(compRef.keys()) | set(compChk.keys()))
427  allProps.sort()
428 
429  for prop in allProps:
430  if prop not in compRef.keys():
431  if not _knownDifference(component, prop, compChk[prop], None):
432  print(f"{prefix}{color.property}{prop} = {color.second}{compChk[prop]} {color.reset} only in 2nd file {color.reset}")
433  countDifferent += 1
434  else:
435  print(f"{prefix}known difference in: {prop}")
436  continue
437 
438  if prop not in compChk.keys():
439  if not _knownDifference(component, prop, compRef[prop], None):
440  print(f"{prefix}{color.property}{prop} = {color.first}{compRef[prop]} {color.reset} only in 1st file {color.reset}")
441  countDifferent += 1
442  else:
443  print(f"{prefix}known difference in: {prop}")
444  continue
445 
446  refVal = compRef[prop]
447  chkVal = compChk[prop]
448 
449  try:
450  refVal = ast.literal_eval(str(refVal)) if refVal else ""
451  chkVal = ast.literal_eval(str(chkVal)) if chkVal else ""
452  except SyntaxError:
453  pass
454  except ValueError:
455  pass # literal_eval exception when parsing particular strings
456 
457  refVal = _handleComponentsReanaming( refVal )
458 
459  refVal, chkVal = _parseNumericalValues((refVal, chkVal))
460  diffmarker = ""
461  if chkVal == refVal:
462  if not args.printIdenticalPerParameter:
463  continue
464  elif _knownDifference(component, prop, chkVal, refVal):
465  print(f"{prefix}known difference in: {prop}")
466  if not args.printIdenticalPerParameter:
467  continue
468  else:
469  diffmarker = f" {color.difference}<<{color.reset}"
470 
471  if not (component == "IOVDbSvc" and prop == "Folders"):
472  print(f"{prefix}{color.property}{prop} = {color.first} {refVal} {color.reset} vs {color.second} {chkVal} {color.reset} {diffmarker}")
473 
474  if refVal and ( isinstance(refVal, list) or isinstance(refVal, dict) ):
475  if component == "IOVDbSvc" and prop == "Folders":
476  countDifferent += _compareIOVDbFolders(refVal, chkVal, "\t", args, color)
477  else:
478  countDifferent += _compareComponent(
479  refVal, chkVal, "\t" + prefix + ">> ", args, component, prop, color
480  )
481 
482  elif isinstance(compRef, (list, tuple, set)) and len(compRef) > 1:
483 
484  if isinstance(compRef[0], list): # to achieve hashability
485  compRef = [tuple(_parseNumericalValues(el)) for el in compRef]
486 
487  if len(compChk) > 0 and isinstance(compChk[0], list):
488  compChk = [tuple(_parseNumericalValues(el)) for el in compChk]
489 
490  # return in case results after parsing are now identical
491  if compRef == compChk:
492  return countDifferent
493 
494  diffRef = list(set(compRef) - set(compChk))
495  diffChk = list(set(compChk) - set(compRef))
496 
497  if diffRef:
498  print(f"{prefix} {color.reset}only in 1st file : {color.first} {diffRef} {color.reset}")
499  countDifferent += 1
500  if diffChk:
501  print(f"{prefix} {color.reset}only in 2nd file : {color.second} {diffChk} {color.reset}")
502  countDifferent += 1
503 
504  if len(compRef) == len(compChk):
505  if sorted(compRef) == sorted(compChk):
506  if any(re.match(f"^{regex}$",f"{component}.{propname}") for regex in args.ignoreOrder):
507  print(f"{prefix} : {color.knowndifference} ^^ Different order ignored ^^ {color.reset}")
508  else:
509  print(f"{prefix} : {color.difference} ^^ Different order ^^ {color.reset}")
510  countDifferent += 1
511  else:
512  for i, (refVal, chkVal) in enumerate(zip(compRef, compChk)):
513  if refVal != chkVal:
514  print(f"{prefix} : {color.first} {refVal} {color.reset} vs {color.second} {chkVal} {color.reset} {color.difference}<< at index {i} {color.reset}")
515  countDifferent += _compareComponent(
516  refVal, chkVal, "\t" + prefix + ">> ", args, "", "", color
517  )
518  return countDifferent
519 
520 def _parseIOVDbFolder(definition):
521  result = {}
522  # db
523  db_match = re.search(r"<db>(.*)</db>", definition)
524  if db_match:
525  result["db"] = db_match.group(1)
526  definition = definition.replace(db_match.group(0), "")
527  # key
528  key_match = re.search(r"<key>(.*)</key>", definition)
529  if key_match:
530  result["key"] = key_match.group(1)
531  definition = definition.replace(key_match.group(0), "")
532  # tag
533  tag_match = re.search(r"<tag>(.*)</tag>", definition)
534  if tag_match:
535  result["tag"] = tag_match.group(1)
536  definition = definition.replace(tag_match.group(0), "")
537  # cache -- ignore for now
538  cache_match = re.search(r"<cache>(.*)</cache>", definition)
539  if cache_match:
540  definition = definition.replace(cache_match.group(0), "")
541  # noover
542  noover_match = re.search(r"<noover/>", definition)
543  if noover_match:
544  result["noover"] = True
545  definition = definition.replace(noover_match.group(0), "")
546  # name
547  result["name"] = definition.strip()
548 
549  return json.dumps(result)
550 
551 
552 def _compareIOVDbFolders(compRef, compChk, prefix, args, color):
553  refParsed = []
554  chkParsed = []
555  for item in compRef:
556  refParsed.append(_parseIOVDbFolder(item))
557  for item in compChk:
558  chkParsed.append(_parseIOVDbFolder(item))
559  return _compareComponent(refParsed, chkParsed, prefix, args, "", "", color)
560 
561 
562 if __name__ == "__main__":
563  sys.exit(parse_args())
confTool._printComps
def _printComps(conf)
Definition: confTool.py:202
replace
std::string replace(std::string s, const std::string &s2, const std::string &s3)
Definition: hcg.cxx:307
confTool._knownDifference
def _knownDifference(comp, prop, chkVal, refVal)
Definition: confTool.py:392
confTool.noColor
Definition: confTool.py:24
CaloCellPos2Ntuple.int
int
Definition: CaloCellPos2Ntuple.py:24
confTool.main
def main(args)
Definition: confTool.py:127
Cut::all
@ all
Definition: SUSYToolsAlg.cxx:67
dumpHVPathFromNtuple.append
bool append
Definition: dumpHVPathFromNtuple.py:91
confTool._compareComponent
def _compareComponent(compRef, compChk, prefix, args, component, propname, color)
Definition: confTool.py:423
confTool._structuredPrint
def _structuredPrint(conf, start, args)
Definition: confTool.py:207
confTool.fullColor
Definition: confTool.py:14
confTool._compareConfig
def _compareConfig(configRef, configChk, args, color)
Definition: confTool.py:322
histSizes.list
def list(name, path='/')
Definition: histSizes.py:38
python.iconfTool.models.loaders.isReference
list isReference(value, compname, conf, svcCache={})
Definition: loaders.py:337
DerivationFramework::TriggerMatchingUtils::sorted
std::vector< typename T::value_type > sorted(T begin, T end)
Helper function to create a sorted vector from an unsorted one.
CxxUtils::set
constexpr std::enable_if_t< is_bitmask_v< E >, E & > set(E &lhs, E rhs)
Convenience function to set bits in a class enum bitmask.
Definition: bitmask.h:232
TCS::join
std::string join(const std::vector< std::string > &v, const char c=',')
Definition: Trigger/TrigT1/L1Topo/L1TopoCommon/Root/StringUtils.cxx:10
TrigJetMonitorAlgorithm.items
items
Definition: TrigJetMonitorAlgorithm.py:79
Trk::open
@ open
Definition: BinningType.h:40
python.iconfTool.models.loaders.loadDifferencesFile
Dict loadDifferencesFile(fname)
Definition: loaders.py:599
confTool._parseNumericalValues
def _parseNumericalValues(values)
Definition: confTool.py:382
confTool._compareIOVDbFolders
def _compareIOVDbFolders(compRef, compChk, prefix, args, color)
Definition: confTool.py:552
confTool._handleComponentsReanaming
def _handleComponentsReanaming(refVal)
Definition: confTool.py:406
confTool._print
def _print(conf, color)
Definition: confTool.py:193
confTool.parse_args
def parse_args()
Definition: confTool.py:35
confTool._parseIOVDbFolder
def _parseIOVDbFolder(definition)
Definition: confTool.py:520
str
Definition: BTagTrackIpAccessor.cxx:11
dbg::print
void print(std::FILE *stream, std::format_string< Args... > fmt, Args &&... args)
Definition: SGImplSvc.cxx:70
readCCLHist.float
float
Definition: readCCLHist.py:83
python.iconfTool.models.loaders.loadConfigFile
Dict loadConfigFile(fname, args)
Definition: loaders.py:410