Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/face/command.py: 39%

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

181 statements  

1import sys 

2from collections import OrderedDict 

3from typing import Callable, List, Optional, Union 

4 

5from face.utils import unwrap_text, get_rdep_map, echo 

6from face.errors import ArgumentParseError, CommandLineError, UsageError 

7from face.parser import Parser, Flag, PosArgSpec 

8from face.helpers import HelpHandler 

9from face.middleware import (inject, 

10 get_arg_names, 

11 is_middleware, 

12 face_middleware, 

13 check_middleware, 

14 get_middleware_chain, 

15 _BUILTIN_PROVIDES) 

16 

17from boltons.strutils import camel2under 

18from boltons.iterutils import unique 

19 

20 

21def _get_default_name(func): 

22 from functools import partial 

23 if isinstance(func, partial): 

24 func = func.func # just one level of partial for now 

25 

26 ret = getattr(func, '__name__', None) # most functions hit this 

27 

28 if ret is None: 

29 ret = camel2under(func.__class__.__name__).lower() # callable instances, etc. 

30 

31 return ret 

32 

33 

34def _docstring_to_doc(func): 

35 doc = func.__doc__ 

36 if not doc: 

37 return '' 

38 

39 unwrapped = unwrap_text(doc) 

40 try: 

41 ret = [g for g in unwrapped.splitlines() if g][0] 

42 except IndexError: 

43 ret = '' 

44 

45 return ret 

46 

47 

48def default_print_error(msg): 

49 return echo.err(msg) 

50 

51 

52DEFAULT_HELP_HANDLER = HelpHandler() 

53 

54 

55# TODO: should name really go here? 

56class Command(Parser): 

57 """The central type in the face framework. Instantiate a Command, 

58 populate it with flags and subcommands, and then call 

59 command.run() to execute your CLI. 

60 

61 Args: 

62 func: The function called when this command is 

63 run with an argv that contains no subcommands. 

64 name: The name of this command, used when this 

65 command is included as a subcommand. (Defaults to name 

66 of function) 

67 doc: A description or message that appears in various 

68 help outputs. 

69 flags: A list of Flag instances to initialize the 

70 Command with. Flags can always be added later with the 

71 .add() method. 

72 posargs: Pass True if the command takes positional 

73 arguments. Defaults to False. Can also pass a PosArgSpec 

74 instance. 

75 post_posargs: Pass True if the command takes 

76 additional positional arguments after a conventional '--' 

77 specifier. 

78 help: Pass False to disable the automatically added 

79 --help flag. Defaults to True. Also accepts a HelpHandler 

80 instance. 

81 middlewares: A list of @face_middleware decorated 

82 callables which participate in dispatch. 

83 """ 

84 def __init__(self, 

85 func: Optional[Callable], 

86 name: Optional[str] = None, 

87 doc: Optional[str] = None, 

88 *, 

89 flags: Optional[List[Flag]] = None, 

90 posargs: Optional[Union[bool, PosArgSpec]] = None, 

91 post_posargs: Optional[bool] = None, 

92 flagfile: bool = True, 

93 help: Union[bool, HelpHandler] = DEFAULT_HELP_HANDLER, 

94 middlewares: Optional[List[Callable]] = None) -> None: 

95 name = name if name is not None else _get_default_name(func) 

96 if doc is None: 

97 doc = _docstring_to_doc(func) 

98 

99 # TODO: default posargs if none by inspecting func 

100 super().__init__(name, doc, 

101 flags=flags, 

102 posargs=posargs, 

103 post_posargs=post_posargs, 

104 flagfile=flagfile) 

105 

106 self.help_handler = help 

107 

108 # TODO: if func is callable, check that "next_" isn't taken 

109 self._path_func_map = OrderedDict() 

110 self._path_func_map[()] = func 

111 

112 middlewares = list(middlewares or []) 

113 self._path_mw_map = OrderedDict() 

114 self._path_mw_map[()] = [] 

115 self._path_wrapped_map = OrderedDict() 

116 self._path_wrapped_map[()] = func 

117 for mw in middlewares: 

118 self.add_middleware(mw) 

119 

120 if help: 

121 if help is True: 

122 help = DEFAULT_HELP_HANDLER 

123 if help.flag: 

124 self.add(help.flag) 

125 if help.subcmd: 

126 self.add(help.func, help.subcmd) # for 'help' as a subcmd 

127 

128 if not func and not help: 

129 raise ValueError('Command requires a handler function or help handler' 

130 ' to be set, not: %r' % func) 

131 

132 return 

133 

134 @property 

135 def func(self): 

136 return self._path_func_map[()] 

137 

138 def add(self, *a, **kw): 

