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

Experiment.run_commandline()   F

Complexity

Conditions 12

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 12
dl 0
loc 49
rs 2.5363
c 2
b 0
f 1

How to fix   Complexity   

Complexity

Complex classes like Experiment.run_commandline() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
#!/usr/bin/env python
2
# coding=utf-8
3
"""This module defines the Experiment class, which is central to sacred."""
4
from __future__ import division, print_function, unicode_literals
5
6
import inspect
7
import os.path
8
import shlex
9
import sys
10
from collections import OrderedDict
11
12
from sacred.arg_parser import get_config_updates, parse_args
13
from sacred.commandline_options import gather_command_line_options, ForceOption
14
from sacred.commands import print_config, print_dependencies, save_config
15
from sacred.config.signature import Signature
16
from sacred.ingredient import Ingredient
17
from sacred.initialize import create_run
18
from sacred.optional import basestring
19
from sacred.utils import print_filtered_stacktrace
20
21
__sacred__ = True  # marks files that should be filtered from stack traces
22
23
__all__ = ('Experiment',)
24
25
26
class Experiment(Ingredient):
27
    """
28
    The central class for each experiment in Sacred.
29
30
    It manages the configuration, the main function, captured methods,
31
    observers, commands, and further ingredients.
32
33
    An Experiment instance should be created as one of the first
34
    things in any experiment-file.
35
    """
36
37
    def __init__(self, name=None, ingredients=(), interactive=False):
38
        """
39
        Create a new experiment with the given name and optional ingredients.
40
41
        Parameters
42
        ----------
43
        name : str, optional
44
            Optional name of this experiment, defaults to the filename.
45
            (Required in interactive mode)
46
47
        ingredients : list[sacred.Ingredient], optional
48
            A list of ingredients to be used with this experiment.
49
50
        interactive : bool, optional
51
            If set to True will allow the experiment to be run in interactive
52
            mode (e.g. IPython or Jupyter notebooks).
53
            However, this mode is discouraged since it won't allow storing the
54
            source-code or reliable reproduction of the runs.
55
        """
56
        caller_globals = inspect.stack()[1][0].f_globals
57
        if name is None:
58
            if interactive:
59
                raise RuntimeError('name is required in interactive mode.')
60
            mainfile = caller_globals.get('__file__')
61
            if mainfile is None:
62
                raise RuntimeError('No main-file found. Are you running in '
63
                                   'interactive mode? If so please provide a '
64
                                   'name and set interactive=True.')
65
            name = os.path.basename(mainfile)
66
            if name.endswith('.py'):
67
                name = name[:-3]
68
            elif name.endswith('.pyc'):
69
                name = name[:-4]
70
        super(Experiment, self).__init__(path=name,
71
                                         ingredients=ingredients,
72
                                         interactive=interactive,
73
                                         _caller_globals=caller_globals)
74
        self.default_command = ""
75
        self.command(print_config, unobserved=True)
76
        self.command(print_dependencies, unobserved=True)
77
        self.command(save_config, unobserved=True)
78
        self.observers = []
79
        self.current_run = None
80
        self.captured_out_filter = None
81
        """Filter function to be applied to captured output of a run"""
82
        self.option_hooks = []
83
84
    # =========================== Decorators ==================================
85
86
    def main(self, function):
87
        """
88
        Decorator to define the main function of the experiment.
89
90
        The main function of an experiment is the default command that is being
91
        run when no command is specified, or when calling the run() method.
92
93
        Usually it is more convenient to use ``automain`` instead.
94
        """
95
        captured = self.command(function)
96
        self.default_command = captured.__name__
97
        return captured
98
99
    def automain(self, function):
100
        """
101
        Decorator that defines *and runs* the main function of the experiment.
102
103
        The decorated function is marked as the default command for this
104
        experiment, and the command-line interface is automatically run when
105
        the file is executed.
106
107
        The method decorated by this should be last in the file because is
108
        equivalent to:
109
110
        .. code-block:: python
111
112
            @ex.main
113
            def my_main():
114
                pass
115
116
            if __name__ == '__main__':
117
                ex.run_commandline()
118
        """
119
        captured = self.main(function)
120
        if function.__module__ == '__main__':
121
            # Ensure that automain is not used in interactive mode.
