Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# (c) Stefan Countryman 2019
3"""
4CLI tools for Flags.
5"""
7import sys
8from textwrap import wrap, dedent
9from argparse import Action, Namespace
10from llama.cli import parse_atom, CliParser
11from llama.classes import ImmutableDict
12from llama.flags import FLAG_PRESETS, ALLOWED_VALUES, FlagDict
15FLAGOPTS = ["{} for {}".format(v, k)
16 for k, v in FlagDict.ALLOWED_VALUES.items()]
17FLAGLISTCLI = ", ".join(FLAGOPTS[:-1]) + ", and " + FLAGOPTS[-1]
20class PrintFlagsAction(Action): # pylint: disable=too-few-public-methods
21 """Action to show flag help and quit."""
23 def __call__(self, parser, namespace, values, option_string=None):
24 """Print available flag values and presets and quit."""
25 print(f"Acceptable flag options: {FLAGOPTS}")
26 print()
27 print("Flag presets available:")
28 for preset in FLAG_PRESETS._fields:
29 print(f"\n{preset}:")
30 preset_dict = getattr(FLAG_PRESETS, preset)
31 for key in sorted(preset_dict):
32 print(f" {key:<24} = {preset_dict[key]:<24}")
33 exit()
36def postprocess_flags_dry_run(_self: CliParser, namespace: Namespace):
37 """
38 Print the flags selected by ``namespace.flags`` and exit without taking
39 further action if ``namespace.dry_run_flags``.
40 """
41 if namespace.dry_run_flags:
42 print(f"FLAGS SELECTED: {namespace.flags}")
43 exit(0)
46def postprocess_flags(self: CliParser, namespace: Namespace):
47 """Check whether flags have been specified under ``--flags``. If not,
48 and if this is an interactive session, prompt the user to manually input
49 flags. If not specified and not interactive, raise an error."""
50 if namespace.flags is None:
51 if not sys.stdout.isatty():
52 self.error("Must provide argument `--flags` when running "
53 "non-interactively.")
54 print("\n".join(wrap(dedent(f"""
55 You haven't specified any `--flags`. Is this a real,
56 observational event? If you say YES below, this event will
57 be able to automatically upload results and alert
58 colaborators ("TRIGGERED_PUBLIC" flag preset). If you say
59 NO, this will be treated like a test event ("TRIGGERED_TEST
60 flag preset). ANSWER CAREFULLY; this is important!
61 """.strip()))))
62 flags = None
63 while flags is None:
64 is_real = input("Is this event a real observation? "
65 "[yes/no] ")
66 if is_real.lower() == "yes":
67 setattr(namespace, self.dest,
68 FLAG_PRESETS.TRIGGERED_PUBLIC)
69 return
70 if is_real.lower() == "no":
71 setattr(namespace, self.dest,
72 FLAG_PRESETS.TRIGGERED_TEST)
73 return
74 print('YOU MUST ANSWER "YES" OR "NO"; ctrl-C to exit.')
77class FlagAction(Action): # pylint: disable=too-few-public-methods
78 """
79 Parses flags provided at the command line. Sets the returned argument to
80 an ``ImmutableDict`` containing the desired flags (either specified by
81 preset or explicit deviations from defaults) after validating that the
82 provided values are valid and quitting with a helpful message if they are
83 not.
84 """
86 def __call__(self, parser, namespace, values, option_string=None):
87 """Set ``namespace.{self.dest}`` to an ``ImmutableDict`` containing the
88 specified flags. Will prompt the user for input or raise error rather
89 than accepting ``None``."""
90 # if only one flag can be specified it won't be in a list
91 if not isinstance(values, list):
92 values = [values]
93 if all('=' in f for f in values):
94 flags = dict()
95 input_flags = dict(f.split("=", 1) for f in values)
96 if len(set(input_flags)) != len(input_flags):
97 parser.error("Do not specify the same flag value more than "
98 f"once. {option_string} given: {values}")
99 for key, value in input_flags.items():
100 if value not in ALLOWED_VALUES[key]:
101 parser.error(f"{option_string} value '{value}' not "
102 f"allowed for '{key}'. Acceptable values: "
103 f"{ALLOWED_VALUES[key]}")
104 flags[key] = value
105 setattr(namespace, self.dest, ImmutableDict(flags))
106 return
107 if len(values) == 1:
108 try:
109 setattr(namespace, self.dest, getattr(FLAG_PRESETS, values[0]))
110 return
111 except AttributeError:
112 parser.error("Unrecognized preset. Pick from: "
113 f"{FLAG_PRESETS._fields}")
114 parser.error(f"Cannot parse {option_string}: {values}")
117class Parsers:
118 """
119 CLI flags related to flags. Same idea as ``llama.cli.Parsers`` but with
120 narrower scope.
121 """
123 flags = parse_atom("--flags", metavar="FLAGNAME=value", nargs="*",
124 postprocessors=(postprocess_flags_dry_run,
125 postprocess_flags),
126 action=FlagAction,
127 help="""
128 A single flag preset (see: ``llama.flags``) to use (print choices with
129 ``--flag-presets``) in ``--outdir`` OR individual flag settings in the
130 format ``FLAGNAME=value``. YOU SHOULD PROBABLY USE A PRESET rather than
131 individual flag settings. Any omitted flags will take on the default
132 values in ``DEFAULT_FLAGS``. If you don't specify ``--flags``, you'll
133 be prompted to provide one; just provide
134 ``--flags`` with no arguments to accept the default/existing flags.
135 Flags are used to set overall behaviors for an event and set
136 intentions, e.g. to mark an event as "ONLINE" and therefore allowed to
137 communicate with partner web APIs and send out products and alerts.
138 Flag name options and default values (for new events) are {}; the full
139 set of allowed values is {}.""".format(FlagDict.DEFAULT_FLAGS,
140 FLAGLISTCLI))
142 flags.add_argument("--flag-presets", action=PrintFlagsAction, nargs=0,
143 help="Print available flag presets.")
144 flags.add_argument("--dry-run-flags", action='store_true',
145 help="Print flags parsed by the CLI and exit.")