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