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

Bear   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 373
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
wmc 37
c 8
b 0
f 0
dl 0
loc 373
rs 8.6

22 Methods

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