122
            import inspect
123
            main_filename = inspect.getfile(function)
124
            if (main_filename == '<stdin>' or
125
                    (main_filename.startswith('<ipython-input-') and
126
                     main_filename.endswith('>'))):
127
                raise RuntimeError('Cannot use @ex.automain decorator in '
128
                                   'interactive mode. Use @ex.main instead.')
129
130
            self.run_commandline()
131
        return captured
132
133
    def option_hook(self, function):
134
        """
135
        Decorator for adding an option hook function.
136
137
        An option hook is a function that is called right before a run
138
        is created. It receives (and potentially modifies) the options
139
        dictionary. That is, the dictionary of commandline options used for
140
        this run.
141
142
        .. note::
143
            The decorated function MUST have an argument called options.
144
145
            The options also contain ``'COMMAND'`` and ``'UPDATE'`` entries,
146
            but changing them has no effect. Only modification on
147
            flags (entries starting with ``'--'``) are considered.
148
        """
149
        sig = Signature(function)
150
        if "options" not in sig.arguments:
151
            raise KeyError("option_hook functions must have an argument called"
152
                           " 'options', but got {}".format(sig.arguments))
153
        self.option_hooks.append(function)
154
        return function
155
156
    # =========================== Public Interface ============================
157
158
    def run(self, command_name=None, config_updates=None, named_configs=(),
159
            meta_info=None, options=None):
160
        """
161
        Run the main function of the experiment or a given command.
162
163
        Parameters
164
        ----------
165
        command_name : str, optional
166
            Name of the command to be run. Defaults to main function.
167
168
        config_updates : dict, optional
169
            Changes to the configuration as a nested dictionary
170
171
        named_configs : list[str], optional
172
            list of names of named_configs to use
173
174
        meta_info : dict, optional
175
            Additional meta information for this run.
176
177
        options : dict, optional
178
            Dictionary of options to use
179
180
        Returns
181
        -------
182
        sacred.run.Run
183
            the Run object corresponding to the finished run
184
        """
185
        run = self._create_run(command_name, config_updates, named_configs,
186
                               meta_info, options)
187
        run()
188
        return run
189
190
    def run_command(self, command_name, config_updates=None,
191
                    named_configs=(), args=(), meta_info=None):
192
        """Run the command with the given name.
193
194
        .. note:: Deprecated in Sacred 0.7
195
            run_command() will be removed in Sacred 1.0.
196
            It is replaced by run() which can now also handle command_names.
197
        """
198
        import warnings
199
        warnings.warn("run_command is deprecated. Use run instead",
200
                      DeprecationWarning)
201
        return self.run(command_name, config_updates, named_configs, meta_info,
202
                        args)
203
204
    def run_commandline(self, argv=None):
205
        """
206
        Run the command-line interface of this experiment.
207
208
        If ``argv`` is omitted it defaults to ``sys.argv``.
209
210
        Parameters
211
        ----------
212
        argv : list[str] or str, optional
213
            Command-line as string or list of strings like ``sys.argv``.
214
215
        Returns
216
        -------
217
        sacred.run.Run
218
            The Run object corresponding to the finished run.
219
        """
220
        if argv is None:
221
            argv = sys.argv
222
        elif isinstance(argv, basestring):
223
            argv = shlex.split(argv)
224
        else:
225
            if not isinstance(argv, (list, tuple)):
226
                raise ValueError("argv must be str or list, but was {}"
227
                                 .format(type(argv)))
228
            if not all([isinstance(a, basestring) for a in argv]):
229
                problems = [a for a in argv if not isinstance(a, basestring)]
230
                raise ValueError("argv must be list of str but contained the "
231
                                 "following elements: {}".format(problems))
232
233
        all_commands = self.gather_commands()
234
235
        args = parse_args(argv,
236
                          description=self.doc,
237
                          commands=OrderedDict(all_commands))
238
        config_updates, named_configs = get_config_updates(args['UPDATE'])
239
        cmd_name = args.get('COMMAND') or self.default_command
240
241
        try:
242
            return self.run(cmd_name, config_updates, named_configs, {}, args)
243
        except Exception:
244
            if not self.current_run or self.current_run.debug:
245
                raise
246
            elif self.current_run.pdb:
