Completed
Pull Request — master (#2337)
by
unknown
01:58
created

Bear.execute()   A

Complexity

Conditions 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 17
rs 9.4285
c 0
b 0
f 0
1
import traceback
2
from functools import partial
3
from os import makedirs
4
from os.path import join, abspath, exists
5
from shutil import copyfileobj
6
from urllib.request import urlopen
7
8
from appdirs import user_data_dir
9
10
from pyprint.Printer import Printer
11
12
from coala_decorators.decorators import (
13
    enforce_signature, classproperty, get_public_members)
14
from coalib.bears.requirements.PackageRequirement import PackageRequirement
0 ignored issues
show
Unused Code introduced by
Unused PackageRequirement imported from coalib.bears.requirements.PackageRequirement
Loading history...
15
from coalib.bears.requirements.PipRequirement import PipRequirement
0 ignored issues
show
Unused Code introduced by
Unused PipRequirement imported from coalib.bears.requirements.PipRequirement
Loading history...
16
from coalib.output.printers.LogPrinter import LogPrinter
17
from coalib.results.Result import Result
18
from coalib.settings.FunctionMetadata import FunctionMetadata
19
from coalib.settings.Section import Section
20
from coalib.settings.ConfigurationGathering import get_config_directory
21
22
23
class Bear(Printer, LogPrinter):
24
    """
25
    A bear contains the actual subroutine that is responsible for checking
26
    source code for certain specifications. However it can actually do
27
    whatever it wants with the files it gets. If you are missing some Result
28
    type, feel free to contact us and/or help us extending the coalib.
29
30
    This is the base class for every bear. If you want to write an bear, you
31
    will probably want to look at the GlobalBear and LocalBear classes that
32
    inherit from this class. In any case you'll want to overwrite at least the
33
    run method. You can send debug/warning/error messages through the
34
    debug(), warn(), err() functions. These will send the
35
    appropriate messages so that they are outputted. Be aware that if you use
36
    err(), you are expected to also terminate the bear run-through
37
    immediately.
38
39
    If you need some setup or teardown for your bear, feel free to overwrite
40
    the set_up() and tear_down() functions. They will be invoked
41
    before/after every run invocation.
42
43
    Settings are available at all times through self.section.
44
45
    To indicate which languages your bear supports, just give it the
46
    ``LANGUAGES`` value which should be a set of string(s):
47
48
    >>> class SomeBear(Bear):
49
    ...     LANGUAGES = {'C', 'CPP','C#', 'D'}
50
51
    To indicate the requirements of the bear, assign ``REQUIREMENTS`` a set
52
    with instances of ``PackageRequirements``.
53
54
    >>> class SomeBear(Bear):
55
    ...     REQUIREMENTS = {
56
    ...         PackageRequirement('pip', 'coala_decorators', '0.2.1')}
57
58
    If your bear uses requirements from a manager we have a subclass from,
59
    you can use the subclass, such as ``PipRequirement``, without specifying
60
    manager:
61
62
    >>> class SomeBear(Bear):
63
    ...     REQUIREMENTS = {PipRequirement('coala_decorators', '0.2.1')}
64
65
    To specify multiple requirements using ``pip``, you can use the multiple
66
    method. This can receive both tuples of strings, in case you want a specific
67
    version, or a simple string, in case you want the latest version to be
68
    specified.
69
70
    >>> class SomeBear(Bear):
71
    ...     REQUIREMENTS = PipRequirement.multiple(
72
    ...         ('colorama', '0.1'), 'coala_decorators')
73
74
    To specify additional attributes to your bear, use the following:
75
76
    >>> class SomeBear(Bear):
77
    ...     AUTHORS = {'Jon Snow'}
78
    ...     AUTHORS_EMAILS = {'[email protected]'}
79
    ...     MAINTAINERS = {'Catelyn Stark'}
80
    ...     MAINTAINERS_EMAILS = {'[email protected]'}
81
    ...     LICENSE = 'AGPL-3.0'
82
83
    If the maintainers are the same as the authors, they can be omitted:
84
85
    >>> class SomeBear(Bear):
86
    ...     AUTHORS = {'Jon Snow'}
87
    ...     AUTHORS_EMAILS = {'[email protected]'}
88
    >>> SomeBear.maintainers
89
    {'Jon Snow'}
90
    >>> SomeBear.maintainers_emails
91
    {'[email protected]'}
92
93
    If your bear needs to include local files, then specify it giving strings
94
    containing relative file paths to the INCLUDE_LOCAL_FILES set:
95
96
    >>> class SomeBear(Bear):
97
    ...     INCLUDE_LOCAL_FILES = {'checkstyle.jar', 'google_checks.xml'}
98
99
    To keep track easier of what a bear can do, simply tell it to the CAN_FIX
100
    and the CAN_DETECT sets. Possible values:
101
102
    >>> CAN_DETECT = {'Syntax', 'Formatting', 'Security', 'Complexity', 'Smell',
103
    ... 'Unused Code', 'Redundancy', 'Variable Misuse', 'Spelling',
104
    ... 'Memory Leak', 'Documentation', 'Duplication', 'Commented Code',
105
    ... 'Grammar', 'Missing Import', 'Unreachable Code', 'Undefined Element',
106
    ... 'Code Simplification'}
107
    >>> CAN_FIX = {'Syntax', ...}
108
109
    Specifying something to CAN_FIX makes it obvious that it can be detected
110
    too, so it may be omitted:
111
112
    >>> class SomeBear(Bear):
113
    ...     CAN_DETECT = {'Syntax', 'Security'}
114
    ...     CAN_FIX = {'Redundancy'}
115
    >>> list(sorted(SomeBear.can_detect))
116
    ['Redundancy', 'Security', 'Syntax']
117
118
    Every bear has a data directory which is unique to that particular bear:
119
120
    >>> class SomeBear(Bear): pass
121
    >>> class SomeOtherBear(Bear): pass
122
    >>> SomeBear.data_dir == SomeOtherBear.data_dir
123
    False
124
    """
125
126
    LANGUAGES = set()
127
    REQUIREMENTS = set()
128
    AUTHORS = set()
129
    AUTHORS_EMAILS = set()
130
    MAINTAINERS = set()
131
    MAINTAINERS_EMAILS = set()
132
    PLATFORMS = {'any'}
133
    LICENSE = ''
134
    INCLUDE_LOCAL_FILES = set()
135
    CAN_DETECT = set()
136
    CAN_FIX = set()
137
138
    @classproperty
139
    def name(cls):
140
        """
141
        :return: The name of the bear
142
        """
143
        return cls.__name__
144
145
    @classproperty
146
    def can_detect(cls):
147
        """
148
        :return: A set that contains everything a bear can detect, gathering
149
                 information from what it can fix too.
150
        """
151
        return cls.CAN_DETECT | cls.CAN_FIX
152
153
    @classproperty
154
    def maintainers(cls):
155
        """
156
        :return: A set containing ``MAINTAINERS`` if specified, else takes
157
                 ``AUTHORS`` by default.
158
        """
159
        return cls.AUTHORS if cls.MAINTAINERS == set() else cls.MAINTAINERS
160
161
    @classproperty
162
    def maintainers_emails(cls):
163
        """
164
        :return: A set containing ``MAINTAINERS_EMAILS`` if specified, else
165
                 takes ``AUTHORS_EMAILS`` by default.
166
        """
167
        return (cls.AUTHORS_EMAILS if cls.MAINTAINERS_EMAILS == set()
168
                else cls.MAINTAINERS)
169
170
    @enforce_signature
171
    def __init__(self,
172
                 section: Section,
173
                 message_queue,
174
                 timeout=0):
175
        """
176
        Constructs a new bear.
177
178
        :param section:       The section object where bear settings are
179
                              contained.
180
        :param message_queue: The queue object for messages. Can be ``None``.
181
        :param timeout:       The time the bear is allowed to run. To set no
182
                              time limit, use 0.
183
        :raises TypeError:    Raised when ``message_queue`` is no queue.
184
        :raises RuntimeError: Raised when bear requirements are not fulfilled.
185
        """
186
        Printer.__init__(self)
187
        LogPrinter.__init__(self, self)
188
189
        if message_queue is not None and not hasattr(message_queue, "put"):
190
            raise TypeError("message_queue has to be a Queue or None.")
191
192
        self.section = section
193
        self.message_queue = message_queue
194
        self.timeout = timeout
195
196
        self.setup_dependencies()
197
        cp = type(self).check_prerequisites()
198
        if cp is not True:
199
            error_string = ("The bear " + self.name +
200
                            " does not fulfill all requirements.")
201
            if cp is not False:
202
                error_string += " " + cp
203
204
            self.warn(error_string)
205
            raise RuntimeError(error_string)
206
207
    def _print(self, output, **kwargs):
208
        self.debug(output)
209
210
    def log_message(self, log_message, timestamp=None, **kwargs):
211
        if self.message_queue is not None:
212
            self.message_queue.put(log_message)
213
214
    def run(self, *args, dependency_results=None, **kwargs):
215
        raise NotImplementedError
216
217
    def run_bear_from_section(self, args, kwargs):
218
        try:
219
            kwargs.update(
220
                self.get_metadata().create_params_from_section(self.section))
221
        except ValueError as err:
222
            self.warn("The bear {} cannot be executed.".format(
223
                self.name), str(err))
224
            return
225
226
        return self.run(*args, **kwargs)
227
228
    def execute(self, *args, **kwargs):
229
        name = self.name
230
        try:
231
            self.debug("Running bear {}...".format(name))
232
            # If it's already a list it won't change it
233
            return list(self.run_bear_from_section(args, kwargs) or [])
234
        except:
235
            self.warn(
236
                "Bear {} failed to run. Take a look at debug messages (`-L "
237
                "DEBUG`) for further information.".format(name))
238
            self.debug(
239
                "The bear {bear} raised an exception. If you are the writer "
240
                "of this bear, please make sure to catch all exceptions. If "
241
                "not and this error annoys you, you might want to get in "
242
                "contact with the writer of this bear.\n\nTraceback "
243
                "information is provided below:\n\n{traceback}"
244
                "\n".format(bear=name, traceback=traceback.format_exc()))
245
246
    @staticmethod
247
    def kind():
248
        """
249
        :return: The kind of the bear
250
        """
251
        raise NotImplementedError
252
253
    @classmethod
254
    def get_metadata(cls):
255
        """
256
        :return: Metadata for the run function. However parameters like
257
                 ``self`` or parameters implicitly used by coala (e.g.
258
                 filename for local bears) are already removed.
259
        """
260
        return FunctionMetadata.from_function(
261
            cls.run,
262
            omit={"self", "dependency_results"})
263
264
    @classmethod
265
    def __json__(cls):
266
        """
267
        Override JSON export of ``Bear`` object.
268
        """
269
        _dict = get_public_members(cls)
270
        metadata = cls.get_metadata()
271
        non_optional_params = metadata.non_optional_params
272
        optional_params = metadata.optional_params
273
        _dict["metadata"] = {
274
            "desc": metadata.desc,
275
            "non_optional_params": [({param: non_optional_params[param][0]}
276
                                     for param in non_optional_params)],
277
            "optional_params": [({param: optional_params[param][0]}
278
                                 for param in optional_params)]}
279
280
        # Delete attributes that cannot be serialized
281
        unserializable_attributes = ["new_result", "printer"]
282
        for attribute in unserializable_attributes:
283
            if attribute in _dict:
284
                del _dict[attribute]
285
        return _dict
286
287
    @classmethod
288
    def missing_dependencies(cls, lst):
289
        """
290
        Checks if the given list contains all dependencies.
291
292
        :param lst: A list of all already resolved bear classes (not
293
                    instances).
294
        :return:    A list of missing dependencies.
295
        """
296
        dep_classes = cls.get_dependencies()
297
298
        for item in lst:
299
            if item in dep_classes:
300
                dep_classes.remove(item)
301
302
        return dep_classes
303
304
    @staticmethod
305
    def get_dependencies():
306
        """
307
        Retrieves bear classes that are to be executed before this bear gets
308
        executed. The results of these bears will then be passed to the
309
        run method as a dict via the dependency_results argument. The dict
310
        will have the name of the Bear as key and the list of its results as
311
        results.
312
313
        :return: A list of bear classes.
314
        """
315
        return []
316
317
    @classmethod
318
    def get_non_optional_settings(cls):
319
        """
320
        This method has to determine which settings are needed by this bear.
321
        The user will be prompted for needed settings that are not available
322
        in the settings file so don't include settings where a default value
323
        would do.
324
325
        :return: A dictionary of needed settings as keys and a tuple of help
326
                 text and annotation as values
327
        """
328
        return cls.get_metadata().non_optional_params
329
330
    @staticmethod
331
    def setup_dependencies():
332
        """
333
        This is a user defined function that can download and set up
334
        dependencies (via download_cached_file or arbitary other means) in an OS
335
        independent way.
336
        """
337
338
    @classmethod
339
    def check_prerequisites(cls):
340
        """
341
        Checks whether needed runtime prerequisites of the bear are satisfied.
342
343
        This function gets executed at construction and returns True by
344
        default.
345
346
        Section value requirements shall be checked inside the ``run`` method.
347
348
        :return: True if prerequisites are satisfied, else False or a string
349
                 that serves a more detailed description of what's missing.
350
        """
351
        return True
352
353
    def get_config_dir(self):
354
        """
355
        Gives the directory where the configuration file is
356
357
        :return: Directory of the config file
358
        """
359
        return get_config_directory(self.section)
360
361
    def download_cached_file(self, url, filename):
362
        """
363
        Downloads the file if needed and caches it for the next time. If a
364
        download happens, the user will be informed.
365
366
        Take a sane simple bear:
367
368
        >>> from queue import Queue
369
        >>> bear = Bear(Section("a section"), Queue())
370
371
        We can now carelessly query for a neat file that doesn't exist yet:
372
373
        >>> from os import remove
374
        >>> if exists(join(bear.data_dir, "a_file")):
375
        ...     remove(join(bear.data_dir, "a_file"))
376
        >>> file = bear.download_cached_file("http://gitmate.com/", "a_file")
377
378
        If we download it again, it'll be much faster as no download occurs:
379
380
        >>> newfile = bear.download_cached_file("http://gitmate.com/", "a_file")
381
        >>> newfile == file
382
        True
383
384
        :param url:      The URL to download the file from.
385
        :param filename: The filename it should get, e.g. "test.txt".
386
        :return:         A full path to the file ready for you to use!
387
        """
388
        filename = join(self.data_dir, filename)
389
        if exists(filename):
390
            return filename
391
392
        self.info("Downloading {filename!r} for bear {bearname} from {url}."
393
                  .format(filename=filename, bearname=self.name, url=url))
394
395
        with urlopen(url) as response, open(filename, 'wb') as out_file:
396
            copyfileobj(response, out_file)
397
        return filename
398
399
    @classproperty
400
    def data_dir(cls):
401
        """
402
        Returns a directory that may be used by the bear to store stuff. Every
403
        bear has an own directory dependent on his name.
404
        """
405
        data_dir = abspath(join(user_data_dir('coala-bears'), cls.name))
406
407
        makedirs(data_dir, exist_ok=True)
408
        return data_dir
409
410
    @property
411
    def new_result(self):
412
        """
413
        Returns a partial for creating a result with this bear already bound.
414
        """
415
        return partial(Result.from_values, self)
416