CommentOption.apply()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
b 0
f 0
dl 0
loc 4
rs 10
1
#!/usr/bin/env python
2
# coding=utf-8
3
"""
4
This module provides the basis for all command-line options (flags) in sacred.
5
6
It defines the base class CommandLineOption and the standard supported flags.
7
Some further options that add observers to the run are defined alongside those.
8
"""
9
10
from __future__ import division, print_function, unicode_literals
11
from sacred.commands import print_config
12
from sacred.settings import SETTINGS
13
from sacred.utils import (convert_camel_case_to_snake_case, get_inheritors,
14
                          module_exists)
15
16
17
# from six.with_metaclass
18
def with_metaclass(meta, *bases):
19
    """Create a base class with a metaclass."""
20
    # This requires a bit of explanation: the basic idea is to make a dummy
21
    # metaclass for one level of class instantiation that replaces itself with
22
    # the actual metaclass.
23
    class Metaclass(meta):
24
        def __new__(cls, name, this_bases, d):
25
            return meta(name, bases, d)
26
    return type.__new__(Metaclass, str('temporary_class'), (), {})
27
28
29
def parse_mod_deps(depends_on):
30
    if not isinstance(depends_on, (list, tuple)):
31
        depends_on = [depends_on]
32
    module_names = []
33
    package_names = []
34
    for d in depends_on:
35
        mod, _, pkg = d.partition('#')
36
        module_names.append(mod)
37
        package_names.append(pkg or mod)
38
    return module_names, package_names
39
40
41
class CheckDependencies(type):
42
    """Modifies the CommandLineOption if a specified dependency is not met."""
43
    def __init__(cls, what, bases=None, dict_=None):  # noqa
44
        if '__depends_on__' in dict_:
45
            mod_names, package_names = parse_mod_deps(dict_['__depends_on__'])
46
            mods_exist = [module_exists(m) for m in mod_names]
47
48
            if not all(mods_exist):
49
                missing_pkgs = [p for p, x in zip(package_names, mods_exist)
50
                                if not x]
51
                if len(missing_pkgs) > 1:
52
                    error_msg = '{} depends on missing [{}] packages.'.format(
53
                        cls.__name__, ", ".join(missing_pkgs))
54
                else:
55
                    error_msg = '{} depends on missing "{}" package.'.format(
56
                        cls.__name__, missing_pkgs[0])
57
58
                def _apply(cls, args, run):
59
                    raise ImportError(error_msg)
60
                cls.__doc__ = '( ' + error_msg + ')'
61
                cls.apply = classmethod(_apply)
62
                cls._enabled = False
63
64
        type.__init__(cls, what, bases, dict_)
65
66
67
class CommandLineOption(with_metaclass(CheckDependencies, object)):
68
    """
69
    Base class for all command-line options.
70
71
    To implement a new command-line option just inherit from this class.
72
    Then add the `flag` class-attribute to specify the name and a class
73
    docstring with the description.
74
    If your command-line option should take an argument you must also provide
75
    its name via the `arg` class attribute and its description as
76
    `arg_description`.
77
    Finally you need to implement the `execute` classmethod. It receives the
78
    value of the argument (if applicable) and the current run. You can modify
79
    the run object in any way.
80
81
    If the command line option depends on one or more installed packages, this
82
    should be specified as the `__depends_on__` attribute.
83
    It can be either a string with the name of the module, or a list/tuple of
84
    such names.
85
    If the module name (import name) differs from the name of the package, the
86
    latter can be specified using a '#' to improve the description and error
87
    message.
88
    For example `__depends_on__ = 'git#GitPython'`.
89
    """
90
91
    _enabled = True
92
93
    short_flag = None
94
    """ The (one-letter) short form (defaults to first letter of flag) """
95
96
    arg = None
97
    """ Name of the argument (optional) """
98
99
    arg_description = None
100
    """ Description of the argument (optional) """
101
102
    @classmethod
103
    def get_flag(cls):
104
        # Get the flag name from the class name
105
        flag = cls.__name__
106
        if flag.endswith("Option"):
107
            flag = flag[:-6]
108
        return '--' + convert_camel_case_to_snake_case(flag)
109
110
    @classmethod
111
    def get_short_flag(cls):
112
        if cls.short_flag is None:
113
            return '-' + cls.get_flag()[2]
114
        else:
115
            return '-' + cls.short_flag
116
117
    @classmethod
118
    def get_flags(cls):
119
        """
120
        Return the short and the long version of this option.
121
122
        The long flag (e.g. '--foo_bar'; used on the command-line like this:
123
        --foo_bar[=ARGS]) is derived from the class-name by stripping away any
124
        -Option suffix and converting the rest to snake_case.
125
126
        The short flag (e.g. '-f'; used on the command-line like this:
127
        -f [ARGS]) the short_flag class-member if that is set, or the first
128
        letter of the long flag otherwise.
129
130
        Returns
131
        -------
132
        (str, str)
133
            tuple of short-flag, and long-flag
134
135
        """
136
        return cls.get_short_flag(), cls.get_flag()
137
138
    @classmethod
139
    def apply(cls, args, run):
140
        """
141
        Modify the current Run base on this command-line option.
142
143
        This function is executed after constructing the Run object, but
144
        before actually starting it.
145
146
        Parameters
147
        ----------
148
        args : bool | str
149
            If this command-line option accepts an argument this will be value
150
            of that argument if set or None.
151
            Otherwise it is either True or False.
152
        run :  sacred.run.Run
153
            The current run to be modified
154
155
        """
156
        pass