247
                import traceback
248
                import pdb
249
                traceback.print_exception(*sys.exc_info())
250
                pdb.post_mortem()
251
            else:
252
                print_filtered_stacktrace()
253
254
    def open_resource(self, filename):
255
        """Open a file and also save it as a resource.
256
257
        Opens a file, reports it to the observers as a resource, and returns
258
        the opened file.
259
260
        In Sacred terminology a resource is a file that the experiment needed
261
        to access during a run. In case of a MongoObserver that means making
262
        sure the file is stored in the database (but avoiding duplicates) along
263
        its path and md5 sum.
264
265
        This function can only be called during a run, and just calls the
266
        :py:meth:`sacred.run.Run.open_resource` method.
267
268
        Parameters
269
        ----------
270
        filename: str
271
            name of the file that should be opened
272
273
        Returns
274
        -------
275
        file
276
            the opened file-object
277
        """
278
        assert self.current_run is not None, "Can only be called during a run."
279
        return self.current_run.open_resource(filename)
280
281
    def add_artifact(self, filename, name=None):
282
        """Add a file as an artifact.
283
284
        In Sacred terminology an artifact is a file produced by the experiment
285
        run. In case of a MongoObserver that means storing the file in the
286
        database.
287
288
        This function can only be called during a run, and just calls the
289
        :py:meth:`sacred.run.Run.add_artifact` method.
290
291
        Parameters
292
        ----------
293
        filename : str
294
            name of the file to be stored as artifact
295
        name : str, optional
296
            optionally set the name of the artifact.
297
            Defaults to the relative file-path.
298
        """
299
        assert self.current_run is not None, "Can only be called during a run."
300
        self.current_run.add_artifact(filename, name)
301
302
    @property
303
    def info(self):
304
        """Access the info-dict for storing custom information.
305
306
        Only works during a run and is essentially a shortcut to:
307
308
        .. code-block:: python
309
310
            @ex.capture
311
            def my_captured_function(_run):
312
                # [...]
313
                _run.info   # == ex.info
314
        """
315
        return self.current_run.info
316
317
    def gather_commands(self):
318
        """Iterator over all commands of this experiment.
319
320
        Also recursively collects all commands from ingredients.
321
322
        Yields
323
        ------
324
        (str, function)
325
            A tuple consisting of the (dotted) command-name and the
326
            corresponding captured function.
327
        """
328
        for cmd_name, cmd in self.commands.items():
329
            yield cmd_name, cmd
330
331
        for ingred in self.ingredients:
332
            for cmd_name, cmd in ingred.gather_commands():
333
                yield cmd_name, cmd
334
335
    def get_default_options(self):
336
        """Get a dictionary of default options as used with run.
337
338
        Returns
339
        -------
340
        dict
341
            A dictionary containing option keys of the form '--beat_interval'.
342
            Their values are boolean if the option is a flag, otherwise None or
343
            its default value.
344
        """
345
        all_commands = self.gather_commands()
346
        args = parse_args(["dummy"],
347
                          description=self.doc,
348
                          commands=OrderedDict(all_commands))
349
        return {k: v for k, v in args.items() if k.startswith('--')}
350
351
    # =========================== Internal Interface ==========================
352
353
    def _create_run(self, command_name=None, config_updates=None,
354
                    named_configs=(), meta_info=None, options=None):
355
        command_name = command_name or self.default_command
356
        if command_name is None:
357
            raise RuntimeError('No command found to be run. Specify a command '
358
                               'or define a main function.')
359
360
        default_options = self.get_default_options()
361
        if options:
362
            default_options.update(options)
363
        options = default_options
364
365
        # call option hooks
366
        for oh in self.option_hooks:
367
            oh(options=options)
368
369
        run = create_run(self, command_name, config_updates,
370
                         named_configs=named_configs,
371
                         force=options.get(ForceOption.get_flag(), False))
372
        run.meta_info['command'] = command_name
373
        run.meta_info['options'] = options
374
375
        if meta_info:
376
            run.meta_info.update(meta_info)
377
378
        for option in gather_command_line_options():
379
            option_value = options.get(option.get_flag(), False)
380
            if option_value:
381
                option.apply(option_value, run)
382
383
        self.current_run = run
384
        return run
385