Completed
Pull Request — master (#2147)
by Lasse
03:02 queued 01:16
created

Bear.get_metadata()   A

Complexity

Conditions 1

Size

Total Lines 10

Duplication

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