Coverage for sacred/sacred/commandline_options.py: 59%

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

123 statements  

1""" 

2Provides the basis for all command-line options (flags) in sacred. 

3 

4It defines the base class CommandLineOption and the standard supported flags. 

5Some further options that add observers to the run are defined alongside those. 

6""" 

7 

8from typing import Callable 

9import inspect 

10import re 

11 

12from sacred.run import Run 

13from sacred.commands import print_config 

14from sacred.utils import convert_camel_case_to_snake_case 

15 

16 

17CLIFunction = Callable[[str, Run], None] 

18 

19 

20class CLIOption: 

21 def __init__( 

22 self, 

23 apply_function: CLIFunction, 

24 short_flag: str, 

25 long_flag: str, 

26 is_flag: bool, 

27 ): 

28 

29 if not re.match(r"-\w$", short_flag): 

30 raise ValueError( 

31 "Short flag malformed. " "One correct short flag would be: `-j`." 

32 ) 

33 if not re.match(r"--[\w-]+\w$", long_flag): 

34 raise ValueError( 

35 "Long flag malformed. One correct long flag " 

36 "would be: `--my-pretty-flag`" 

37 ) 

38 self.apply_function = apply_function 

39 self.short_flag = short_flag 

40 self.long_flag = long_flag 

41 self.is_flag = is_flag 

42 

43 # trick for backward compatibility: 

44 self.arg = None if is_flag else "VALUE" 

45 self.arg_description = None if is_flag else "" 

46 

47 def __call__(self, *args, **kwargs): 

48 return self.apply_function(*args, **kwargs) 

49 

50 def get_flag(self): 

51 """Legacy function. Should be removed at some point.""" 

52 return self.long_flag 

53 

54 def get_short_flag(self): 

55 """Legacy function. Should be removed at some point.""" 

56 return self.short_flag 

57 

58 def get_flags(self): 

59 """Legacy function. Should be removed at some point.""" 

60 return self.short_flag, self.long_flag 

61 

62 def apply(self, args, run): 

63 """Legacy function. Should be removed at some point.""" 

64 return self.apply_function(args, run) 

65 

66 def get_name(self): 

67 return self.apply_function.__name__ 

68 

69 def get_description(self): 

70 return inspect.getdoc(self.apply_function) or "" 

71 

72 

73def cli_option(short_flag: str, long_flag: str, is_flag=False): 

74 def wrapper(f: CLIFunction): 

75 return CLIOption(f, short_flag, long_flag, is_flag) 

76 

77 return wrapper 

78 

79 

80class CommandLineOption: 

81 """ 

82 Base class for all command-line options. 

83 

84 To implement a new command-line option just inherit from this class. 

85 Then add the `flag` class-attribute to specify the name and a class 

86 docstring with the description. 

87 If your command-line option should take an argument you must also provide 

88 its name via the `arg` class attribute and its description as 

89 `arg_description`. 

90 Finally you need to implement the `execute` classmethod. It receives the 

91 value of the argument (if applicable) and the current run. You can modify 

92 the run object in any way. 

93 

94 If the command line option depends on one or more installed packages, those 

95 should be imported in the `apply` method to get a proper ImportError 

96 if the packages are not available. 

97 """ 

98 

99 _enabled = True 

100 

101 short_flag = None 

102 """ The (one-letter) short form (defaults to first letter of flag) """ 

103 

104 arg = None 

105 """ Name of the argument (optional) """ 

106 

107 arg_description = None 

108 """ Description of the argument (optional) """ 

109 

110 @classmethod 

111 def get_flag(cls): 

112 # Get the flag name from the class name 

113 flag = cls.__name__ 

114 if flag.endswith("Option"): 

115 flag = flag[:-6] 

116 return "--" + convert_camel_case_to_snake_case(flag) 

117 

118 @classmethod 

119 def get_short_flag(cls): 

120 if cls.short_flag is None: 

121 return "-" + cls.get_flag()[2] 

122 else: 

123 return "-" + cls.short_flag 

124 

125 @classmethod 

126 def get_flags(cls): 

127 """ 

128 Return the short and the long version of this option. 

129 

130 The long flag (e.g. '--foo_bar'; used on the command-line like this: 

131 --foo_bar[=ARGS]) is derived from the class-name by stripping away any 

132 -Option suffix and converting the rest to snake_case. 

133 

134 The short flag (e.g. '-f'; used on the command-line like this: 

135 -f [ARGS]) the short_flag class-member if that is set, or the first 

136 letter of the long flag otherwise. 

137 

138 Returns 

139 ------- 

140 (str, str) 

141 tuple of short-flag, and long-flag 

142 

143 """ 

144 return cls.get_short_flag(), cls.get_flag() 

145 

146 @classmethod 

147 def apply(cls, args, run): 

