Completed
Pull Request — master (#2166)
by Zatreanu
01:46
created

Bear.kind()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 1
dl 0
loc 6
rs 9.4285
1
import traceback
2
from os import makedirs
3
from os.path import join, abspath, exists
4
from shutil import copyfileobj
5
from urllib.request import urlopen
6
7
from appdirs import user_data_dir
8
9
from pyprint.Printer import Printer
10
11
from coalib.misc.Decorators import enforce_signature, classproperty
12
from coalib.output.printers.LogPrinter import LogPrinter
13
from coalib.settings.FunctionMetadata import FunctionMetadata
14
from coalib.settings.Section import Section
15
from coalib.settings.ConfigurationGathering import get_config_directory
16
17
18
class Bear(Printer, LogPrinter):
19
    """
20
    A bear contains the actual subroutine that is responsible for checking
21
    source code for certain specifications. However it can actually do
22
    whatever it wants with the files it gets. If you are missing some Result
23
    type, feel free to contact us and/or help us extending the coalib.
24
25
    This is the base class for every bear. If you want to write an bear, you
26
    will probably want to look at the GlobalBear and LocalBear classes that
27
    inherit from this class. In any case you'll want to overwrite at least the
28
    run method. You can send debug/warning/error messages through the
29
    debug(), warn(), err() functions. These will send the
30
    appropriate messages so that they are outputted. Be aware that if you use
31
    err(), you are expected to also terminate the bear run-through
32
    immediately.
33
34
    If you need some setup or teardown for your bear, feel free to overwrite
35
    the set_up() and tear_down() functions. They will be invoked
36
    before/after every run invocation.
37
38
    Settings are available at all times through self.section.
39
40
    To indicate which languages your bear supports, just give it the
41
    ``LANGUAGES`` value which can either be a string (if the bear supports
42
    only 1 language) or a tuple of strings:
43
44
    >>> class SomeBear(Bear):
45
    ...     LANGUAGES = ('C', 'CPP','C#', 'D')
46
47
    >>> class SomeBear(Bear):
48
    ...     LANGUAGES = "Java"
49
50
    To see what requirements the bear has, give the bear a value for
51
    ``REQUIREMENTS``, which will be a tuple of strongs:
52
53
    >>> class SomeBear(Bear):
54
    ...     REQUIREMENTS = ('yamllint~=1.0.1')
55
56
    Every bear has a data directory which is unique to that particular bear:
57
58
    >>> class SomeBear(Bear): pass
59
    >>> class SomeOtherBear(Bear): pass
60
    >>> SomeBear.data_dir == SomeOtherBear.data_dir
61
    False
62
    """
63
64
    LANGUAGES = ()
65
66
    REQUIREMENTS = ()
67
68
    @classproperty
69
    def name(cls):
70
        """
71
        :return: The name of the bear
72
        """
73
        return cls.__name__
74
75
    @classproperty
76
    def supported_languages(cls):
77
        """
78
        :return: The languages supported by the bear.
79
        """
80
        return (cls.LANGUAGES if isinstance(
81
            cls.LANGUAGES, tuple) else (cls.LANGUAGES,))
82
83
    @classproperty
84
    def requirements(cls):
85
        """
86
        :return: The requirements needed by the bear.
87
        """
88
        return cls.REQUIREMENTS
89
90
    @enforce_signature
91
    def __init__(self,
92
                 section: Section,
93
                 message_queue,
94
                 timeout=0):
95
        """
96
        Constructs a new bear.
97
98
        :param section:       The section object where bear settings are
99
                              contained.
100
        :param message_queue: The queue object for messages. Can be ``None``.
101
        :param timeout:       The time the bear is allowed to run. To set no
102
                              time limit, use 0.
103
        :raises TypeError:    Raised when ``message_queue`` is no queue.
104
        :raises RuntimeError: Raised when bear requirements are not fulfilled.
105
        """
106
        Printer.__init__(self)
107
        LogPrinter.__init__(self, self)
108
109
        if message_queue is not None and not hasattr(message_queue, "put"):
110
            raise TypeError("message_queue has to be a Queue or None.")
111
112
        self.section = section
113
        self.message_queue = message_queue
114
        self.timeout = timeout
115
116
        self.setup_dependencies()
117
        cp = type(self).check_prerequisites()
118
        if cp is not True:
119
            error_string = ("The bear " + self.name +
120
                            " does not fulfill all requirements.")
121
            if cp is not False:
122
                error_string += " " + cp
123
124
            self.warn(error_string)
125
            raise RuntimeError(error_string)
126
127
    def _print(self, output, **kwargs):
128
        self.debug(output)
129
130
    def log_message(self, log_message, timestamp=None, **kwargs):
131
        if self.message_queue is not None:
132
            self.message_queue.put(log_message)
133
134
    def run(self, *args, dependency_results=None, **kwargs):
135
        raise NotImplementedError
136
137
    def run_bear_from_section(self, args, kwargs):
138
        try:
139
            kwargs.update(
140
                self.get_metadata().create_params_from_section(self.section))
141
        except ValueError as err:
142
            self.warn("The bear {} cannot be executed.".format(
143
                self.name), str(err))
144
            return
145
146
        return self.run(*args, **kwargs)
147
148
    def execute(self, *args, **kwargs):
149
        name = self.name
150
        try:
151
            self.debug("Running bear {}...".format(name))
152
            # If it's already a list it won't change it
153
            return list(self.run_bear_from_section(args, kwargs) or [])
154
        except:
155
            self.warn(
156
                "Bear {} failed to run. Take a look at debug messages for "
157
                "further information.".format(name))
158
            self.debug(
159
                "The bear {bear} raised an exception. If you are the writer "
160
                "of this bear, please make sure to catch all exceptions. If "
161
                "not and this error annoys you, you might want to get in "
162
                "contact with the writer of this bear.\n\nTraceback "
163
                "information is provided below:\n\n{traceback}"
164
                "\n".format(bear=name, traceback=traceback.format_exc()))
165
166
    @staticmethod
167
    def kind():
168
        """
169
        :return: The kind of the bear
170
        """
171
        raise NotImplementedError
172
173
    @classmethod
174
    def get_metadata(cls):
175
        """
176
        :return: Metadata for the run function. However parameters like
177
                 ``self`` or parameters implicitly used by coala (e.g.
178
                 filename for local bears) are already removed.
179
        """
180
        return FunctionMetadata.from_function(
181
            cls.run,
182
            omit={"self", "dependency_results"})
183
184
    @classmethod
185
    def missing_dependencies(cls, lst):
186
        """
187
        Checks if the given list contains all dependencies.
188
189
        :param lst: A list of all already resolved bear classes (not
190
                    instances).
191
        :return:    A list of missing dependencies.
192
        """
193
        dep_classes = cls.get_dependencies()
194
195
        for item in lst:
196
            if item in dep_classes:
197
                dep_classes.remove(item)
198
199
        return dep_classes
200
201
    @staticmethod
202
    def get_dependencies():
203
        """
204
        Retrieves bear classes that are to be executed before this bear gets
205
        executed. The results of these bears will then be passed to the
206
        run method as a dict via the dependency_results argument. The dict
207
        will have the name of the Bear as key and the list of its results as
208
        results.
209
210
        :return: A list of bear classes.
211
        """
212
        return []
213
214
    @classmethod
215
    def get_non_optional_settings(cls):
216
        """
217
        This method has to determine which settings are needed by this bear.
218
        The user will be prompted for needed settings that are not available
219
        in the settings file so don't include settings where a default value
220
        would do.
221
222
        :return: A dictionary of needed settings as keys and a tuple of help
223
                 text and annotation as values
224
        """
225
        return cls.get_metadata().non_optional_params
226
227
    @staticmethod
228
    def setup_dependencies():
229
        """
230
        This is a user defined function that can download and set up
231
        dependencies (via download_cached_file or arbitary other means) in an OS
232
        independent way.
233
        """
234
235
    @classmethod
236
    def check_prerequisites(cls):
237
        """
238
        Checks whether needed runtime prerequisites of the bear are satisfied.
239
240
        This function gets executed at construction and returns True by
241
        default.
242
243
        Section value requirements shall be checked inside the ``run`` method.
244
245
        :return: True if prerequisites are satisfied, else False or a string
246
                 that serves a more detailed description of what's missing.
247
        """
248
        return True
249
250
    def get_config_dir(self):
251
        """
252
        Gives the directory where the configuration file is
253
254
        :return: Directory of the config file
255
        """
256
        return get_config_directory(self.section)
257
258
    def download_cached_file(self, url, filename):
259
        """
260
        Downloads the file if needed and caches it for the next time. If a
261
        download happens, the user will be informed.
262
263
        Take a sane simple bear:
264
265
        >>> from queue import Queue
266
        >>> bear = Bear(Section("a section"), Queue())
267
268
        We can now carelessly query for a neat file that doesn't exist yet:
269
270
        >>> from os import remove
271
        >>> if exists(join(bear.data_dir, "a_file")):
272
        ...     remove(join(bear.data_dir, "a_file"))
273
        >>> file = bear.download_cached_file("http://gitmate.com/", "a_file")
274
275
        If we download it again, it'll be much faster as no download occurs:
276
277
        >>> newfile = bear.download_cached_file("http://gitmate.com/", "a_file")
278
        >>> newfile == file
279
        True
280
281
        :param url:      The URL to download the file from.
282
        :param filename: The filename it should get, e.g. "test.txt".
283
        :return:         A full path to the file ready for you to use!
284
        """
285
        filename = join(self.data_dir, filename)
286
        if exists(filename):
287
            return filename
288
289
        self.info("Downloading {filename!r} for bear {bearname} from {url}."
290
                  .format(filename=filename, bearname=self.name, url=url))
291
292
        with urlopen(url) as response, open(filename, 'wb') as out_file:
293
            copyfileobj(response, out_file)
294
        return filename
295
296
    @classproperty
297
    def data_dir(cls):
298
        """
299
        Returns a directory that may be used by the bear to store stuff. Every
300
        bear has an own directory dependent on his name.
301
        """
302
        data_dir = abspath(join(user_data_dir('coala-bears'), cls.name))
303
304
        makedirs(data_dir, exist_ok=True)
305
        return data_dir
306