157
158
159
def gather_command_line_options(filter_disabled=None):
160
    """Get a sorted list of all CommandLineOption subclasses."""
161
    if filter_disabled is None:
162
        filter_disabled = not SETTINGS.COMMAND_LINE.SHOW_DISABLED_OPTIONS
163
    options = [opt for opt in get_inheritors(CommandLineOption)
164
               if not filter_disabled or opt._enabled]
165
    return sorted(options, key=lambda opt: opt.__name__)
166
167
168
class HelpOption(CommandLineOption):
169
    """Print this help message and exit."""
170
171
172
class DebugOption(CommandLineOption):
173
    """
174
    Suppress warnings about missing observers and don't filter the stacktrace.
175
176
    Also enables usage with ipython --pdb.
177
    """
178
179
    @classmethod
180
    def apply(cls, args, run):
181
        """Set this run to debug mode."""
182
        run.debug = True
183
184
185
class PDBOption(CommandLineOption):
186
    """Automatically enter post-mortem debugging with pdb on failure."""
187
188
    short_flag = 'D'
189
190
    @classmethod
191
    def apply(cls, args, run):
192
        run.pdb = True
193
194
195
class LoglevelOption(CommandLineOption):
196
    """Adjust the loglevel."""
197
198
    arg = 'LEVEL'
199
    arg_description = 'Loglevel either as 0 - 50 or as string: DEBUG(10), ' \
200
                      'INFO(20), WARNING(30), ERROR(40), CRITICAL(50)'
201
202
    @classmethod
203
    def apply(cls, args, run):
204
        """Adjust the loglevel of the root-logger of this run."""
205
        # TODO: sacred.initialize.create_run already takes care of this
206
207
        try:
208
            lvl = int(args)
209
        except ValueError:
210
            lvl = args
211
        run.root_logger.setLevel(lvl)
212
213
214
class CommentOption(CommandLineOption):
215
    """Adds a message to the run."""
216
217
    arg = 'COMMENT'
218
    arg_description = 'A comment that should be stored along with the run.'
219
220
    @classmethod
221
    def apply(cls, args, run):
222
        """Add a comment to this run."""
223
        run.meta_info['comment'] = args
224
225
226
class BeatIntervalOption(CommandLineOption):
227
    """Control the rate of heartbeat events."""
228
229
    arg = 'BEAT_INTERVAL'
230
    arg_description = "Time between two heartbeat events measured in seconds."
231
232
    @classmethod
233
    def apply(cls, args, run):
234
        """Set the heart-beat interval for this run."""
235
        run.beat_interval = float(args)
236
237
238
class UnobservedOption(CommandLineOption):
239
    """Ignore all observers for this run."""
240
241
    @classmethod
242
    def apply(cls, args, run):
243
        """Set this run to unobserved mode."""
244
        run.unobserved = True
245
246
247
class QueueOption(CommandLineOption):
248
    """Only queue this run, do not start it."""
249
250
    @classmethod
251
    def apply(cls, args, run):
252
        """Set this run to queue only mode."""
253
        run.queue_only = True
254
255
256
class ForceOption(CommandLineOption):
257
    """Disable warnings about suspicious changes for this run."""
258
259
    @classmethod
260
    def apply(cls, args, run):
261
        """Set this run to not warn about suspicous changes."""
262
        run.force = True
263
264
265
class PriorityOption(CommandLineOption):
266
    """Sets the priority for a queued up experiment."""
267
268
    short_flag = 'P'
269
    arg = 'PRIORITY'
270
    arg_description = 'The (numeric) priority for this run.'
271
272
    @classmethod
273
    def apply(cls, args, run):
274
        """Add priority info for this run."""
275
        try:
276
            priority = float(args)
277
        except ValueError:
278
            raise ValueError("The PRIORITY argument must be a number! "
279
                             "(but was '{}')".format(args))
280
        run.meta_info['priority'] = priority
281
282
283
class EnforceCleanOption(CommandLineOption):
284
    """Fail if any version control repository is dirty."""
285
286
    __depends_on__ = 'git#GitPython'
287
288
    @classmethod
289
    def apply(cls, args, run):
290
        repos = run.experiment_info['repositories']
291
        if not repos:
292
            raise RuntimeError('No version control detected. '
293
                               'Cannot enforce clean repository.\n'
294
                               'Make sure that your sources under VCS and the '
295
                               'corresponding python package is installed.')
296
        else:
297
            for repo in repos:
298
                if repo['dirty']:
299
                    raise RuntimeError('EnforceClean: Uncommited changes in '
300
                                       'the "{}" repository.'.format(repo))
301
302
303
class PrintConfigOption(CommandLineOption):
304
    """Always print the configuration first."""
305
306
    @classmethod
307
    def apply(cls, args, run):
308
        print_config(run)
309
        print('-' * 79)
310
311
312
class NameOption(CommandLineOption):
313
    """Set the name for this run."""
314
315
    arg = 'NAME'
316
    arg_description = 'Name for this run.'
317
318
    @classmethod
319
    def apply(cls, args, run):
320
        run.experiment_info['name'] = args
321
        run.run_logger = run.root_logger.getChild(args)
322
323
324
class CaptureOption(CommandLineOption):
325
    """Control the way stdout and stderr are captured."""
326
327
    short_flag = 'C'
328
    arg = 'CAPTURE_MODE'
329
    arg_description = "stdout/stderr capture mode. One of [no, sys, fd]"
330
331
    @classmethod
332
    def apply(cls, args, run):
333
        run.capture_mode = args
334