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