1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
|
# Copyright 2014 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""API for implementing commands and verbs which used the verb pattern."""
import sys
import inspect
try:
import importlib.metadata as importlib_metadata
except ModuleNotFoundError:
import importlib_metadata
def call_prepare_arguments(func, parser, sysargs=None):
"""Call a prepare_arguments function with the correct number of parameters.
The ``prepare_arguments`` function of a verb can either take one parameter,
``parser``, or two parameters ``parser`` and ``args``, where ``args`` are
the current arguments being processed.
:param func: Callable ``prepare_arguments`` function.
:type func: Callable
:param parser: parser which is always passed to the function
:type parser: :py:class:`argparse.ArgumentParser`
:param sysargs: arguments to optionally pass to the function, if needed
:type sysargs: list
:returns: return value of function or the parser if the function
returns None.
:rtype: :py:class:`argparse.ArgumentParser`
:raises: ValueError if a function with the wrong number of parameters
is given
"""
func_args = [parser]
# If the provided function takes two arguments and args were given
# also give the args to the function
# Remove the following if condition and keep else condition once Xenial is
# dropped
if sys.version_info[0] < 3:
arguments, _, _, defaults = inspect.getargspec(func)
else:
arguments, _, _, defaults, _, _, _ = inspect.getfullargspec(func)
if arguments[0] == 'self':
del arguments[0]
if defaults:
arguments = arguments[:-len(defaults)]
if len(arguments) not in [1, 2]:
# Remove the following if condition once Xenial is dropped
if sys.version_info[0] < 3:
raise ValueError("Given function '{0}' must have one or two "
"parameters (excluding self), but got '{1}' "
"parameters: '{2}'"
.format(func.__name__,
len(arguments),
', '.join(inspect.getargspec(func)[0])))
raise ValueError("Given function '{0}' must have one or two "
"parameters (excluding self), but got '{1}' "
"parameters: '{2}'"
.format(func.__name__,
len(arguments),
', '.join(inspect.getfullargspec(func)[0])))
if len(arguments) == 2:
func_args.append(sysargs or [])
return func(*func_args) or parser
def create_subparsers(parser, cmd_name, verbs, group, sysargs, title=None):
"""Creates argparse subparsers for each verb which can be discovered.
Using the ``verbs`` parameter, the available verbs are iterated through.
For each verb a subparser is created for it using the ``parser`` parameter.
The ``cmd_name`` is used to fill the title and description of the
``add_subparsers`` function call.
The ``group`` parameter is used with each verb to load the verb's
``description``, ``prepare_arguments`` function, and the verb's
``argument_preprocessors`` if available.
Each verb's ``prepare_arguments`` function is called, allowing them to
add arguments.
Finally a list of ``argument_preprocessors`` functions and verb subparsers
are returned, one for each verb.
:param parser: parser for this command
:type parser: :py:class:`argparse.ArgumentParser`
:param str cmd_name: name of the command to which the verbs are being added
:param list verbs: list of verbs (by name as a string)
:param str group: name of the ``entry_point`` group for the verbs
:param list sysargs: list of system arguments
:param str title: optional custom title for the command
:returns: tuple of argument_preprocessors and verb subparsers
:rtype: tuple
"""
metavar = '[' + ' | '.join(verbs) + ']'
subparser = parser.add_subparsers(
title=title or '{0} command'.format(cmd_name),
metavar=metavar,
description='Call `{0} {1} -h` for help on each verb.'.format(
cmd_name, metavar),
dest='verb'
)
argument_preprocessors = {}
verb_subparsers = {}
for verb in verbs:
desc = load_verb_description(verb, group)
cmd_parser = subparser.add_parser(
desc['verb'], description=desc['description'])
cmd_parser = call_prepare_arguments(
desc['prepare_arguments'],
cmd_parser,
sysargs,
)
cmd_parser.set_defaults(main=desc['main'])
if 'argument_preprocessor' in desc:
argument_preprocessors[verb] = desc['argument_preprocessor']
else:
argument_preprocessors[verb] = default_argument_preprocessor
verb_subparsers[verb] = cmd_parser
return argument_preprocessors, verb_subparsers
def default_argument_preprocessor(args):
"""Return unmodified args and an empty dict for extras"""
extras = {}
return args, extras
def list_verbs(group):
"""List verbs available for a given ``entry_point`` group.
:param str group: ``entry_point`` group name for the verbs to list
:returns: list of verb names for the given ``entry_point`` group
:rtype: list of str
"""
verbs = []
entry_points = importlib_metadata.entry_points()
if hasattr(entry_points, 'select'):
groups = entry_points.select(group=group)
else:
groups = entry_points.get(group, [])
for entry_point in groups:
verbs.append(entry_point.name)
return verbs
def load_verb_description(verb_name, group):
"""Load description of a verb in a given group by name.
:param str verb_name: name of the verb to load, as a string
:param str group: ``entry_point`` group name which the verb is in
:returns: verb description
:rtype: dict
"""
entry_points = importlib_metadata.entry_points()
if hasattr(entry_points, 'select'):
groups = entry_points.select(group=group)
else:
groups = entry_points.get(group, [])
for entry_point in groups:
if entry_point.name == verb_name:
return entry_point.load()
def split_arguments_by_verb(arguments):
"""Split arguments by verb.
Given a list of arguments (list of strings), the verb, the pre verb
arguments, and the post verb arguments are returned.
For example:
.. code-block:: python
>>> args = ['--command-arg1', 'verb', '--verb-arg1', '--verb-arg2']
>>> split_arguments_by_verb(args)
('verb', ['--command-arg1'], ['--verb-arg1', '--verb-arg2'])
:param list arguments: list of system arguments
:returns: the verb (str), pre verb args (list), and post verb args (list)
:rtype: tuple
"""
verb = None
pre_verb_args = []
post_verb_args = []
for index, arg in enumerate(arguments):
# If the arg does not start with a `-` then it is a positional argument
# The first positional argument must be the verb
if not arg.startswith('-'):
verb = arg
post_verb_args = arguments[index + 1:]
break
# Otherwise it is a pre-verb option
pre_verb_args.append(arg)
return verb, pre_verb_args, post_verb_args
|