Completed
Push — master ( dbc38f...56accc )
by Klaus
01:34
created

_format_options_usage()   B

Complexity

Conditions 3

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
dl 0
loc 31
rs 8.8571
c 1
b 0
f 0
1
#!/usr/bin/env python
2
# coding=utf-8
3
"""
4
This module contains the command-line parsing and help for experiments.
5
6
The command-line interface of sacred is built on top of ``docopt``, which
7
constructs a command-line parser from a usage text. Curiously in sacred we
8
first programmatically generate a usage text and then parse it with ``docopt``.
9
"""
10
from __future__ import division, print_function, unicode_literals
11
import ast
12
from collections import OrderedDict
13
import textwrap
14
import sys
15
import inspect
16
from docopt import docopt
17
18
from sacred.commandline_options import gather_command_line_options
19
from sacred.commands import help_for_command
20
from sacred.serializer import restore
21
from sacred.settings import SETTINGS
22
from sacred.utils import set_by_dotted_path
23
24
__sacred__ = True  # marks files that should be filtered from stack traces
25
26
__all__ = ('parse_args', 'get_config_updates')
27
28
29
USAGE_TEMPLATE = """Usage:
30
  {program_name} [(with UPDATE...)] [options]
31
  {program_name} help [COMMAND]
32
  {program_name} (-h | --help)
33
  {program_name} COMMAND [(with UPDATE...)] [options]
34
35
{description}
36
37
Options:
38
{options}
39
40
Arguments:
41
  COMMAND   Name of command to run (see below for list of commands)
42
  UPDATE    Configuration assignments of the form foo.bar=17
43
{arguments}
44
{commands}"""
45
46
47
def parse_args(argv, description="", commands=None, print_help=True):
48
    """
49
    Parse the given commandline-arguments.
50
51
    Parameters
52
    ----------
53
        argv: list[str]
54
            list of command-line arguments as in ``sys.argv``
55
        description: str:
56
            description of the experiment (docstring) to be used in the help
57
            text.
58
        commands: Optional[dict[str, func]]
59
            list of commands that are supported by this experiment
60
        print_help: bool
61
            if True (default) this function will print the help-text and exit
62
            if that is required by the parsed arguments.
63
64
    Returns
65
    -------
66
        dict[str, (str | bool | None)]
67
            parsed values for all command-line options.
68
            See ``docopt`` for more details.
69
    """
70
    options = gather_command_line_options()
71
    usage = _format_usage(argv[0], description, commands, options)
72
    args = docopt(usage, [str(a) for a in argv[1:]], help=print_help)
73
    if not args['help'] or not print_help:
74
        return args
75
76
    if args['COMMAND'] is None:
77
        print(usage)
78
        sys.exit()
79
    else:
80
        print(help_for_command(commands[args['COMMAND']]))
81
        sys.exit()
82
83
84
def get_config_updates(updates):
85
    """
86
    Parse the UPDATES given on the commandline.
87
88
    Parameters
89
    ----------
90
        updates (list[str]):
91
            list of update-strings of the form NAME=LITERAL or just NAME.
92
93
    Returns
94
    -------
95
        (dict, list):
96
            Config updates and named configs to use
97
    """
98
    config_updates = {}
99
    named_configs = []
100
    if not updates:
101
        return config_updates, named_configs
102
    for upd in updates:
103
        if upd == '':
104
            continue
105
        path, sep, value = upd.partition('=')
106
        if sep == '=':
107
            path = path.strip()    # get rid of surrounding whitespace
108
            value = value.strip()  # get rid of surrounding whitespace
109
            set_by_dotted_path(config_updates, path, _convert_value(value))
110
        else:
111
            named_configs.append(path)
112
    return config_updates, named_configs
113
114
115
def _format_options_usage(options):
116
    """
117
    Format the Options-part of the usage text.
118
119
    Parameters
120
    ----------
121
        options : list[sacred.commandline_options.CommandLineOption]
122
            A list of all supported commandline options.
123
124
    Returns
125
    -------
126
        str
127
            Text formatted as a description for the commandline options
128
    """