139 """Add a flag, subcommand, or middleware to this Command. 

140 

141 If the first argument is a callable, this method contructs a 

142 Command from it and the remaining arguments, all of which are 

143 optional. See the Command docs for for full details on names 

144 and defaults. 

145 

146 If the first argument is a string, this method constructs a 

147 Flag from that flag string and the rest of the method 

148 arguments, all of which are optional. See the Flag docs for 

149 more options. 

150 

151 If the argument is already an instance of Flag or Command, an 

152 exception is only raised on conflicting subcommands and 

153 flags. See add_command for details. 

154 

155 Middleware is only added if it is already decorated with 

156 @face_middleware. Use .add_middleware() for automatic wrapping 

157 of callables. 

158 

159 """ 

160 # TODO: need to check for middleware provides names + flag names 

161 # conflict 

162 

163 target = a[0] 

164 

165 if is_middleware(target): 

166 return self.add_middleware(target) 

167 

168 subcmd = a[0] 

169 if not isinstance(subcmd, Command) and callable(subcmd) or subcmd is None: 

170 subcmd = Command(*a, **kw) # attempt to construct a new subcmd 

171 

172 if isinstance(subcmd, Command): 

173 self.add_command(subcmd) 

174 return subcmd 

175 

176 flag = a[0] 

177 if not isinstance(flag, Flag): 

178 flag = Flag(*a, **kw) # attempt to construct a Flag from arguments 

179 super().add(flag) 

180 

181 return flag 

182 

183 def add_command(self, subcmd): 

184 """Add a Command, and all of its subcommands, as a subcommand of this 

185 Command. 

186 

187 Middleware from the current command is layered on top of the 

188 subcommand's. An exception may be raised if there are 

189 conflicting middlewares or subcommand names. 

190 """ 

191 if not isinstance(subcmd, Command): 

192 raise TypeError(f'expected Command instance, not: {subcmd!r}') 

193 self_mw = self._path_mw_map[()] 

194 super().add(subcmd) 

195 # map in new functions 

196 for path in self.subprs_map: 

197 if path not in self._path_func_map: 

198 self._path_func_map[path] = subcmd._path_func_map[path[1:]] 

199 sub_mw = subcmd._path_mw_map[path[1:]] 

200 self._path_mw_map[path] = self_mw + sub_mw # TODO: check for conflicts 

201 return 

202 

203 def add_middleware(self, mw): 

204 """Add a single middleware to this command. Outermost middleware 

205 should be added first. Remember: first added, first called. 

206 

207 """ 

208 if not is_middleware(mw): 

209 mw = face_middleware(mw) 

210 check_middleware(mw) 

211 

212 for flag in mw._face_flags: 

213 self.add(flag) 

214 

215 for path, mws in self._path_mw_map.items(): 

216 self._path_mw_map[path] = [mw] + mws # TODO: check for conflicts 

217 

218 return 

219 

220 # TODO: add_flag() 

221 

222 def get_flag_map(self, path=(), with_hidden=True): 

223 """Command's get_flag_map differs from Parser's in that it filters 

224 the flag map to just the flags used by the endpoint at the 

225 associated subcommand *path*. 

226 """ 

227 flag_map = super().get_flag_map(path=path, with_hidden=with_hidden) 

228 dep_names = self.get_dep_names(path) 

229 if 'args_' in dep_names or 'flags_' in dep_names: 

230 # the argument parse result and flag dict both capture 

231 # _all_ the flags, so for functions accepting these 

232 # arguments we bypass filtering. 

233 

234 # Also note that by setting an argument default in the 

235 # function definition, the dependency becomes "weak", and 

236 # this bypassing of filtering will not trigger, unless 

237 # another function in the chain has a non-default, 

238 # "strong" dependency. This behavior is especially useful 

239 # for middleware. 

240 

241 # TODO: add decorator for the corner case where a function 

242 # accepts these arguments and doesn't use them all. 

243 return OrderedDict(flag_map) 

244 

245 return OrderedDict([(k, f) for k, f in flag_map.items() if f.name in dep_names 

246 or f is self.flagfile_flag or f is self.help_handler.flag]) 

247 

248 def get_dep_names(self, path=()): 

249 """Get a list of the names of all required arguments of a command (and 

250 any associated middleware). 

251 

252 By specifying *path*, the same can be done for any subcommand. 

253 """ 

254 func = self._path_func_map[path] 

255 if not func: 

256 return [] # for when no handler is specified 

257 

258 mws = self._path_mw_map[path] 

259 

260 # start out with all args of handler function, which gets stronger dependencies 

261 required_args = set(get_arg_names(func, only_required=False)) 

262 dep_map = {func: set(required_args)} 

263 for mw in mws: 

264 arg_names = set(get_arg_names(mw, only_required=True)) 

265 for provide in mw._face_provides: 

266 dep_map[provide] = arg_names 

267 if not mw._face_optional: 

268 # all non-optional middlewares get their args required, too. 

269 required_args.update(arg_names) 

270 

271 rdep_map = get_rdep_map(dep_map) 

272 

273 recursive_required_args = rdep_map[func].union(required_args) 

274 

275 return sorted(recursive_required_args) 

276 

277 def prepare(self, paths=None): 

