Issues (70)

coalib/bearlib/abstractions/ExternalBearWrap.py (3 issues)

1
import json
2
import inspect
3
from functools import partial
4
from collections import OrderedDict
5
6
from coalib.bears.LocalBear import LocalBear
7
from coala_utils.decorators import enforce_signature
8
from coalib.misc.Shell import run_shell_command
9
from coalib.results.Diff import Diff
0 ignored issues
show
Unused Diff imported from coalib.results.Diff
Loading history...
10
from coalib.results.Result import Result
11
from coalib.results.SourceRange import SourceRange
12
from coalib.results.RESULT_SEVERITY import RESULT_SEVERITY
0 ignored issues
show
Unused RESULT_SEVERITY imported from coalib.results.RESULT_SEVERITY
Loading history...
13
from coalib.settings.FunctionMetadata import FunctionMetadata
14
15
16
def _prepare_options(options):
17
    """
18
    Checks for illegal options and raises ValueError.
19
20
    :param options:
21
        The options dict that contains user/developer inputs.
22
    :raises ValueError:
23
        Raised when illegal options are specified.
24
    """
25
    allowed_options = {"executable",
26
                       "settings"}
27
28
    # Check for illegal superfluous options.
29
    superfluous_options = options.keys() - allowed_options
30
    if superfluous_options:
31
        raise ValueError(
32
            "Invalid keyword arguments provided: " +
33
            ", ".join(repr(s) for s in sorted(superfluous_options)))
34
35
    if not 'settings' in options:
36
        options['settings'] = {}
37
38
39
def _create_wrapper(klass, options):
40
    NoDefaultValue = object()
41
42
    class ExternalBearWrapBase(LocalBear):
43
44
        @staticmethod
45
        def create_arguments():
46
            """
47
            This method has to be implemented by the class that uses
48
            the decorator in order to create the arguments needed for
49
            the executable.
50
            """
51
            return ()
52
53
        @classmethod
54
        def get_executable(cls):
55
            """
56
            Returns the executable of this class.
57
58
            :return:
59
                The executable name.
60
            """
61
            return options["executable"]
62
63
        @staticmethod
64
        def _normalize_desc(description, setting_type,
65
                            default_value=NoDefaultValue):
66
            """
67
            Normalizes the description of the parameters only if there
68
            is none provided.
69
70
            :param description:
71
                The parameter description to be modified in case it is empty.
72
            :param setting_type:
73
                The type of the setting. It is needed to create the final
74
                tuple.
75
            :param default_value:
76
                The default value of the setting.
77
            :return:
78
                A value for the OrderedDict in the ``FunctionMetadata`` object.
79
            """
80
            if description == "":
81
                description = FunctionMetadata.str_nodesc
82
83
            if default_value is NoDefaultValue:
84
                return (description, setting_type)
85
            else:
86
                return (description + " " +
87
                        FunctionMetadata.str_optional.format(default_value),
88
                        setting_type, default_value)
89
90
        @classmethod
91
        def get_non_optional_params(cls):
92
            """
93
            Fetches the non_optional_params from ``options['settings']``
94
            and also normalizes their descriptions.
95
96
            :return:
97
                An OrderedDict that is used to create a
98
                ``FunctionMetadata`` object.
99
            """
100
            non_optional_params = {}
101
            for setting_name, description in options['settings'].items():
102
                if len(description) == 2:
103
                    non_optional_params[
104
                        setting_name] = cls._normalize_desc(description[0],
105
                                                            description[1])
106
            return OrderedDict(non_optional_params)
107
108
        @classmethod
109
        def get_optional_params(cls):
110
            """
111
            Fetches the optional_params from ``options['settings']``
112
            and also normalizes their descriptions.
113
114
            :return:
115
                An OrderedDict that is used to create a
116
                ``FunctionMetadata`` object.
117
            """
118
            optional_params = {}
119
            for setting_name, description in options['settings'].items():
120
                if len(description) == 3:
121
                    optional_params[
122
                        setting_name] = cls._normalize_desc(description[0],
123
                                                            description[1],
124
                                                            description[2])
125
            return OrderedDict(optional_params)
126
127
        @classmethod
128
        def get_metadata(cls):
129
            metadata = FunctionMetadata(
130
                'run',
131
                optional_params=cls.get_optional_params(),
132
                non_optional_params=cls.get_non_optional_params())
133
            metadata.desc = inspect.getdoc(cls)
134
            return metadata
135
136
        @classmethod
137
        def _prepare_settings(cls, settings):
138
            """
139
            Adds the optional settings to the settings dict in-place.
140
141
            :param settings:
142
                The settings dict.
143
            """
144
            opt_params = cls.get_optional_params()
145
            for setting_name, description in opt_params.items():
146
                if setting_name not in settings:
147
                    settings[setting_name] = description[2]
148
149
        def parse_output(self, out, filename):
0 ignored issues
show
This method could be written as a function/class method.

If a method does not access any attributes of the class, it could also be implemented as a function or static method. This can help improve readability. For example

class Foo:
    def some_method(self, x, y):
        return x + y;

could be written as

class Foo:
    @classmethod
    def some_method(cls, x, y):
        return x + y;
Loading history...
150
            """
151
            Parses the output JSON into Result objects.
152
153
            :param out:
154
                Raw output from the given executable (should be JSON).
155
            :param filename:
156
                The filename of the analyzed file. Needed to
157
                create the Result objects.
158
            :return:
159
                An iterator yielding ``Result`` objects.
160
            """
161
            output = json.loads(out)
162
163
            for result in output['results']:
164
                affected_code = tuple(
165
                    SourceRange.from_values(
166
                        code_range['file'],
167
                        code_range['start']['line'],
168
                        code_range['start'].get('column'),
169
                        code_range.get('end', {}).get('line'),
170
                        code_range.get('end', {}).get('column'))
171
                    for code_range in result['affected_code'])
172
                yield Result(
173
                    origin=result['origin'],
174
                    message=result['message'],
175
                    affected_code=affected_code,
176
                    severity=result.get('severity', 1),
177
                    debug_msg=result.get('debug_msg', ""),
178
                    additional_info=result.get('additional_info', ""))
179
180
        def run(self, filename, file, **settings):
181
            self._prepare_settings(settings)
182
            json_string = json.dumps({'filename': filename,
183
                                      'file': file,
184
                                      'settings': settings})
185
186
            args = self.create_arguments()
187
            try:
188
                args = tuple(args)
189
            except TypeError:
190
                self.err("The given arguments "
191
                         "{!r} are not iterable.".format(args))
192
                return
193
194
            shell_command = (self.get_executable(),) + args
195
            out, err = run_shell_command(shell_command, json_string)
196
197
            return self.parse_output(out, filename)
198
199
    result_klass = type(klass.__name__, (klass, ExternalBearWrapBase), {})
200
    result_klass.__doc__ = klass.__doc__ or ""
201
    return result_klass
202
203
204
@enforce_signature
205
def external_bear_wrap(executable: str, **options):
206
207
    options["executable"] = executable
208
    _prepare_options(options)
209
210
    return partial(_create_wrapper, options=options)
211