Completed
Pull Request — master (#2147)
by Lasse
01:45
created

Bear.log_message()   A

Complexity

Conditions 2

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 2
dl 0
loc 3
rs 10
1
import traceback
2
from os import makedirs
3
from os.path import join, abspath, isdir, 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
    Every bear has a data directory which is unique to that particular bear:
51
52
    >>> class SomeBear(Bear): pass
53
    >>> class SomeOtherBear(Bear): pass
54
    >>> SomeBear.data_dir == SomeOtherBear.data_dir
55
    False
56
    """
57
58
    LANGUAGES = ()
59
60
    @classproperty
61
    def name(cls):
62
        """
63
        :return: The name of the bear
64
        """
65
        return cls.__name__
66
67
    @classproperty
68
    def supported_languages(cls):
69
        """
70
        :return: The languages supported by the bear.
71
        """
72
        return (cls.LANGUAGES if isinstance(
73
            cls.LANGUAGES, tuple) else (cls.LANGUAGES,))
74
75
    @enforce_signature
76
    def __init__(self,
77
                 section: Section,
78
                 message_queue,
79
                 timeout=0):
80
        """
81
        Constructs a new bear.
82
83
        :param section:       The section object where bear settings are
84
                              contained.
85
        :param message_queue: The queue object for messages. Can be ``None``.
86
        :param timeout:       The time the bear is allowed to run. To set no
87
                              time limit, use 0.
88
        :raises TypeError:    Raised when ``message_queue`` is no queue.
89
        :raises RuntimeError: Raised when bear requirements are not fulfilled.
90
        """
91
        Printer.__init__(self)
92
        LogPrinter.__init__(self, self)
93
94
        if message_queue is not None and not hasattr(message_queue, "put"):
95
            raise TypeError("message_queue has to be a Queue or None.")
96
97
        self.section = section
98
        self.message_queue = message_queue
99
        self.timeout = timeout
100
101
        self.setup_dependencies()
102
        cp = type(self).check_prerequisites()
103
        if cp is not True:
104
            error_string = ("The bear " + self.name +
105
                            " does not fulfill all requirements.")
106
            if cp is not False:
107
                error_string += " " + cp
108
109
            self.warn(error_string)
110
            raise RuntimeError(error_string)
111
112
    def _print(self, output, **kwargs):
113
        self.debug(output)
114
115
    def log_message(self, log_message, timestamp=None, **kwargs):
116
        if self.message_queue is not None:
117
            self.message_queue.put(log_message)
118
119
    def run(self, *args, dependency_results=None, **kwargs):
120
        raise NotImplementedError
121
122
    def run_bear_from_section(self, args, kwargs):
123
        try:
124
            kwargs.update(
125
                self.get_metadata().create_params_from_section(self.section))
126
        except ValueError as err:
127
            self.warn("The bear {} cannot be executed.".format(
128
                self.name), str(err))
129
            return
130
131
        return self.run(*args, **kwargs)
132
133
    def execute(self, *args, **kwargs):
134
        name = self.name
135
        try:
136
            self.debug("Running bear {}...".format(name))
137
            # If it's already a list it won't change it
138
            return list(self.run_bear_from_section(args, kwargs) or [])
139
        except:
140
            self.warn(
141
                "Bear {} failed to run. Take a look at debug messages for "
142
                "further information.".format(name))
143
            self.debug(
144
                "The bear {bear} raised an exception. If you are the writer "
145
                "of this bear, please make sure to catch all exceptions. If "
146
                "not and this error annoys you, you might want to get in "
147
                "contact with the writer of this bear.\n\nTraceback "
148
                "information is provided below:\n\n{traceback}"
149
                "\n".format(bear=name, traceback=traceback.format_exc()))
150
151
    @staticmethod
152
    def kind():
153
        """
154
        :return: The kind of the bear
155
        """
156
        raise NotImplementedError
157
158
    @classmethod
159
    def get_metadata(cls):
160
        """
161
        :return: Metadata for the run function. However parameters like
162
                 ``self`` or parameters implicitly used by coala (e.g.
163
                 filename for local bears) are already removed.
164
        """
165
        return FunctionMetadata.from_function(
166
            cls.run,
167
            omit={"self", "dependency_results"})
168
169
    @classmethod
170
    def missing_dependencies(cls, lst):
171
        """
172
        Checks if the given list contains all dependencies.
173
174
        :param lst: A list of all already resolved bear classes (not
175
                    instances).
176
        :return:    A list of missing dependencies.
177
        """
178
        dep_classes = cls.get_dependencies()
179
180
        for item in lst:
181
            if item in dep_classes:
182
                dep_classes.remove(item)
183
184
        return dep_classes
185
186
    @staticmethod
187
    def get_dependencies():
188
        """
189
        Retrieves bear classes that are to be executed before this bear gets
190
        executed. The results of these bears will then be passed to the
191
        run method as a dict via the dependency_results argument. The dict
192
        will have the name of the Bear as key and the list of its results as
193
        results.
194
195
        :return: A list of bear classes.
196
        """
197
        return []
198
199
    @classmethod
200
    def get_non_optional_settings(cls):
201
        """
202
        This method has to determine which settings are needed by this bear.
203
        The user will be prompted for needed settings that are not available
204
        in the settings file so don't include settings where a default value
205
        would do.
206
207
        :return: A dictionary of needed settings as keys and a tuple of help
208
                 text and annotation as values
209
        """
210
        return cls.get_metadata().non_optional_params
211
212
    @staticmethod
213
    def setup_dependencies():
214
        """
215
        This is a user defined function that can download and set up
216
        dependencies (via download_cached_file or arbitary other means) in an OS
217
        independent way.
218
        """
219
220
    @classmethod
221
    def check_prerequisites(cls):
222
        """
223
        Checks whether needed runtime prerequisites of the bear are satisfied.
224
225
        This function gets executed at construction and returns True by
226
        default.
227
228
        Section value requirements shall be checked inside the ``run`` method.
229
230
        :return: True if prerequisites are satisfied, else False or a string
231
                 that serves a more detailed description of what's missing.
232
        """
233
        return True
234
235
    def get_config_dir(self):
236
        """
237
        Gives the directory where the configuration file is
238
239
        :return: Directory of the config file
240
        """
241
        return get_config_directory(self.section)
242
243
    def download_cached_file(self, url, filename):
244
        """
245
        Downloads the file if needed and caches it for the next time. If a
246
        download happens, the user will be informed.
247
248
        Take a sane simple bear:
249
250
        >>> from queue import Queue
251
        >>> bear = Bear(Section("a section"), Queue())
252
253
        We can now carelessly query for a neat file that doesn't exist yet:
254
255
        >>> from os import remove
256
        >>> if exists(join(bear.data_dir, "a_file")):
257
        ...     remove(join(bear.data_dir, "a_file"))
258
        >>> file = bear.download_cached_file("http://gitmate.com/", "a_file")
259
260
        If we download it again, it'll be much faster as no download occurs:
261
262
        >>> newfile = bear.download_cached_file("http://gitmate.com/", "a_file")
263
        >>> newfile == file
264
        True
265
266
        :param url: The URL to download the file from.
267
        :param filename: The filename it should get, e.g. "test.txt".
268
        :return: A full path to the file ready for you to use!
269
        """
270
        filename = join(self.data_dir, filename)
271
        if exists(filename):
272
            return filename
273
274
        self.info("Downloading {filename!r} for bear {bearname} from {url}."
275
                  .format(filename=filename, bearname=self.name, url=url))
276
277
        with urlopen(url) as response, open(filename, 'wb') as out_file:
278
            copyfileobj(response, out_file)
279
        return filename
280
281
    @classproperty
282
    def data_dir(cls):
283
        """
284
        Returns a directory that may be used by the bear to store stuff. Every
285
        bear has an own directory dependent on his name.
286
        """
287
        data_dir = abspath(join(user_data_dir('coala-bears'), cls.name))
288
289
        if not isdir(data_dir):  # pragma: no cover
290
            makedirs(data_dir)
291
        return data_dir
292