Completed
Pull Request — master (#2338)
by Zatreanu
01:57
created

Bear.maintainers()   A

Complexity

Conditions 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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