148 """ 

149 Modify the current Run base on this command-line option. 

150 

151 This function is executed after constructing the Run object, but 

152 before actually starting it. 

153 

154 Parameters 

155 ---------- 

156 args : bool | str 

157 If this command-line option accepts an argument this will be value 

158 of that argument if set or None. 

159 Otherwise it is either True or False. 

160 run : sacred.run.Run 

161 The current run to be modified 

162 

163 """ 

164 pass 

165 

166 

167def get_name(option): 

168 if isinstance(option, CLIOption): 

169 return option.get_name() 

170 else: 

171 return option.__name__ 

172 

173 

174@cli_option("-h", "--help", is_flag=True) 

175def help_option(args, run): 

176 """Print this help message and exit.""" 

177 pass 

178 

179 

180@cli_option("-d", "--debug", is_flag=True) 

181def debug_option(args, run): 

182 """ 

183 Set this run to debug mode. 

184 

185 Suppress warnings about missing observers and don't filter the stacktrace. 

186 Also enables usage with ipython `--pdb`. 

187 """ 

188 run.debug = True 

189 

190 

191@cli_option("-D", "--pdb", is_flag=True) 

192def pdb_option(args, run): 

193 """Automatically enter post-mortem debugging with pdb on failure.""" 

194 run.pdb = True 

195 

196 

197@cli_option("-l", "--loglevel") 

198def loglevel_option(args, run): 

199 """ 

200 Set the LogLevel. 

201 

202 Loglevel either as 0 - 50 or as string: DEBUG(10), 

203 INFO(20), WARNING(30), ERROR(40), CRITICAL(50) 

204 """ 

205 # TODO: sacred.initialize.create_run already takes care of this 

206 

207 try: 

208 lvl = int(args) 

209 except ValueError: 

210 lvl = args 

211 run.root_logger.setLevel(lvl) 

212 

213 

214@cli_option("-c", "--comment") 

215def comment_option(args, run): 

216 """Add a comment to this run.""" 

217 run.meta_info["comment"] = args 

218 

219 

220@cli_option("-b", "--beat-interval") 

221def beat_interval_option(args, run): 

222 """ 

223 Set the heart-beat interval for this run. 

224 

225 Time between two heartbeat events is measured in seconds. 

226 """ 

227 run.beat_interval = float(args) 

228 

229 

230@cli_option("-u", "--unobserved", is_flag=True) 

231def unobserved_option(args, run): 

232 """Ignore all observers for this run.""" 

233 run.unobserved = True 

234 

235 

236@cli_option("-q", "--queue", is_flag=True) 

237def queue_option(args, run): 

238 """Only queue this run, do not start it.""" 

239 run.queue_only = True 

240 

241 

242@cli_option("-f", "--force", is_flag=True) 

243def force_option(args, run): 

244 """Disable warnings about suspicious changes for this run.""" 

245 run.force = True 

246 

247 

248@cli_option("-P", "--priority") 

249def priority_option(args, run): 

250 """Sets the priority for a queued up experiment. 

251 

252 `--priority=NUMBER` 

253 The number represent the priority for this run. 

254 """ 

255 try: 

256 priority = float(args) 

257 except ValueError: 

258 raise ValueError( 

259 "The PRIORITY argument must be a number! (but was '{}')".format(args) 

260 ) 

261 run.meta_info["priority"] = priority 

262 

263 

264@cli_option("-e", "--enforce_clean", is_flag=True) 

265def enforce_clean_option(args, run): 

266 """Fail if any version control repository is dirty.""" 

267 repos = run.experiment_info["repositories"] 

268 if not repos: 

269 raise RuntimeError( 

270 "No version control detected. " 

271 "Cannot enforce clean repository.\n" 

272 "Make sure that your sources under VCS and the " 

273 "corresponding python package is installed." 

274 ) 

275 else: 

276 for repo in repos: 

277 if repo["dirty"]: 

278 raise RuntimeError( 

279 "EnforceClean: Uncommited changes in " 

280 'the "{}" repository.'.format(repo) 

281 ) 

282 

283 

284@cli_option("-p", "--print-config", is_flag=True) 

285def print_config_option(args, run): 

286 """Always print the configuration first.""" 

287 print_config(run) 

288 print("-" * 79) 

289 

290 

291@cli_option("-i", "--id") 

292def id_option(args, run): 

293 """Set the id for this run.""" 

294 run._id = args 

295 

296 

297@cli_option("-n", "--name") 

298def name_option(args, run): 

299 """Set the name for this run.""" 

300 run.experiment_info["name"] = args 

301 run.run_logger = run.root_logger.getChild(args) 

302 

303 

304@cli_option("-C", "--capture") 

305def capture_option(args, run): 

306 """ 

307 Control the way stdout and stderr are captured. 

308 

309 The argument value must be one of [no, sys, fd] 

310 """ 

311 run.capture_mode = args