Completed
Pull Request — master (#2432)
by Zatreanu
01:47
created

Bear   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 370
Duplicated Lines 0 %

Importance

Changes 11
Bugs 0 Features 0
Metric Value
wmc 34
c 11
b 0
f 0
dl 0
loc 370
rs 9.2
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-utils.decorators import enforce_signature, classproperty
13
from coalib.bears.requirements.PackageRequirement import PackageRequirement
14
from coalib.bears.requirements.PipRequirement import PipRequirement
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 set
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 ``PipRequirement``, without specifying
59
    manager:
60
61
    >>> class SomeBear(Bear):
62
    ...     REQUIREMENTS = {PipRequirement('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 = PipRequirement.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
84
    >>> class SomeBear(Bear):
85
    ...     AUTHORS = {'Jon Snow'}
86
    ...     AUTHORS_EMAILS = {'[email protected]'}
87
    >>> SomeBear.maintainers
88
    {'Jon Snow'}
89
    >>> SomeBear.maintainers_emails
90
    {'[email protected]'}
91
92
    If your bear needs to include local files, then specify it giving strings
93
    containing relative file paths to the INCLUDE_LOCAL_FILES set:
94
95
    >>> class SomeBear(Bear):
96
    ...     INCLUDE_LOCAL_FILES = {'checkstyle.jar', 'google_checks.xml'}
97
98
    To keep track easier of what a bear can do, simply tell it to the CAN_FIX
99
    and the CAN_DETECT sets. Possible values:
100
101
    >>> CAN_DETECT = {'Syntax', 'Formatting', 'Security', 'Complexity', 'Smell',
102
    ... 'Unused Code', 'Redundancy', 'Variable Misuse', 'Spelling',
103
    ... 'Memory Leak', 'Documentation', 'Duplication', 'Commented Code',
104
    ... 'Grammar', 'Missing Import', 'Unreachable Code', 'Undefined Element',
105
    ... 'Code Simplification'}
106
    >>> CAN_FIX = {'Syntax', ...}
107
108
    Specifying something to CAN_FIX makes it obvious that it can be detected
109
    too, so it may be omitted:
110
111
    >>> class SomeBear(Bear):
112
    ...     CAN_DETECT = {'Syntax', 'Security'}
113
    ...     CAN_FIX = {'Redundancy'}
114
    >>> list(sorted(SomeBear.can_detect))
115
    ['Redundancy', 'Security', 'Syntax']
116
117
    Every bear has a data directory which is unique to that particular bear:
118
119
    >>> class SomeBear(Bear): pass
120
    >>> class SomeOtherBear(Bear): pass
121
    >>> SomeBear.data_dir == SomeOtherBear.data_dir
122
    False
123
    """
124
125
    LANGUAGES = set()
126
    REQUIREMENTS = set()
127
    AUTHORS = set()
128
    AUTHORS_EMAILS = set()
129
    MAINTAINERS = set()
130
    MAINTAINERS_EMAILS = set()
131
    PLATFORMS = {'any'}
132
    LICENSE = ''
133
    INCLUDE_LOCAL_FILES = set()
134
    CAN_DETECT = set()
135
    CAN_FIX = set()
136
137
    @classproperty
138
    def name(cls):
139
        """
140
        :return: The name of the bear
141
        """
142
        return cls.__name__
143
144
    @classproperty
145
    def can_detect(cls):
146
        """
147
        :return: A set that contains everything a bear can detect, gathering
148
                 information from what it can fix too.
149
        """
150
        return cls.CAN_DETECT | cls.CAN_FIX
151
152
    @classproperty
153
    def maintainers(cls):
154
        """
155
        :return: A set containing ``MAINTAINERS`` if specified, else takes
156
                 ``AUTHORS`` by default.
157
        """
158
        return cls.AUTHORS if cls.MAINTAINERS == set() else cls.MAINTAINERS
159
160
    @classproperty
161
    def maintainers_emails(cls):
162
        """
163
        :return: A set containing ``MAINTAINERS_EMAILS`` if specified, else
164
                 takes ``AUTHORS_EMAILS`` by default.
165
        """
166
        return (cls.AUTHORS_EMAILS if cls.MAINTAINERS_EMAILS == set()
167
                else cls.MAINTAINERS)
168
169
    @enforce_signature
170
    def __init__(self,
171
                 section: Section,
172
                 message_queue,
173
                 timeout=0):
174
        """
175
        Constructs a new bear.
176
177
        :param section:       The section object where bear settings are
178
                              contained.
179
        :param message_queue: The queue object for messages. Can be ``None``.
180
        :param timeout:       The time the bear is allowed to run. To set no
181
                              time limit, use 0.
182
        :raises TypeError:    Raised when ``message_queue`` is no queue.
183
        :raises RuntimeError: Raised when bear requirements are not fulfilled.
184
        """
185
        Printer.__init__(self)
186
        LogPrinter.__init__(self, self)
187
188
        if message_queue is not None and not hasattr(message_queue, "put"):
189
            raise TypeError("message_queue has to be a Queue or None.")
190
191
        self.section = section
192
        self.message_queue = message_queue
193
        self.timeout = timeout
194
195
        self.setup_dependencies()
196
        cp = type(self).check_prerequisites()
197
        if cp is not True:
198
            error_string = ("The bear " + self.name +
199
                            " does not fulfill all requirements.")
200
            if cp is not False:
201
                error_string += " " + cp
202
203
            self.warn(error_string)
204
            raise RuntimeError(error_string)
205
206
    def _print(self, output, **kwargs):
207
        self.debug(output)
208
209
    def log_message(self, log_message, timestamp=None, **kwargs):
210
        if self.message_queue is not None:
211
            self.message_queue.put(log_message)
212
213
    def run(self, *args, dependency_results=None, **kwargs):
214
        raise NotImplementedError
215
216
    def run_bear_from_section(self, args, kwargs):
217
        try:
218
            kwargs.update(
219
                self.get_metadata().create_params_from_section(self.section))
220
        except ValueError as err:
221
            self.warn("The bear {} cannot be executed.".format(
222
                self.name), str(err))
223
            return
224
225
        return self.run(*args, **kwargs)
226
227
    def execute(self, *args, **kwargs):
228
        name = self.name
229
        try:
230
            self.debug("Running bear {}...".format(name))
231
            # If it's already a list it won't change it
232
            return list(self.run_bear_from_section(args, kwargs) or [])
233
        except:
234
            self.warn(
235
                "Bear {} failed to run. Take a look at debug messages (`-L "
236
                "DEBUG`) for further information.".format(name))
237
            self.debug(
238
                "The bear {bear} raised an exception. If you are the writer "
239
                "of this bear, please make sure to catch all exceptions. If "
240
                "not and this error annoys you, you might want to get in "
241
                "contact with the writer of this bear.\n\nTraceback "
242
                "information is provided below:\n\n{traceback}"
243
                "\n".format(bear=name, traceback=traceback.format_exc()))
244
245
    @staticmethod
246
    def kind():
247
        """
248
        :return: The kind of the bear
249
        """
250
        raise NotImplementedError
251
252
    @classmethod
253
    def get_metadata(cls):
254
        """
255
        :return: Metadata for the run function. However parameters like
256
                 ``self`` or parameters implicitly used by coala (e.g.
257
                 filename for local bears) are already removed.
258
        """
259
        return FunctionMetadata.from_function(
260
            cls.run,
261
            omit={"self", "dependency_results"})
262
263
    @classmethod
264
    def missing_dependencies(cls, lst):
265
        """
266
        Checks if the given list contains all dependencies.
267
268
        :param lst: A list of all already resolved bear classes (not
269
                    instances).
270
        :return:    A list of missing dependencies.
271
        """
272
        dep_classes = cls.get_dependencies()
273
274
        for item in lst:
275
            if item in dep_classes:
276
                dep_classes.remove(item)
277
278
        return dep_classes
279
280
    @staticmethod
281
    def get_dependencies():
282
        """
283
        Retrieves bear classes that are to be executed before this bear gets
284
        executed. The results of these bears will then be passed to the
285
        run method as a dict via the dependency_results argument. The dict
286
        will have the name of the Bear as key and the list of its results as
287
        results.
288
289
        :return: A list of bear classes.
290
        """
291
        return []
292
293
    @classmethod
294
    def get_non_optional_settings(cls):
295
        """
296
        This method has to determine which settings are needed by this bear.
297
        The user will be prompted for needed settings that are not available
298
        in the settings file so don't include settings where a default value
299
        would do.
300
301
        :return: A dictionary of needed settings as keys and a tuple of help
302
                 text and annotation as values
303
        """
304
        return cls.get_metadata().non_optional_params
305
306
    @staticmethod
307
    def setup_dependencies():
308
        """
309
        This is a user defined function that can download and set up
310
        dependencies (via download_cached_file or arbitary other means) in an OS
311
        independent way.
312
        """
313
314
    @classmethod
315
    def check_prerequisites(cls):
316
        """
317
        Checks whether needed runtime prerequisites of the bear are satisfied.
318
319
        This function gets executed at construction and returns True by
320
        default.
321
322
        Section value requirements shall be checked inside the ``run`` method.
323
324
        :return: True if prerequisites are satisfied, else False or a string
325
                 that serves a more detailed description of what's missing.
326
        """
327
        return True
328
329
    def get_config_dir(self):
330
        """
331
        Gives the directory where the configuration file is
332
333
        :return: Directory of the config file
334
        """
335
        return get_config_directory(self.section)
336
337
    def download_cached_file(self, url, filename):
338
        """
339
        Downloads the file if needed and caches it for the next time. If a
340
        download happens, the user will be informed.
341
342
        Take a sane simple bear:
343
344
        >>> from queue import Queue
345
        >>> bear = Bear(Section("a section"), Queue())
346
347
        We can now carelessly query for a neat file that doesn't exist yet:
348
349
        >>> from os import remove
350
        >>> if exists(join(bear.data_dir, "a_file")):
351
        ...     remove(join(bear.data_dir, "a_file"))
352
        >>> file = bear.download_cached_file("http://gitmate.com/", "a_file")
353
354
        If we download it again, it'll be much faster as no download occurs:
355
356
        >>> newfile = bear.download_cached_file("http://gitmate.com/", "a_file")
357
        >>> newfile == file
358
        True
359
360
        :param url:      The URL to download the file from.
361
        :param filename: The filename it should get, e.g. "test.txt".
362
        :return:         A full path to the file ready for you to use!
363
        """
364
        filename = join(self.data_dir, filename)
365
        if exists(filename):
366
            return filename
367
368
        self.info("Downloading {filename!r} for bear {bearname} from {url}."
369
                  .format(filename=filename, bearname=self.name, url=url))
370
371
        with urlopen(url) as response, open(filename, 'wb') as out_file:
372
            copyfileobj(response, out_file)
373
        return filename
374
375
    @classproperty
376
    def data_dir(cls):
377
        """
378
        Returns a directory that may be used by the bear to store stuff. Every
379
        bear has an own directory dependent on his name.
380
        """
381
        data_dir = abspath(join(user_data_dir('coala-bears'), cls.name))
382
383
        makedirs(data_dir, exist_ok=True)
384
        return data_dir
385
386
    @property
387
    def new_result(self):
388
        """
389
        Returns a partial for creating a result with this bear already bound.
390
        """
391
        return partial(Result.from_values, self)
392