Completed
Push — master ( c73972...b998ba )
by Klaus
32s
created

format_usage()   B

Complexity

Conditions 2

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
c 0
b 0
f 0
dl 0
loc 31
rs 8.8571
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
    """
71
    options = gather_command_line_options()
72
    usage = format_usage(argv[0], description, commands, options)
73
    args = docopt(usage, [str(a) for a in argv[1:]], help=print_help)
74
    if not args['help'] or not print_help:
75
        return args
76
77
    if args['COMMAND'] is None:
78
        print(usage)
79
        sys.exit()
80
    else:
81
        print(help_for_command(commands[args['COMMAND']]))
82
        sys.exit()
83
84
85
def get_config_updates(updates):
86
    """
87
    Parse the UPDATES given on the commandline.
88
89
    Parameters
90
    ----------
91
        updates (list[str]):
92
            list of update-strings of the form NAME=LITERAL or just NAME.
93
94
    Returns
95
    -------
96
        (dict, list):
97
            Config updates and named configs to use
98
99
    """
100
    config_updates = {}
101
    named_configs = []
102
    if not updates:
103
        return config_updates, named_configs
104
    for upd in updates:
105
        if upd == '':
106
            continue
107
        path, sep, value = upd.partition('=')
108
        if sep == '=':
109
            path = path.strip()    # get rid of surrounding whitespace
110
            value = value.strip()  # get rid of surrounding whitespace
111
            set_by_dotted_path(config_updates, path, _convert_value(value))
112
        else:
113
            named_configs.append(path)
114
    return config_updates, named_configs
115
116
117
def _format_options_usage(options):
118
    """
119
    Format the Options-part of the usage text.
120
121
    Parameters
122
    ----------
123
        options : list[sacred.commandline_options.CommandLineOption]
124
            A list of all supported commandline options.
125
126
    Returns
127
    -------
128
        str
129
            Text formatted as a description for the commandline options
130
131
    """
132
    options_usage = ""
133
    for op in options:
134
        short, long = op.get_flags()
135
        if op.arg:
136
            flag = "{short} {arg} {long}={arg}".format(
137
                short=short, long=long, arg=op.arg)
138
        else:
139
            flag = "{short} {long}".format(short=short, long=long)
140
141
        wrapped_description = textwrap.wrap(inspect.cleandoc(op.__doc__),
142
                                            width=79,
143
                                            initial_indent=' ' * 32,
144
                                            subsequent_indent=' ' * 32)
145
        wrapped_description = "\n".join(wrapped_description).strip()
146
147
        options_usage += "  {0:28}  {1}\n".format(flag, wrapped_description)
148
    return options_usage
149
150
151
def _format_arguments_usage(options):
152
    """
153
    Construct the Arguments-part of the usage text.
154
155
    Parameters
156
    ----------
157
        options : list[sacred.commandline_options.CommandLineOption]
158
            A list of all supported commandline options.
159
160
    Returns
161
    -------
162
        str
163
            Text formatted as a description of the arguments supported by the
164
            commandline options.
165
166
    """
167
    argument_usage = ""
168
    for op in options:
169
        if op.arg and op.arg_description:
170
            wrapped_description = textwrap.wrap(op.arg_description,
171
                                                width=79,
172
                                                initial_indent=' ' * 12,
173
                                                subsequent_indent=' ' * 12)
174
            wrapped_description = "\n".join(wrapped_description).strip()
175
            argument_usage += "  {0:8}  {1}\n".format(op.arg,
176
                                                      wrapped_description)
177
    return argument_usage
178
179
180
def _format_command_usage(commands):
181
    """
182
    Construct the Commands-part of the usage text.
183
184
    Parameters
185
    ----------
186
        commands : dict[str, func]
187
            dictionary of supported commands.
188
            Each entry should be a tuple of (name, function).
189
190
    Returns
191
    -------
192
        str
193
            Text formatted as a description of the commands.
194
195
    """
196
    if not commands:
197
        return ""
198
    command_usage = "\nCommands:\n"
199
    cmd_len = max([len(c) for c in commands] + [8])
200
    command_doc = OrderedDict(
201
        [(cmd_name, _get_first_line_of_docstring(cmd_doc))
202
         for cmd_name, cmd_doc in commands.items()])
203
    for cmd_name, cmd_doc in command_doc.items():
204
        command_usage += ("  {:%d}  {}\n" % cmd_len).format(cmd_name, cmd_doc)
205
    return command_usage
206
207
208
def format_usage(program_name, description, commands=None, options=()):
209
    """
210
    Construct the usage text.
211
212
    Parameters
213
    ----------
214
        program_name : str
215
            Usually the name of the python file that contains the experiment.
216
        description : str
217
            description of this experiment (usually the docstring).
218
        commands : dict[str, func]
219
            Dictionary of supported commands.
220
            Each entry should be a tuple of (name, function).
221
        options : list[sacred.commandline_options.CommandLineOption]
222
            A list of all supported commandline options.
223
224
    Returns
225
    -------
226
        str
227
            The complete formatted usage text for this experiment.
228
            It adheres to the structure required by ``docopt``.
229
230
    """
231
    usage = USAGE_TEMPLATE.format(
232
        program_name=program_name,
233
        description=description.strip() if description else '',
234
        options=_format_options_usage(options),
235
        arguments=_format_arguments_usage(options),
236
        commands=_format_command_usage(commands)
237
    )
238
    return usage
239
240
241
def _get_first_line_of_docstring(func):
242
    return textwrap.dedent(func.__doc__ or "").strip().split('\n')[0]
243
244
245
def _convert_value(value):
246
    """Parse string as python literal if possible and fallback to string."""
247
    try:
248
        return restore(ast.literal_eval(value))
249
    except (ValueError, SyntaxError):
250
        if SETTINGS.COMMAND_LINE.STRICT_PARSING:
251
            raise
252
        # use as string if nothing else worked
253
        return value
254