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

Bear.__init__()   B

Complexity

Conditions 5

Size

Total Lines 36

Duplication

Lines 0
Ratio 0 %

Importance

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