Completed
Pull Request — master (#2352)
by Zatreanu
02:03
created

Bear.run()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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