Hide keyboard shortcuts

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 

2 

3""" 

4CLI tools for Flags. 

5""" 

6 

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 

13 

14 

15FLAGOPTS = ["{} for {}".format(v, k) 

16 for k, v in FlagDict.ALLOWED_VALUES.items()] 

17FLAGLISTCLI = ", ".join(FLAGOPTS[:-1]) + ", and " + FLAGOPTS[-1] 

18 

19 

20class PrintFlagsAction(Action): # pylint: disable=too-few-public-methods 

21 """Action to show flag help and quit.""" 

22 

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() 

34 

35 

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) 

44 

45 

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.') 

75 

76 

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 """ 

85 

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}") 

115 

116 

117class Parsers: 

118 """ 

119 CLI flags related to flags. Same idea as ``llama.cli.Parsers`` but with 

120 narrower scope. 

121 """ 

122 

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)) 

141 

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.")