Completed
Pull Request — master (#2409)
by
unknown
01:54
created

Bear.run_bear_from_section()   A

Complexity

Conditions 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
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_utils.decorators import (enforce_signature, classproperty,
13
                                    get_public_members)
14
15
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...
16
from coalib.bears.requirements.PipRequirement import PipRequirement
0 ignored issues
show
Unused Code introduced by
Unused PipRequirement imported from coalib.bears.requirements.PipRequirement
Loading history...
17
from coalib.output.printers.LogPrinter import LogPrinter
18
from coalib.results.Result import Result
19
from coalib.settings.FunctionMetadata import FunctionMetadata
20
from coalib.settings.Section import Section
21
from coalib.settings.ConfigurationGathering import get_config_directory
22
23
24
class Bear(Printer, LogPrinter):
25
    """
26
    A bear contains the actual subroutine that is responsible for checking
27
    source code for certain specifications. However it can actually do
28
    whatever it wants with the files it gets. If you are missing some Result
29
    type, feel free to contact us and/or help us extending the coalib.
30
31
    This is the base class for every bear. If you want to write an bear, you
32
    will probably want to look at the GlobalBear and LocalBear classes that
33
    inherit from this class. In any case you'll want to overwrite at least the
34
    run method. You can send debug/warning/error messages through the
35
    debug(), warn(), err() functions. These will send the
36
    appropriate messages so that they are outputted. Be aware that if you use
37
    err(), you are expected to also terminate the bear run-through
38
    immediately.
39
40
    If you need some setup or teardown for your bear, feel free to overwrite
41
    the set_up() and tear_down() functions. They will be invoked
42
    before/after every run invocation.
43
44
    Settings are available at all times through self.section.
45
46
    To indicate which languages your bear supports, just give it the
47
    ``LANGUAGES`` value which should be a set of string(s):
48
49
    >>> class SomeBear(Bear):
50
    ...     LANGUAGES = {'C', 'CPP','C#', 'D'}
51
52
    To indicate the requirements of the bear, assign ``REQUIREMENTS`` a set
53
    with instances of ``PackageRequirements``.
54
55
    >>> class SomeBear(Bear):
56
    ...     REQUIREMENTS = {
57
    ...         PackageRequirement('pip', 'coala_decorators', '0.2.1')}
58
59
    If your bear uses requirements from a manager we have a subclass from,
60
    you can use the subclass, such as ``PipRequirement``, without specifying
61
    manager:
62
63
    >>> class SomeBear(Bear):
64
    ...     REQUIREMENTS = {PipRequirement('coala_decorators', '0.2.1')}
65
66
    To specify multiple requirements using ``pip``, you can use the multiple
67
    method. This can receive both tuples of strings, in case you want a specific
68
    version, or a simple string, in case you want the latest version to be
69
    specified.
70
71
    >>> class SomeBear(Bear):
72
    ...     REQUIREMENTS = PipRequirement.multiple(
73
    ...         ('colorama', '0.1'), 'coala_decorators')
74
75
    To specify additional attributes to your bear, use the following:
76
77
    >>> class SomeBear(Bear):
78
    ...     AUTHORS = {'Jon Snow'}
79
    ...     AUTHORS_EMAILS = {'[email protected]'}
80
    ...     MAINTAINERS = {'Catelyn Stark'}
81
    ...     MAINTAINERS_EMAILS = {'[email protected]'}
82
    ...     LICENSE = 'AGPL-3.0'
83
84
    If the maintainers are the same as the authors, they can be omitted:
85
86
    >>> class SomeBear(Bear):
87
    ...     AUTHORS = {'Jon Snow'}
88
    ...     AUTHORS_EMAILS = {'[email protected]'}
89
    >>> SomeBear.maintainers
90
    {'Jon Snow'}
91
    >>> SomeBear.maintainers_emails
92
    {'[email protected]'}
93
94
    If your bear needs to include local files, then specify it giving strings
95
    containing relative file paths to the INCLUDE_LOCAL_FILES set:
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 sets. Possible values:
102
103
    >>> CAN_DETECT = {'Syntax', 'Formatting', 'Security', 'Complexity', 'Smell',
104
    ... 'Unused Code', 'Redundancy', 'Variable Misuse', 'Spelling',
105
    ... 'Memory Leak', 'Documentation', 'Duplication', 'Commented Code',
106
    ... 'Grammar', 'Missing Import', 'Unreachable Code', 'Undefined Element',
107
    ... 'Code Simplification'}
108
    >>> CAN_FIX = {'Syntax', ...}
109
110
    Specifying something to CAN_FIX makes it obvious that it can be detected
111
    too, so it may be omitted:
112
113
    >>> class SomeBear(Bear):
114
    ...     CAN_DETECT = {'Syntax', 'Security'}
115
    ...     CAN_FIX = {'Redundancy'}
116
    >>> list(sorted(SomeBear.can_detect))
117
    ['Redundancy', 'Security', 'Syntax']
118
119
    Every bear has a data directory which is unique to that particular bear:
120
121
    >>> class SomeBear(Bear): pass
122
    >>> class SomeOtherBear(Bear): pass
123
    >>> SomeBear.data_dir == SomeOtherBear.data_dir
124
    False
125
    """
126
127
    LANGUAGES = set()
128
    REQUIREMENTS = set()
129
    AUTHORS = set()
130
    AUTHORS_EMAILS = set()
131
    MAINTAINERS = set()
132
    MAINTAINERS_EMAILS = set()
133
    PLATFORMS = {'any'}
134
    LICENSE = ''
135
    INCLUDE_LOCAL_FILES = set()
136
    CAN_DETECT = set()
137
    CAN_FIX = set()
138
139
    @classproperty
140
    def name(cls):
141
        """
142
        :return: The name of the bear
143
        """
144
        return cls.__name__
145
146
    @classproperty
147
    def can_detect(cls):
148
        """
149
        :return: A set that contains everything a bear can detect, gathering
150
                 information from what it can fix too.
151
        """
152
        return cls.CAN_DETECT | cls.CAN_FIX
153
154
    @classproperty
155
    def maintainers(cls):
156
        """
157
        :return: A set containing ``MAINTAINERS`` if specified, else takes
158
                 ``AUTHORS`` by default.
159
        """
160
        return cls.AUTHORS if cls.MAINTAINERS == set() else cls.MAINTAINERS
161
162
    @classproperty
163
    def maintainers_emails(cls):
164
        """
165
        :return: A set containing ``MAINTAINERS_EMAILS`` if specified, else
166
                 takes ``AUTHORS_EMAILS`` by default.
167
        """
168
        return (cls.AUTHORS_EMAILS if cls.MAINTAINERS_EMAILS == set()
169
                else cls.MAINTAINERS)
170
171
    @enforce_signature
172
    def __init__(self,
173
                 section: Section,
174
                 message_queue,
175
                 timeout=0):
176
        """
177
        Constructs a new bear.
178
179
        :param section:       The section object where bear settings are
180
                              contained.
181
        :param message_queue: The queue object for messages. Can be ``None``.
182
        :param timeout:       The time the bear is allowed to run. To set no
183
                              time limit, use 0.
184
        :raises TypeError:    Raised when ``message_queue`` is no queue.
185
        :raises RuntimeError: Raised when bear requirements are not fulfilled.
186
        """
187
        Printer.__init__(self)
188
        LogPrinter.__init__(self, self)
189
190
        if message_queue is not None and not hasattr(message_queue, "put"):
191
            raise TypeError("message_queue has to be a Queue or None.")
192
193
        self.section = section
194
        self.message_queue = message_queue
195
        self.timeout = timeout
196
197
        self.setup_dependencies()
198
        cp = type(self).check_prerequisites()
199
        if cp is not True:
200
            error_string = ("The bear " + self.name +
201
                            " does not fulfill all requirements.")
202
            if cp is not False:
203
                error_string += " " + cp
204
205
            self.warn(error_string)
206
            raise RuntimeError(error_string)
207
208
    def _print(self, output, **kwargs):
209
        self.debug(output)
210
211
    def log_message(self, log_message, timestamp=None, **kwargs):
212
        if self.message_queue is not None:
213
            self.message_queue.put(log_message)
214
215
    def run(self, *args, dependency_results=None, **kwargs):
216
        raise NotImplementedError
217
218
    def run_bear_from_section(self, args, kwargs):
219
        try:
220
            kwargs.update(
221
                self.get_metadata().create_params_from_section(self.section))
222
        except ValueError as err:
223
            self.warn("The bear {} cannot be executed.".format(
224
                self.name), str(err))
225
            return
226
227
        return self.run(*args, **kwargs)
228
229
    def execute(self, *args, **kwargs):
230
        name = self.name
231
        try:
232
            self.debug("Running bear {}...".format(name))
233
            # If it's already a list it won't change it
234
            result = self.run_bear_from_section(args, kwargs)
235
            return [] if result is None else list(result)
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 __json__(cls):
268
        """
269
        Override JSON export of ``Bear`` object.
270
        """
271
        # json cannot serialize properties, so drop them
272
        _dict = {key: value for key, value in get_public_members(cls).items()
273
                 if not isinstance(value, property)}
274
        metadata = cls.get_metadata()
275
        non_optional_params = metadata.non_optional_params
276
        optional_params = metadata.optional_params
277
        _dict["metadata"] = {
278
            "desc": metadata.desc,
279
            "non_optional_params": ({param: non_optional_params[param][0]}
280
                                    for param in non_optional_params),
281
            "optional_params": ({param: optional_params[param][0]}
282
                                for param in optional_params)}
283
        return _dict
284
285
    @classmethod
286
    def missing_dependencies(cls, lst):
287
        """
288
        Checks if the given list contains all dependencies.
289
290
        :param lst: A list of all already resolved bear classes (not
291
                    instances).
292
        :return:    A list of missing dependencies.
293
        """
294
        dep_classes = cls.get_dependencies()
295
296
        for item in lst:
297
            if item in dep_classes:
298
                dep_classes.remove(item)
299
300
        return dep_classes
301
302
    @staticmethod
303
    def get_dependencies():
304
        """
305
        Retrieves bear classes that are to be executed before this bear gets
306
        executed. The results of these bears will then be passed to the
307
        run method as a dict via the dependency_results argument. The dict
308
        will have the name of the Bear as key and the list of its results as
309
        results.
310
311
        :return: A list of bear classes.
312
        """
313
        return []
314
315
    @classmethod
316
    def get_non_optional_settings(cls):
317
        """
318
        This method has to determine which settings are needed by this bear.
319
        The user will be prompted for needed settings that are not available
320
        in the settings file so don't include settings where a default value
321
        would do.
322
323
        :return: A dictionary of needed settings as keys and a tuple of help
324
                 text and annotation as values
325
        """
326
        return cls.get_metadata().non_optional_params
327
328
    @staticmethod
329
    def setup_dependencies():
330
        """
331
        This is a user defined function that can download and set up
332
        dependencies (via download_cached_file or arbitary other means) in an OS
333
        independent way.
334
        """
335
336
    @classmethod
337
    def check_prerequisites(cls):
338
        """
339
        Checks whether needed runtime prerequisites of the bear are satisfied.
340
341
        This function gets executed at construction and returns True by
342
        default.
343
344
        Section value requirements shall be checked inside the ``run`` method.
345
346
        :return: True if prerequisites are satisfied, else False or a string
347
                 that serves a more detailed description of what's missing.
348
        """
349
        return True
350
351
    def get_config_dir(self):
352
        """
353
        Gives the directory where the configuration file is
354
355
        :return: Directory of the config file
356
        """
357
        return get_config_directory(self.section)
358
359
    def download_cached_file(self, url, filename):
360
        """
361
        Downloads the file if needed and caches it for the next time. If a
362
        download happens, the user will be informed.
363
364
        Take a sane simple bear:
365
366
        >>> from queue import Queue
367
        >>> bear = Bear(Section("a section"), Queue())
368
369
        We can now carelessly query for a neat file that doesn't exist yet:
370
371
        >>> from os import remove
372
        >>> if exists(join(bear.data_dir, "a_file")):
373
        ...     remove(join(bear.data_dir, "a_file"))
374
        >>> file = bear.download_cached_file("http://gitmate.com/", "a_file")
375
376
        If we download it again, it'll be much faster as no download occurs:
377
378
        >>> newfile = bear.download_cached_file("http://gitmate.com/", "a_file")
379
        >>> newfile == file
380
        True
381
382
        :param url:      The URL to download the file from.
383
        :param filename: The filename it should get, e.g. "test.txt".
384
        :return:         A full path to the file ready for you to use!
385
        """
386
        filename = join(self.data_dir, filename)
387
        if exists(filename):
388
            return filename
389
390
        self.info("Downloading {filename!r} for bear {bearname} from {url}."
391
                  .format(filename=filename, bearname=self.name, url=url))
392
393
        with urlopen(url) as response, open(filename, 'wb') as out_file:
394
            copyfileobj(response, out_file)
395
        return filename
396
397
    @classproperty
398
    def data_dir(cls):
399
        """
400
        Returns a directory that may be used by the bear to store stuff. Every
401
        bear has an own directory dependent on their name.
402
        """
403
        data_dir = abspath(join(user_data_dir('coala-bears'), cls.name))
404
405
        makedirs(data_dir, exist_ok=True)
406
        return data_dir
407
408
    @property
409
    def new_result(self):
410
        """
411
        Returns a partial for creating a result with this bear already bound.
412
        """
413
        return partial(Result.from_values, self)
414