Coverage for sacred/sacred/arg_parser.py: 90%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

68 statements  

1""" 

2Contains the command-line parsing and help for experiments. 

3 

4The command-line interface of sacred is built on top of ``docopt``, which 

5constructs a command-line parser from a usage text. Curiously in sacred we 

6first programmatically generate a usage text and then parse it with ``docopt``. 

7""" 

8 

9import ast 

10import textwrap 

11import inspect 

12from shlex import quote 

13 

14from sacred.serializer import restore 

15from sacred.settings import SETTINGS 

16from sacred.utils import set_by_dotted_path 

17from sacred.commandline_options import CLIOption 

18 

19 

20__all__ = ("get_config_updates", "format_usage") 

21 

22 

23USAGE_TEMPLATE = """Usage: 

24 {program_name} [(with UPDATE...)] [options] 

25 {program_name} help [COMMAND] 

26 {program_name} (-h | --help) 

27 {program_name} COMMAND [(with UPDATE...)] [options] 

28 

29{description} 

30 

31Options: 

32{options} 

33 

34Arguments: 

35 COMMAND Name of command to run (see below for list of commands) 

36 UPDATE Configuration assignments of the form foo.bar=17 

37{arguments} 

38{commands}""" 

39 

40 

41def get_config_updates(updates): 

42 """ 

43 Parse the UPDATES given on the commandline. 

44 

45 Parameters 

46 ---------- 

47 updates (list[str]): 

48 list of update-strings of the form NAME=LITERAL or just NAME. 

49 

50 Returns 

51 ------- 

52 (dict, list): 

53 Config updates and named configs to use 

54 

55 """ 

56 config_updates = {} 

57 named_configs = [] 

58 if not updates: 

59 return config_updates, named_configs 

60 for upd in updates: 

61 if upd == "": 

62 continue 

63 path, sep, value = upd.partition("=") 

64 if sep == "=": 

65 path = path.strip() # get rid of surrounding whitespace 

66 value = value.strip() # get rid of surrounding whitespace 

67 set_by_dotted_path(config_updates, path, _convert_value(value)) 

68 else: 

69 named_configs.append(path) 

70 return config_updates, named_configs 

71 

72 

73def _format_options_usage(options): 

74 """ 

75 Format the Options-part of the usage text. 

76 

77 Parameters 

78 ---------- 

79 options : list[sacred.commandline_options.CommandLineOption] 

80 A list of all supported commandline options. 

81 

82 Returns 

83 ------- 

84 str 

85 Text formatted as a description for the commandline options 

86 

87 """ 

88 options_usage = "" 

89 for op in options: 

90 short, long = op.get_flags() 

91 if op.arg: 

92 flag = "{short} {arg} {long}={arg}".format( 

93 short=short, long=long, arg=op.arg 

94 ) 

95 else: 

96 flag = "{short} {long}".format(short=short, long=long) 

97 

98 if isinstance(op, CLIOption): 

99 doc = op.get_description() 

100 else: 

101 # legacy 

102 doc = inspect.cleandoc(op.__doc__) 

103 wrapped_description = textwrap.wrap( 

104 doc, width=79, initial_indent=" " * 32, subsequent_indent=" " * 32 

105 ) 

106 wrapped_description = "\n".join(wrapped_description).strip() 

107 

108 options_usage += " {:28} {}\n".format(flag, wrapped_description) 

109 return options_usage 

110 

111 

112def _format_arguments_usage(options): 

113 """ 

114 Construct the Arguments-part of the usage text. 

115 

116 Parameters 

117 ---------- 

118 options : list[sacred.commandline_options.CommandLineOption] 

119 A list of all supported commandline options. 

120 

121 Returns 

122 ------- 

123 str 

124 Text formatted as a description of the arguments supported by the 

125 commandline options. 

126 

127 """ 

128 argument_usage = "" 

129 for op in options: 

130 if op.arg and op.arg_description: 

131 wrapped_description = textwrap.wrap( 

132 op.arg_description, 

133 width=79, 

134 initial_indent=" " * 12, 

135 subsequent_indent=" " * 12, 

136 ) 

137 wrapped_description = "\n".join(wrapped_description).strip() 

138 argument_usage += " {:8} {}\n".format(op.arg, wrapped_description) 

139 return argument_usage 

140 

141 

142def _format_command_usage(commands): 

143 """ 

144 Construct the Commands-part of the usage text. 

145 

146 Parameters 

147 ---------- 

148 commands : dict[str, func] 

149 dictionary of supported commands. 

150 Each entry should be a tuple of (name, function). 

151 

152 Returns 

153 ------- 

154 str 

155 Text formatted as a description of the commands. 

156 

157 """ 

158 if not commands: 

159 return "" 

160 command_usage = "\nCommands:\n" 

161 cmd_len = max([len(c) for c in commands] + [8]) 

162 

163 for cmd_name, cmd_doc in commands.items(): 

164 cmd_doc = _get_first_line_of_docstring(cmd_doc) 

165 command_usage += (" {:%d} {}\n" % cmd_len).format(cmd_name, cmd_doc) 

166 return command_usage 

167 

168 

169def format_usage(program_name, description, commands=None, options=()): 

170 """ 

171 Construct the usage text. 

172 

173 Parameters 

174 ---------- 

175 program_name : str 

176 Usually the name of the python file that contains the experiment. 

177 description : str 

178 description of this experiment (usually the docstring). 

179 commands : dict[str, func] 

180 Dictionary of supported commands. 

181 Each entry should be a tuple of (name, function). 

182 options : list[sacred.commandline_options.CommandLineOption] 

183 A list of all supported commandline options. 

184 

185 Returns 

186 ------- 

187 str 

188 The complete formatted usage text for this experiment. 

189 It adheres to the structure required by ``docopt``. 

190 

191 """ 

192 usage = USAGE_TEMPLATE.format( 

193 program_name=quote(program_name), 

194 description=description.strip() if description else "", 

195 options=_format_options_usage(options), 

196 arguments=_format_arguments_usage(options), 

197 commands=_format_command_usage(commands), 

198 ) 

199 return usage 

200 

201 

202def _get_first_line_of_docstring(func): 

203 return textwrap.dedent(func.__doc__ or "").strip().split("\n")[0] 

204 

205 

206def _convert_value(value): 

207 """Parse string as python literal if possible and fallback to string.""" 

208 try: 

209 return restore(ast.literal_eval(value)) 

210 except (ValueError, SyntaxError): 

211 if SETTINGS.COMMAND_LINE.STRICT_PARSING: 

212 raise 

213 # use as string if nothing else worked 

214 return value