You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

501 lines
17 KiB

4 years ago
  1. # coding: utf-8
  2. #
  3. # Copyright © 2010—2014 Andrey Mikhaylenko and contributors
  4. #
  5. # This file is part of Argh.
  6. #
  7. # Argh is free software under terms of the GNU Lesser
  8. # General Public License version 3 (LGPLv3) as published by the Free
  9. # Software Foundation. See the file README.rst for copying conditions.
  10. #
  11. """
  12. Assembling
  13. ~~~~~~~~~~
  14. Functions and classes to properly assemble your commands in a parser.
  15. """
  16. import argparse
  17. import sys
  18. import warnings
  19. from argh.completion import COMPLETION_ENABLED
  20. from argh.compat import OrderedDict
  21. from argh.constants import (
  22. ATTR_ALIASES,
  23. ATTR_ARGS,
  24. ATTR_NAME,
  25. ATTR_EXPECTS_NAMESPACE_OBJECT,
  26. PARSER_FORMATTER,
  27. DEFAULT_ARGUMENT_TEMPLATE,
  28. DEST_FUNCTION,
  29. )
  30. from argh.utils import get_subparsers, get_arg_spec
  31. from argh.exceptions import AssemblingError
  32. __all__ = [
  33. 'SUPPORTS_ALIASES',
  34. 'set_default_command',
  35. 'add_commands',
  36. 'add_subcommands',
  37. ]
  38. def _check_support_aliases():
  39. p = argparse.ArgumentParser()
  40. s = p.add_subparsers()
  41. try:
  42. s.add_parser('x', aliases=[])
  43. except TypeError:
  44. return False
  45. else:
  46. return True
  47. SUPPORTS_ALIASES = _check_support_aliases()
  48. """
  49. Calculated on load. If `True`, current version of argparse supports
  50. alternative command names (can be set via :func:`~argh.decorators.aliases`).
  51. """
  52. def _get_args_from_signature(function):
  53. if getattr(function, ATTR_EXPECTS_NAMESPACE_OBJECT, False):
  54. return
  55. spec = get_arg_spec(function)
  56. defaults = dict(zip(*[reversed(x) for x in (spec.args,
  57. spec.defaults or [])]))
  58. defaults.update(getattr(spec, 'kwonlydefaults', None) or {})
  59. kwonly = getattr(spec, 'kwonlyargs', [])
  60. if sys.version_info < (3,0):
  61. annotations = {}
  62. else:
  63. annotations = dict((k,v) for k,v in function.__annotations__.items()
  64. if isinstance(v, str))
  65. # define the list of conflicting option strings
  66. # (short forms, i.e. single-character ones)
  67. chars = [a[0] for a in spec.args + kwonly]
  68. char_counts = dict((char, chars.count(char)) for char in set(chars))
  69. conflicting_opts = tuple(char for char in char_counts
  70. if 1 < char_counts[char])
  71. for name in spec.args + kwonly:
  72. flags = [] # name_or_flags
  73. akwargs = {} # keyword arguments for add_argument()
  74. if name in annotations:
  75. # help message: func(a : "b") -> add_argument("a", help="b")
  76. akwargs.update(help=annotations.get(name))
  77. if name in defaults or name in kwonly:
  78. if name in defaults:
  79. akwargs.update(default=defaults.get(name))
  80. else:
  81. akwargs.update(required=True)
  82. flags = ('-{0}'.format(name[0]), '--{0}'.format(name))
  83. if name.startswith(conflicting_opts):
  84. # remove short name
  85. flags = flags[1:]
  86. else:
  87. # positional argument
  88. flags = (name,)
  89. # cmd(foo_bar) -> add_argument('foo-bar')
  90. flags = tuple(x.replace('_', '-') for x in flags)
  91. yield dict(option_strings=flags, **akwargs)
  92. if spec.varargs:
  93. # *args
  94. yield dict(option_strings=[spec.varargs], nargs='*')
  95. def _guess(kwargs):
  96. """
  97. Adds types, actions, etc. to given argument specification.
  98. For example, ``default=3`` implies ``type=int``.
  99. :param arg: a :class:`argh.utils.Arg` instance
  100. """
  101. guessed = {}
  102. # Parser actions that accept argument 'type'
  103. TYPE_AWARE_ACTIONS = 'store', 'append'
  104. # guess type/action from default value
  105. value = kwargs.get('default')
  106. if value is not None:
  107. if isinstance(value, bool):
  108. if kwargs.get('action') is None:
  109. # infer action from default value
  110. guessed['action'] = 'store_false' if value else 'store_true'
  111. elif kwargs.get('type') is None:
  112. # infer type from default value
  113. # (make sure that action handler supports this keyword)
  114. if kwargs.get('action', 'store') in TYPE_AWARE_ACTIONS:
  115. guessed['type'] = type(value)
  116. # guess type from choices (first item)
  117. if kwargs.get('choices') and 'type' not in list(guessed) + list(kwargs):
  118. guessed['type'] = type(kwargs['choices'][0])
  119. return dict(kwargs, **guessed)
  120. def _is_positional(args, prefix_chars='-'):
  121. assert args
  122. if 1 < len(args) or args[0][0].startswith(tuple(prefix_chars)):
  123. return False
  124. else:
  125. return True
  126. def _get_parser_param_kwargs(parser, argspec):
  127. argspec = argspec.copy() # parser methods modify source data
  128. args = argspec['option_strings']
  129. if _is_positional(args, prefix_chars=parser.prefix_chars):
  130. get_kwargs = parser._get_positional_kwargs
  131. else:
  132. get_kwargs = parser._get_optional_kwargs
  133. kwargs = get_kwargs(*args, **argspec)
  134. kwargs['dest'] = kwargs['dest'].replace('-', '_')
  135. return kwargs
  136. def _get_dest(parser, argspec):
  137. kwargs = _get_parser_param_kwargs(parser, argspec)
  138. return kwargs['dest']
  139. def _require_support_for_default_command_with_subparsers():
  140. if sys.version_info < (3,4):
  141. raise AssemblingError(
  142. 'Argparse library bundled with this version of Python '
  143. 'does not support combining a default command with nested ones.')
  144. def set_default_command(parser, function):
  145. """
  146. Sets default command (i.e. a function) for given parser.
  147. If `parser.description` is empty and the function has a docstring,
  148. it is used as the description.
  149. .. note::
  150. An attempt to set default command to a parser which already has
  151. subparsers (e.g. added with :func:`~argh.assembling.add_commands`)
  152. results in a `AssemblingError`.
  153. .. note::
  154. If there are both explicitly declared arguments (e.g. via
  155. :func:`~argh.decorators.arg`) and ones inferred from the function
  156. signature (e.g. via :func:`~argh.decorators.command`), declared ones
  157. will be merged into inferred ones. If an argument does not conform
  158. function signature, `AssemblingError` is raised.
  159. .. note::
  160. If the parser was created with ``add_help=True`` (which is by default),
  161. option name ``-h`` is silently removed from any argument.
  162. """
  163. if parser._subparsers:
  164. _require_support_for_default_command_with_subparsers()
  165. spec = get_arg_spec(function)
  166. declared_args = getattr(function, ATTR_ARGS, [])
  167. inferred_args = list(_get_args_from_signature(function))
  168. if inferred_args and declared_args:
  169. # We've got a mixture of declared and inferred arguments
  170. # a mapping of "dest" strings to argument declarations.
  171. #
  172. # * a "dest" string is a normalized form of argument name, i.e.:
  173. #
  174. # '-f', '--foo' → 'foo'
  175. # 'foo-bar' → 'foo_bar'
  176. #
  177. # * argument declaration is a dictionary representing an argument;
  178. # it is obtained either from _get_args_from_signature() or from
  179. # an @arg decorator (as is).
  180. #
  181. dests = OrderedDict()
  182. for argspec in inferred_args:
  183. dest = _get_parser_param_kwargs(parser, argspec)['dest']
  184. dests[dest] = argspec
  185. for declared_kw in declared_args:
  186. # an argument is declared via decorator
  187. dest = _get_dest(parser, declared_kw)
  188. if dest in dests:
  189. # the argument is already known from function signature
  190. #
  191. # now make sure that this declared arg conforms to the function
  192. # signature and therefore only refines an inferred arg:
  193. #
  194. # @arg('my-foo') maps to func(my_foo)
  195. # @arg('--my-bar') maps to func(my_bar=...)
  196. # either both arguments are positional or both are optional
  197. decl_positional = _is_positional(declared_kw['option_strings'])
  198. infr_positional = _is_positional(dests[dest]['option_strings'])
  199. if decl_positional != infr_positional:
  200. kinds = {True: 'positional', False: 'optional'}
  201. raise AssemblingError(
  202. '{func}: argument "{dest}" declared as {kind_i} '
  203. '(in function signature) and {kind_d} (via decorator)'
  204. .format(
  205. func=function.__name__,
  206. dest=dest,
  207. kind_i=kinds[infr_positional],
  208. kind_d=kinds[decl_positional],
  209. ))
  210. # merge explicit argument declaration into the inferred one
  211. # (e.g. `help=...`)
  212. dests[dest].update(**declared_kw)
  213. else:
  214. # the argument is not in function signature
  215. varkw = getattr(spec, 'varkw', getattr(spec, 'keywords', []))
  216. if varkw:
  217. # function accepts **kwargs; the argument goes into it
  218. dests[dest] = declared_kw
  219. else:
  220. # there's no way we can map the argument declaration
  221. # to function signature
  222. xs = (dests[x]['option_strings'] for x in dests)
  223. raise AssemblingError(
  224. '{func}: argument {flags} does not fit '
  225. 'function signature: {sig}'.format(
  226. flags=', '.join(declared_kw['option_strings']),
  227. func=function.__name__,
  228. sig=', '.join('/'.join(x) for x in xs)))
  229. # pack the modified data back into a list
  230. inferred_args = dests.values()
  231. command_args = inferred_args or declared_args
  232. # add types, actions, etc. (e.g. default=3 implies type=int)
  233. command_args = [_guess(x) for x in command_args]
  234. for draft in command_args:
  235. draft = draft.copy()
  236. if 'help' not in draft:
  237. draft.update(help=DEFAULT_ARGUMENT_TEMPLATE)
  238. dest_or_opt_strings = draft.pop('option_strings')
  239. if parser.add_help and '-h' in dest_or_opt_strings:
  240. dest_or_opt_strings = [x for x in dest_or_opt_strings if x != '-h']
  241. completer = draft.pop('completer', None)
  242. try:
  243. action = parser.add_argument(*dest_or_opt_strings, **draft)
  244. if COMPLETION_ENABLED and completer:
  245. action.completer = completer
  246. except Exception as e:
  247. raise type(e)('{func}: cannot add arg {args}: {msg}'.format(
  248. args='/'.join(dest_or_opt_strings), func=function.__name__, msg=e))
  249. if function.__doc__ and not parser.description:
  250. parser.description = function.__doc__
  251. parser.set_defaults(**{
  252. DEST_FUNCTION: function,
  253. })
  254. def add_commands(parser, functions, namespace=None, namespace_kwargs=None,
  255. func_kwargs=None,
  256. # deprecated args:
  257. title=None, description=None, help=None):
  258. """
  259. Adds given functions as commands to given parser.
  260. :param parser:
  261. an :class:`argparse.ArgumentParser` instance.
  262. :param functions:
  263. a list of functions. A subparser is created for each of them.
  264. If the function is decorated with :func:`~argh.decorators.arg`, the
  265. arguments are passed to :class:`argparse.ArgumentParser.add_argument`.
  266. See also :func:`~argh.dispatching.dispatch` for requirements
  267. concerning function signatures. The command name is inferred from the
  268. function name. Note that the underscores in the name are replaced with
  269. hyphens, i.e. function name "foo_bar" becomes command name "foo-bar".
  270. :param namespace:
  271. an optional string representing the group of commands. For example, if
  272. a command named "hello" is added without the namespace, it will be
  273. available as "prog.py hello"; if the namespace if specified as "greet",
  274. then the command will be accessible as "prog.py greet hello". The
  275. namespace itself is not callable, so "prog.py greet" will fail and only
  276. display a help message.
  277. :param func_kwargs:
  278. a `dict` of keyword arguments to be passed to each nested ArgumentParser
  279. instance created per command (i.e. per function). Members of this
  280. dictionary have the highest priority, so a function's docstring is
  281. overridden by a `help` in `func_kwargs` (if present).
  282. :param namespace_kwargs:
  283. a `dict` of keyword arguments to be passed to the nested ArgumentParser
  284. instance under given `namespace`.
  285. Deprecated params that should be moved into `namespace_kwargs`:
  286. :param title:
  287. passed to :meth:`argparse.ArgumentParser.add_subparsers` as `title`.
  288. .. deprecated:: 0.26.0
  289. Please use `namespace_kwargs` instead.
  290. :param description:
  291. passed to :meth:`argparse.ArgumentParser.add_subparsers` as
  292. `description`.
  293. .. deprecated:: 0.26.0
  294. Please use `namespace_kwargs` instead.
  295. :param help:
  296. passed to :meth:`argparse.ArgumentParser.add_subparsers` as `help`.
  297. .. deprecated:: 0.26.0
  298. Please use `namespace_kwargs` instead.
  299. .. note::
  300. This function modifies the parser object. Generally side effects are
  301. bad practice but we don't seem to have any choice as ArgumentParser is
  302. pretty opaque.
  303. You may prefer :class:`~argh.helpers.ArghParser.add_commands` for a bit
  304. more predictable API.
  305. .. note::
  306. An attempt to add commands to a parser which already has a default
  307. function (e.g. added with :func:`~argh.assembling.set_default_command`)
  308. results in `AssemblingError`.
  309. """
  310. # FIXME "namespace" is a correct name but it clashes with the "namespace"
  311. # that represents arguments (argparse.Namespace and our ArghNamespace).
  312. # We should rename the argument here.
  313. if DEST_FUNCTION in parser._defaults:
  314. _require_support_for_default_command_with_subparsers()
  315. namespace_kwargs = namespace_kwargs or {}
  316. # FIXME remove this by 1.0
  317. #
  318. if title:
  319. warnings.warn('argument `title` is deprecated in add_commands(),'
  320. ' use `parser_kwargs` instead', DeprecationWarning)
  321. namespace_kwargs['description'] = title
  322. if help:
  323. warnings.warn('argument `help` is deprecated in add_commands(),'
  324. ' use `parser_kwargs` instead', DeprecationWarning)
  325. namespace_kwargs['help'] = help
  326. if description:
  327. warnings.warn('argument `description` is deprecated in add_commands(),'
  328. ' use `parser_kwargs` instead', DeprecationWarning)
  329. namespace_kwargs['description'] = description
  330. #
  331. # /
  332. subparsers_action = get_subparsers(parser, create=True)
  333. if namespace:
  334. # Make a nested parser and init a deeper _SubParsersAction under it.
  335. # Create a named group of commands. It will be listed along with
  336. # root-level commands in ``app.py --help``; in that context its `title`
  337. # can be used as a short description on the right side of its name.
  338. # Normally `title` is shown above the list of commands
  339. # in ``app.py my-namespace --help``.
  340. subsubparser_kw = {
  341. 'help': namespace_kwargs.get('title'),
  342. }
  343. subsubparser = subparsers_action.add_parser(namespace, **subsubparser_kw)
  344. subparsers_action = subsubparser.add_subparsers(**namespace_kwargs)
  345. else:
  346. assert not namespace_kwargs, ('`parser_kwargs` only makes sense '
  347. 'with `namespace`.')
  348. for func in functions:
  349. cmd_name, func_parser_kwargs = _extract_command_meta_from_func(func)
  350. # override any computed kwargs by manually supplied ones
  351. if func_kwargs:
  352. func_parser_kwargs.update(func_kwargs)
  353. # create and set up the parser for this command
  354. command_parser = subparsers_action.add_parser(cmd_name, **func_parser_kwargs)
  355. set_default_command(command_parser, func)
  356. def _extract_command_meta_from_func(func):
  357. # use explicitly defined name; if none, use function name (a_b → a-b)
  358. cmd_name = getattr(func, ATTR_NAME,
  359. func.__name__.replace('_','-'))
  360. func_parser_kwargs = {
  361. # add command help from function's docstring
  362. 'help': func.__doc__,
  363. # set default formatter
  364. 'formatter_class': PARSER_FORMATTER,
  365. }
  366. # try adding aliases for command name
  367. if SUPPORTS_ALIASES:
  368. func_parser_kwargs['aliases'] = getattr(func, ATTR_ALIASES, [])
  369. return cmd_name, func_parser_kwargs
  370. def add_subcommands(parser, namespace, functions, **namespace_kwargs):
  371. """
  372. A wrapper for :func:`add_commands`.
  373. These examples are equivalent::
  374. add_commands(parser, [get, put], namespace='db',
  375. namespace_kwargs={
  376. 'title': 'database commands',
  377. 'help': 'CRUD for our silly database'
  378. })
  379. add_subcommands(parser, 'db', [get, put],
  380. title='database commands',
  381. help='CRUD for our silly database')
  382. """
  383. add_commands(parser, functions, namespace=namespace,
  384. namespace_kwargs=namespace_kwargs)