3 from collections
import defaultdict
4 from copy
import copy, deepcopy
5 from difflib
import get_close_matches
6 from enum
import EnumMeta
7 from operator
import attrgetter
10 from AthenaCommon.Logging
import logging
11 from PyUtils.moduleExists
import moduleExists
12 from PyUtils.Decorators
import deprecate
14 _msg = logging.getLogger(
'AthConfigFlags')
17 """Return whether or not this is a gaudi-based (athena) environment"""
22 """The base flag object.
24 A flag can be set to either a fixed value or a callable, which computes
25 the value based on other flags.
28 __slots__ = [
'_value',
'_setDef',
'_type',
'_help']
34 def __init__(self, default, type=None, help=None):
35 """Initialise the flag with the default value.
37 Optionally set the type of the flag value and the help string.
40 raise ValueError(
"Default value of a flag must not be None")
47 """Set the value of the flag.
49 Can be a constant value or a callable.
60 def get(self, flagdict=None):
61 """Get the value of the flag.
63 If the currently set value is a callable, a dictionary of all available
64 flags needs to be provided.
67 if self.
_value is not None:
68 return deepcopy(self.
_value)
76 raise RuntimeError(
"Flag is using a callable but all flags are not available.")
89 return deepcopy(value)
92 if self.
_value is not None:
98 if (self.
_type is None or value
is None or
99 isinstance(value, self.
_type)
or
103 raise TypeError(f
"Flag is of type '{self._type.__name__}', "
104 f
"but value '{value}' of type '{type(value).__name__}' set.")
108 """Flags to dict converter
110 Used by both FlagAddress and AthConfigFlags. The input must be an
111 iterator over flags to be included in the dict.
115 for key, item
in iterator:
117 subkeys = key.split(
'.')
118 for subkey
in subkeys[:-1]:
119 x = x.setdefault(subkey,{})
120 x[subkeys[-1]] = item
125 if isinstance(flag, AthConfigFlags):
129 elif isinstance(flag, FlagAddress):
131 self.
_name = f
"{flag._name}.{name}"
134 raise TypeError(f
"Cannot create FlagAddress for object {name} of type {type(flag)}")
138 if self.
_name is None:
139 raise AttributeError(f
"Accessing category {name} has been blocked by cloneAndReplace")
143 return getattr(self.
_flags, self.
_name +
"." + name)
146 if name.startswith(
"_"):
147 return object.__setattr__(self, name, value)
148 merged = self.
_name +
"." + name
150 if merged
not in self.
_flags._flagdict:
151 self.
_flags._loadDynaFlags( merged )
153 if merged
not in self.
_flags._flagdict:
154 raise RuntimeError(
"No such flag: {} The name is likely incomplete.".
format(merged) )
155 return self.
_flags._set( merged, value )
161 raise RuntimeError(
"No such flag: "+ self.
_name+
". The name is likely incomplete." )
170 raise RuntimeError(
"No such flag: "+ self.
_name+
". The name is likely incomplete." )
173 return getattr(self, name)
176 setattr(self, name, value)
179 merged = self.
_name +
"." + name
183 return hasattr(self, name)
186 self.
_flags.loadAllDynamicFlags()
187 rmap = self.
_flags._renamed_map()
189 for flag
in self.
_flags._flagdict.keys():
190 if flag.startswith(self.
_name.rstrip(
'.') +
'.'):
191 for newflag
in rmap[flag]:
192 ntrim = len(self.
_name) + 1
193 n_dots_in = flag[:ntrim].
count(
'.')
194 remaining = newflag.split(
'.')[n_dots_in]
195 if remaining
not in used:
200 """Subflag iterator specialized for this address
203 self.
_flags.loadAllDynamicFlags()
205 rename = self.
_flags._renamed_map()
206 for key
in self.
_flags._flagdict.keys():
207 if key.startswith(address.rstrip(
'.') +
'.'):
208 ntrim = len(address) + 1
209 remaining = key[ntrim:]
210 for r
in rename[key]:
211 yield r, getattr(self, remaining)
214 """Convert to a python dictionary
216 Recursively convert this flag and all subflags into a
217 structure of nested dictionaries. All dynamic flags are
218 resolved in the process.
220 The resulting data structure should be easy to serialize as
250 raise RuntimeError(
"Cannot calculate hash of unlocked flag container")
251 elif self.
_hash is None:
256 raise DeprecationWarning(
"__hash__ method in AthConfigFlags is deprecated. Probably called from function decorator, use AccumulatorCache decorator instead.")
265 AthConfigFlags._hashedFlags.append (self)
270 _flagdict = object.__getattribute__(self,
"_flagdict")
273 if name
in _flagdict:
274 return self.
_get(name)
280 raise AttributeError(f
"No such flag: {name}")
283 if name.startswith(
"_"):
284 return object.__setattr__(self, name, value)
287 return self.
_set(name, value)
288 raise RuntimeError(
"No such flag: "+ name+
". The name is likely incomplete." )
294 return getattr(self, name)
297 setattr(self, name, value)
303 if key.startswith(name):
308 return hasattr(self, name)
316 first = r.split(
'.',1)[0]
317 if first
not in used:
322 """Convert to a python dictionary
324 This is identical to the `asdict` in FlagAddress, but for all
332 """mapping from the old names to the new names
334 This is the inverse of _renamed, which maps new names to old
337 Returns a list of the new names corresponding to the old names
338 (since cloneAndReplace may or may not disable access to the old name,
339 it is possible that an old name renames to multiple new names)
341 revmap = defaultdict(list)
348 for old, newlist
in revmap.items():
349 if key.startswith(old +
'.'):
350 stem = key.removeprefix(old)
351 return [ f
'{new}{stem}' if new
else '' for new
in newlist ]
357 """Subflag iterator for all flags
359 This is used by the asdict() function.
372 yield new, getattr(self, old)
373 except ModuleNotFoundError
as err:
374 _msg.debug(f
'missing module: {err}')
377 def addFlag(self, name, setDef, type=None, help=None):
380 raise KeyError(
"Duplicated flag name: {}".
format( name ))
386 The path is the beginning of the flag name (e.g. "X" for flags generated with name "X.*").
387 The generator is a function that returns a flags container, the flags have to start with the same path.
388 When the prefix is True the flags created by the generator are prefixed by "path".
390 Supported calls are then:
391 addFlagsCategory("A", g) - where g is function creating flags is f.addFlag("A.x", someValue)
392 addFlagsCategory("A", g, True) - when flags are defined in g like this: f.addFalg("x", somevalue),
393 The latter option allows to share one generator among flags that are later loaded in different paths.
396 _msg.debug(
"Adding flag category %s", path)
400 """ public interface for _loadDynaFlags """
405 loads the flags of the form "A.B.C" first attempting the path "A" then "A.B" and then "A.B.C"
408 def __load_impl( flagBaseName ):
409 if flagBaseName
in self.
_loaded:
410 _msg.debug(
"Flags %s already loaded",flagBaseName )
413 _msg.debug(
"Dynamically loading the flags under %s", flagBaseName )
418 generator, prefix = self.
_dynaflags[flagBaseName]
425 pathfrags = name.split(
'.')
426 for maxf
in range(1, len(pathfrags)+1):
427 __load_impl(
'.'.
join(pathfrags[:maxf]) )
430 """Force load all the dynamic flags """
437 """Check if category exists (loads dynamic flags if needed)"""
442 if (re_name := self.
_renames.
get(name))
is not None and re_name != name:
451 if f.startswith(name+
'.'):
455 if c.startswith(name):
462 """Check if flag exists (loads dynamic flags if needed)"""
467 attrgetter(name)(self)
470 except AttributeError:
478 closestMatch = get_close_matches(name,self.
_flagdict.
keys(),1)
479 raise KeyError(f
"No flag with name '{name}' found" +
480 (f
". Did you mean '{closestMatch[0]}'?" if closestMatch
else ""))
486 closestMatch = get_close_matches(name,self.
_flagdict.
keys(),1)
487 raise KeyError(f
"No flag with name '{name}' found" +
488 (f
". Did you mean '{closestMatch[0]}'?" if closestMatch
else ""))
490 @
deprecate(
"Use '[...]' rather than '(...)' to access flags", print_context=
True)
492 return self.
_get(name)
506 raise RuntimeError(
"Attempt to modify locked flag container")
512 """Return an unlocked copy of self (dynamic flags are not loaded)"""
516 cln._renames = deepcopy(self.
_renames)
522 This is to replace subsets of configuration flags like
525 newflags = flags.cloneAndReplace('Muon', 'Trigger.Offline.Muon')
528 _msg.debug(
"cloning flags and replacing %s by %s", subsetToReplace, replacementSubset)
533 subsetToReplace = subsetToReplace.strip(
".")
534 replacementSubset = replacementSubset.strip(
".")
537 if (subsetToReplace == replacementSubset):
538 raise RuntimeError(f
'Can not replace flags {subsetToReplace} with themselves')
542 if src
is None:
continue
543 if src+
"." in subsetToReplace:
544 raise RuntimeError(f
'Can not replace flags {subsetToReplace} by {replacementSubset} because of already present replacement of {alias} by {src}')
547 newFlags =
copy(self)
548 newFlags._renames = deepcopy(self.
_renames)
550 if replacementSubset
in newFlags._renames:
551 newFlags._renames[subsetToReplace] = newFlags._renames[replacementSubset]
553 newFlags._renames[subsetToReplace] = replacementSubset
556 if replacementSubset
not in newFlags._renames
or newFlags._renames[replacementSubset] == replacementSubset:
557 newFlags._renames[replacementSubset] =
None
559 del newFlags._renames[replacementSubset]
564 if replacementSubset
not in newFlags._renames:
565 newFlags._renames[replacementSubset] = replacementSubset
567 newFlags._hash =
None
571 def join(self, other, prefix=''):
573 Merges two flag containers
574 When the prefix is passed each flag from the "other" is prefixed by "prefix."
578 for (name,flag)
in other._flagdict.items():
579 fullName = prefix+
"."+name
if prefix !=
"" else name
581 raise KeyError(
"Duplicated flag name: {}".
format( fullName ) )
584 for (name,loader)
in other._dynaflags.items():
585 fullName = prefix+
"."+name
if prefix !=
"" else name
587 raise KeyError(
"Duplicated dynamic flags name: {}".
format( fullName ) )
588 _msg.debug(
"Joining dynamic flags with %s", fullName)
592 def dump(self, pattern=".*", evaluate=False, formatStr="{:40} : {}
", maxLength=None):
594 compiled = re.compile(pattern)
595 def truncate(s):
return s[:maxLength] + (
"..." if maxLength
and len(s)>maxLength
else "")
596 reverse_renames = {value: key
for key, value
in self.
_renames.
items()
if value
is not None}
599 if any([name.startswith(r)
for r
in reverse_renames.keys()
if r
is not None]):
600 for oldprefix, newprefix
in reverse_renames.items():
601 if name.startswith(oldprefix):
602 renamed = name.replace(oldprefix, newprefix)
604 if compiled.match(renamed):
613 except Exception
as e:
619 print(
"Flag categories that can be loaded dynamically")
620 print(
"{:25} : {:>30} : {}".
format(
"Category",
"Generator name",
"Defined in" ) )
622 if compiled.match(name):
623 print(
"{:25} : {:>30} : {}".
format( name, gen_and_prefix[0].__name__,
'/'.
join(gen_and_prefix[0].__code__.co_filename.split(
'/')[-2:]) ) )
625 print(
"Flag categories that are redirected by the cloneAndReplace")
627 print(
"{:30} points to {:>30} ".
format( alias, src
if src
else "nothing") )
632 Mostly a self-test method
641 Scripts calling AthConfigFlags.fillFromArgs can extend this parser, and pass their version to fillFromArgs
644 from AthenaCommon.AthOptionsParser
import getArgumentParser
646 parser.add_argument(
"---",dest=
"terminator",action=
'store_true', help=argparse.SUPPRESS)
659 """Fill the flags from a string of type key=value"""
663 key, value = flag_string.split(
"=")
665 raise ValueError(f
"Cannot interpret argument {flag_string}, expected a key=value format")
668 value = value.strip()
679 raise KeyError(f
"{key} is not a known configuration flag")
682 if flag_type
is None:
685 ast.literal_eval(value)
689 elif isinstance(flag_type, EnumMeta):
691 ENUM = importlib.import_module(flag_type.__module__)
692 value=f
"ENUM.{value}"
695 exec(f
"self.{key}{oper}{value}")
699 def fillFromArgs(self, listOfArgs=None, parser=None, return_unknown=False):
701 Used to set flags from command-line parameters, like flags.fillFromArgs(sys.argv[1:])
703 if return_unknown=False, returns: args
704 otherwise returns: args, uknown_args
705 where unknown_args is the list of arguments that did not correspond to one of the flags
714 argList = listOfArgs
if listOfArgs
is not None else sys.argv[1:]
721 unrequiredActions = []
722 if "-h" in argList
or "--help" in argList:
724 if "-h" in argList: argList.remove(
"-h")
725 if "--help" in argList: argList.remove(
"--help")
727 for a
in parser._actions:
729 unrequiredActions.append(a)
731 (args,leftover)=parser.parse_known_args(argList)
732 for a
in unrequiredActions: a.required=
True
735 argList = [a
for a
in argList
if a
not in leftover]
739 """Check if dest is available in parser and has been set"""
740 return vars(args).
get(dest,
None)
is not None
743 self.Exec.DebugStage=args.debug
745 if arg_set(
'evtMax'):
746 self.Exec.MaxEvents=args.evtMax
748 if arg_set(
'interactive'):
749 self.Exec.Interactive=args.interactive
751 if arg_set(
'skipEvents'):
752 self.Exec.SkipEvents=args.skipEvents
754 if arg_set(
'filesInput'):
755 self.Input.Files = []
756 for f
in args.filesInput.split(
","):
759 self.Input.Files += found
if found
else [f]
761 if "-l" in argList
or "--loglevel" in argList:
762 from AthenaCommon
import Constants
763 self.Exec.OutputLevel = getattr(Constants, args.loglevel)
765 if arg_set(
'config_only')
and args.config_only
is not False:
766 from os
import environ
767 environ[
"PICKLECAFILE"] =
"" if args.config_only
is True else args.config_only
769 if arg_set(
'threads'):
770 self.Concurrency.NumThreads = args.threads
774 if args.concurrent_events
is None and self.Concurrency.NumConcurrentEvents==0:
775 self.Concurrency.NumConcurrentEvents = args.threads
777 if arg_set(
'concurrent_events'):
778 self.Concurrency.NumConcurrentEvents = args.concurrent_events
780 if arg_set(
'nprocs'):
781 self.Concurrency.NumProcs = args.nprocs
783 if arg_set(
'perfmon'):
784 from PerfMonComps.PerfMonConfigHelpers
import setPerfmonFlagsFromRunArgs
788 self.Exec.MTEventService = args.mtes
790 if arg_set(
'mtes_channel'):
791 self.Exec.MTEventServiceChannel = args.mtes_channel
793 if arg_set(
'profile_python'):
794 from AthenaCommon.Debugging
import dumpPythonProfile
795 import atexit, cProfile, functools
796 cProfile._athena_python_profiler = cProfile.Profile()
797 cProfile._athena_python_profiler.enable()
800 atexit.register(functools.partial(dumpPythonProfile, args.profile_python))
803 self.Exec.MPI = args.mpi
811 if do_help
and '=' not in arg:
812 argList += arg.split(
".")
816 except KeyError
as e:
818 unknown_args += [arg]
823 if parser.epilog
is None: parser.epilog=
""
824 parser.epilog +=
" Note: Specify additional flags in form <flagName>=<value>."
825 subparsers = {
"":[parser,parser.add_subparsers(help=argparse.SUPPRESS)]}
827 logging.root.setLevel(logging.ERROR)
828 def getParser(category):
829 if category
not in subparsers.keys():
830 cat1,cat2 = category.rsplit(
".",1)
if "." in category
else (
"",category)
831 p,subp = getParser(cat1)
832 if subp.help==argparse.SUPPRESS:
833 subp.help =
"Flag subcategories:"
834 newp = subp.add_parser(cat2,help=
"{} flags".
format(category),
835 formatter_class = argparse.ArgumentDefaultsHelpFormatter,usage=argparse.SUPPRESS)
836 newp._positionals.title =
"flags"
837 subparsers[category] = [newp,newp.add_subparsers(help=argparse.SUPPRESS)]
838 return subparsers[category]
841 category,flagName = name.rsplit(
".",1)
if "." in name
else (
"",name)
844 val =
repr(flag.get(self))
847 if flag._help != argparse.SUPPRESS:
849 if flag._help
is not None:
850 helptext = f
": {flag._help}"
851 if flag._type
is not None:
852 helptext += f
' [type: {flag._type.__name__}]'
853 if val
is not None and helptext ==
"":
855 getParser(category)[0].add_argument(name, nargs=
'?', default=val, help=helptext)
857 parser._positionals.title =
'flags and positional arguments'
858 parser.parse_known_args(argList + [
"--help"])
863 return args,unknown_args