Completed
Pull Request — master (#2353)
by Zatreanu
02:00
created

Bear.get_dependencies()   A

Complexity

Conditions 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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