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

Bear.get_metadata()   A

Complexity

Conditions 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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