Completed
Push — master ( dbc3e8...c58680 )
by Klaus
37s
created

CheckDependencies.__init__()   C

Complexity

Conditions 8

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
c 0
b 0
f 0
dl 0
loc 22
rs 5.2631

1 Method

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