Completed
Push — master ( 2d5fd3...e2acfa )
by Klaus
01:03
created

sacred.config.get_function_body_code()   B

Complexity

Conditions 4

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 4
dl 0
loc 22
rs 8.9197
1
#!/usr/bin/env python
2
# coding=utf-8
3
from __future__ import division, print_function, unicode_literals
4
5
import ast
6
import inspect
7
import re
8
from copy import copy
9
10
from sacred.config.config_summary import ConfigSummary
11
from sacred.config.utils import dogmatize, normalize_or_die, recursive_fill_in
12
13
__sacred__ = True
14
15
16
class ConfigScope(object):
17
    def __init__(self, func):
18
        super(ConfigScope, self).__init__()
19
        self.arg_spec = inspect.getargspec(func)
20
        assert self.arg_spec.varargs is None, \
21
            "varargs are not allowed for ConfigScope functions"
22
        assert self.arg_spec.keywords is None, \
23
            "kwargs are not allowed for ConfigScope functions"
24
        assert self.arg_spec.defaults is None, \
25
            "default values are not allowed for ConfigScope functions"
26
27
        self._func = func
28
        self._body_code = get_function_body_code(func)
29
30
    def __call__(self, fixed=None, preset=None, fallback=None):
31
        """
32
        Evaluate this ConfigScope.
33
34
        This will evaluate the function body and fill the relevant local
35
        variables into entries into keys in this dictionary.
36
37
        :param fixed: Dictionary of entries that should stay fixed during the
38
                      evaluation. All of them will be part of the final config.
39
        :type fixed: dict
40
        :param preset: Dictionary of preset values that will be available
41
                       during the evaluation (if they are declared in the
42
                       function argument list). All of them will be part of the
43
                       final config.
44
        :type preset: dict
45
        :param fallback: Dictionary of fallback values that will be available
46
                         during the evaluation (if they are declared in the
47
                         function argument list). They will NOT be part of the
48
                         final config.
49
        :type fallback: dict
50
        :return: self
51
        :rtype: ConfigScope
52
        """
53
        cfg_locals = dogmatize(fixed or {})
54
        fallback = fallback or {}
55
        preset = preset or {}
56
        fallback_view = {}
57
58
        available_entries = set(preset.keys()) | set(fallback.keys())
59
60
        for arg in self.arg_spec.args:
61
            if arg not in available_entries:
62
                raise KeyError("'{}' not in preset for ConfigScope. "
63
                               "Available options are: {}"
64
                               .format(arg, available_entries))
65
            if arg in preset:
66
                cfg_locals[arg] = preset[arg]
67
            else:  # arg in fallback
68
                fallback_view[arg] = fallback[arg]
69
70
        cfg_locals.fallback = fallback_view
71
        eval(self._body_code, copy(self._func.__globals__), cfg_locals)
72
73
        added = cfg_locals.revelation()
74
        config_summary = ConfigSummary(added, cfg_locals.modified,
75
                                       cfg_locals.typechanges,
76
                                       cfg_locals.fallback_writes)
77
        # fill in the unused presets
78
        recursive_fill_in(cfg_locals, preset)
79
80
        for key, value in cfg_locals.items():
81
            try:
82
                config_summary[key] = normalize_or_die(value)
83
            except ValueError:
84
                pass
85
        return config_summary
86
87
88
def get_function_body(func):
89
    func_code_lines, start_idx = inspect.getsourcelines(func)
90
    func_code = ''.join(func_code_lines)
91
    arg = "(?:[a-zA-Z_][a-zA-Z0-9_]*)"
92
    arguments = r"{0}(?:\s*,\s*{0})*".format(arg)
93
    func_def = re.compile(
94
        r"^[ \t]*def[ \t]*{}[ \t]*\(\s*({})?\s*\)[ \t]*:[ \t]*\n".format(
95
            func.__name__, arguments), flags=re.MULTILINE)
96
    defs = list(re.finditer(func_def, func_code))
97
    assert defs
98
    line_offset = start_idx + func_code[:defs[0].end()].count('\n') - 1
99
    func_body = func_code[defs[0].end():]
100
    return func_body, line_offset
101
102
103
def is_empty_or_comment(line):
104
    sline = line.strip()
105
    return sline == '' or sline.startswith('#')
106
107
108
def dedent_line(line, indent):
109
    for i, (line_sym, indent_sym) in enumerate(zip(line, indent)):
110
        if line_sym != indent_sym:
111
            start = i
112
            break
113
    else:
114
        start = len(indent)
115
    return line[start:]
116
117
118
def dedent_function_body(body):
119
    lines = body.split('\n')
120
    # find indentation by first line
121
    indent = ''
122
    for line in lines:
123
        if is_empty_or_comment(line):
124
            continue
125
        else:
126
            indent = re.match('^\s*', line).group()
127
            break
128
129
    out_lines = [dedent_line(line, indent) for line in lines]
130
    return '\n'.join(out_lines)
131
132
133
def get_function_body_code(func):
134
    filename = inspect.getfile(func)
135
    func_body, line_offset = get_function_body(func)
136
    body_source = dedent_function_body(func_body)
137
    try:
138
        body_code = compile(body_source, filename, "exec", ast.PyCF_ONLY_AST)
139
        body_code = ast.increment_lineno(body_code, n=line_offset)
140
        body_code = compile(body_code, filename, "exec")
141
    except SyntaxError as e:
142
        if e.args[0] == "'return' outside function":
143
            filename, lineno, _, statement = e.args[1]
144
            raise SyntaxError('No return statements allowed in ConfigScopes\n'
145
                              '(\'{}\' in File "{}", line {})'.format(
146
                                  statement.strip(), filename, lineno))
147
        elif e.args[0] == "'yield' outside function":
148
            filename, lineno, _, statement = e.args[1]
149
            raise SyntaxError('No yield statements allowed in ConfigScopes\n'
150
                              '(\'{}\' in File "{}", line {})'.format(
151
                                  statement.strip(), filename, lineno))
152
        else:
153
            raise
154
    return body_code
155