File: verb_pattern.py

package info (click to toggle)
ros-osrf-pycommon 2.1.7-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 360 kB
  • sloc: python: 1,726; makefile: 146; xml: 16
file content (213 lines) | stat: -rw-r--r-- 7,963 bytes parent folder | download | duplicates (2)
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