Completed
Pull Request — master (#2342)
by Zatreanu
01:59
created

Bear   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
dl 0
loc 340
rs 10
c 7
b 0
f 0
wmc 30

19 Methods

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