278 """Compile and validate one or more subcommands to ensure all 

279 dependencies are met. Call this once all flags, subcommands, 

280 and middlewares have been added (using .add()). 

281 

282 This method is automatically called by .run() method, but it 

283 only does so for the specific subcommand being invoked. More 

284 conscientious users may want to call this method with no 

285 arguments to validate that all subcommands are ready for 

286 execution. 

287 """ 

288 # TODO: also pre-execute help formatting to make sure all 

289 # values are sane there, too 

290 if paths is None: 

291 paths = self._path_func_map.keys() 

292 

293 for path in paths: 

294 func = self._path_func_map[path] 

295 if func is None: 

296 continue # handled by run() 

297 

298 prs = self.subprs_map[path] if path else self 

299 provides = [] 

300 if prs.posargs.provides: 

301 provides += [prs.posargs.provides] 

302 if prs.post_posargs.provides: 

303 provides += [prs.post_posargs.provides] 

304 

305 deps = self.get_dep_names(path) 

306 flag_names = [f.name for f in self.get_flags(path=path)] 

307 all_mws = self._path_mw_map[path] 

308 

309 # filter out unused middlewares 

310 mws = [mw for mw in all_mws if not mw._face_optional 

311 or [p for p in mw._face_provides if p in deps]] 

312 provides += _BUILTIN_PROVIDES + flag_names 

313 try: 

314 wrapped = get_middleware_chain(mws, func, provides) 

315 except NameError as ne: 

316 ne.args = (ne.args[0] + f' (in path: {path!r})',) 

317 raise 

318 

319 self._path_wrapped_map[path] = wrapped 

320 

321 return 

322 

323 def run(self, argv=None, extras=None, print_error=None): 

324 """Parses arguments and dispatches to the appropriate subcommand 

325 handler. If there is a parse error due to invalid user input, 

326 an error is printed and a CommandLineError is raised. If not 

327 caught, a CommandLineError will exit the process, typically 

328 with status code 1. Also handles dispatching to the 

329 appropriate HelpHandler, if configured. 

330 

331 Defaults to handling the arguments on the command line 

332 (``sys.argv``), but can also be explicitly passed arguments 

333 via the *argv* parameter. 

334 

335 Args: 

336 argv (list): A sequence of strings representing the 

337 command-line arguments. Defaults to ``sys.argv``. 

338 extras (dict): A map of additional arguments to be made 

339 available to the subcommand's handler function. 

340 print_error (callable): The function that formats/prints 

341 error messages before program exit on CLI errors. 

342 

343 .. note:: 

344 

345 For efficiency, :meth:`run()` only checks the subcommand 

346 invoked by *argv*. To ensure that all subcommands are 

347 configured properly, call :meth:`prepare()`. 

348 

349 """ 

350 if print_error is None or print_error is True: 

351 print_error = default_print_error 

352 elif print_error and not callable(print_error): 

353 raise TypeError(f'expected callable for print_error, not {print_error!r}') 

354 

355 kwargs = dict(extras) if extras else {} 

356 kwargs['print_error_'] = print_error # TODO: print_error_ in builtin provides? 

357 

358 try: 

359 prs_res = self.parse(argv=argv) 

360 except ArgumentParseError as ape: 

361 prs_res = ape.prs_res 

362 

363 # even if parsing failed, check if the caller was trying to access the help flag 

364 cmd = prs_res.to_cmd_scope()['subcommand_'] 

365 if cmd.help_handler and prs_res.flags and prs_res.flags.get(cmd.help_handler.flag.name): 

366 kwargs.update(prs_res.to_cmd_scope()) 

367 return inject(cmd.help_handler.func, kwargs) 

368 

369 msg = 'error: ' + (prs_res.name or self.name) 

370 if prs_res.subcmds: 

371 msg += ' ' + ' '.join(prs_res.subcmds or ()) 

372 

373 # args attribute, nothing to do with cmdline args this is 

374 # the standard-issue Exception 

375 e_msg = ape.args[0] 

376 if e_msg: 

377 msg += ': ' + e_msg 

378 cle = CommandLineError(msg) 

379 if print_error: 

380 print_error(msg) 

381 raise cle 

382 

383 kwargs.update(prs_res.to_cmd_scope()) 

384 

385 # default in case no middlewares have been installed 

386 func = self._path_func_map[prs_res.subcmds] 

387 

388 cmd = kwargs['subcommand_'] 

389 if cmd.help_handler and (not func or (prs_res.flags and prs_res.flags.get(cmd.help_handler.flag.name))): 

390 return inject(cmd.help_handler.func, kwargs) 

391 elif not func: # pragma: no cover 

392 raise RuntimeError('expected command handler or help handler to be set') 

393 

394 self.prepare(paths=[prs_res.subcmds]) 

395 wrapped = self._path_wrapped_map.get(prs_res.subcmds, func) 

396 

397 try: 

398 ret = inject(wrapped, kwargs) 

399 except UsageError as ue: 

400 if print_error: 

401 print_error(ue.format_message()) 

402 raise 

403 return ret