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