129
    options_usage = ""
130
    for op in options:
131
        short, long = op.get_flags()
132
        if op.arg:
133
            flag = "{short} {arg} {long}={arg}".format(
134
                short=short, long=long, arg=op.arg)
135
        else:
136
            flag = "{short} {long}".format(short=short, long=long)
137
138
        wrapped_description = textwrap.wrap(inspect.cleandoc(op.__doc__),
139
                                            width=79,
140
                                            initial_indent=' ' * 32,
141
                                            subsequent_indent=' ' * 32)
142
        wrapped_description = "\n".join(wrapped_description).strip()
143
144
        options_usage += "  {0:28}  {1}\n".format(flag, wrapped_description)
145
    return options_usage
146
147
148
def _format_arguments_usage(options):
149
    """
150
    Construct the Arguments-part of the usage text.
151
152
    Parameters
153
    ----------
154
        options : list[sacred.commandline_options.CommandLineOption]
155
            A list of all supported commandline options.
156
157
    Returns
158
    -------
159
        str
160
            Text formatted as a description of the arguments supported by the
161
            commandline options.
162
    """
163
    argument_usage = ""
164
    for op in options:
165
        if op.arg and op.arg_description:
166
            wrapped_description = textwrap.wrap(op.arg_description,
167
                                                width=79,
168
                                                initial_indent=' ' * 12,
169
                                                subsequent_indent=' ' * 12)
170
            wrapped_description = "\n".join(wrapped_description).strip()
171
            argument_usage += "  {0:8}  {1}\n".format(op.arg,
172
                                                      wrapped_description)
173
    return argument_usage
174
175
176
def _format_command_usage(commands):
177
    """
178
    Construct the Commands-part of the usage text.
179
180
    Parameters
181
    ----------
182
        commands : list[str, str]
183
            List of supported commands.
184
            Each entry should be a tuple of (name, description).
185
186
    Returns
187
    -------
188
        str
189
            Text formatted as a description of the commands.
190
    """
191
    if not commands:
192
        return ""
193
    command_usage = "\nCommands:\n"
194
    cmd_len = max([len(c) for c in commands] + [8])
195
    command_doc = OrderedDict(
196
        [(cmd_name, _get_first_line_of_docstring(cmd_doc))
197
         for cmd_name, cmd_doc in commands.items()])
198
    for cmd_name, cmd_doc in command_doc.items():
199
        command_usage += ("  {:%d}  {}\n" % cmd_len).format(cmd_name, cmd_doc)
200
    return command_usage
201
202
203
def _format_usage(program_name, description, commands=None, options=()):
204
    """
205
    Construct the usage text.
206
207
    Parameters
208
    ----------
209
        program_name : str
210
            Usually the name of the python file that contains the experiment.
211
        description : str
212
            description of this experiment (usually the docstring).
213
        commands : list[str, str]
214
            List of supported commands.
215
            Each entry should be a tuple of (name, description).
216
        options : list[sacred.commandline_options.CommandLineOption]
217
            A list of all supported commandline options.
218
219
    Returns
220
    -------
221
        str
222
            The complete formatted usage text for this experiment.
223
            It adheres to the structure required by ``docopt``.
224
    """
225
    usage = USAGE_TEMPLATE.format(
226
        program_name=program_name,
227
        description=description.strip() if description else '',
228
        options=_format_options_usage(options),
229
        arguments=_format_arguments_usage(options),
230
        commands=_format_command_usage(commands)
231
    )
232
    return usage
233
234
235
def _get_first_line_of_docstring(func):
236
    return textwrap.dedent(func.__doc__ or "").strip().split('\n')[0]
237
238
239
def _convert_value(value):
240
    """Parse string as python literal if possible and fallback to string."""
241
    try:
242
        return restore(ast.literal_eval(value))
243
    except (ValueError, SyntaxError):
244
        if SETTINGS.COMMAND_LINE.STRICT_PARSING:
245
            raise
246
        # use as string if nothing else worked
247
        return value
248