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

Bear.download_cached_file()   B

Complexity

Conditions 3

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 37
rs 8.8571
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
        pass
220
221
    @classmethod
222
    def check_prerequisites(cls):
223
        """
224
        Checks whether needed runtime prerequisites of the bear are satisfied.
225
226
        This function gets executed at construction and returns True by
227
        default.
228
229
        Section value requirements shall be checked inside the ``run`` method.
230
231
        :return: True if prerequisites are satisfied, else False or a string
232
                 that serves a more detailed description of what's missing.
233
        """
234
        return True
235
236
    def get_config_dir(self):
237
        """
238
        Gives the directory where the configuration file is
239
240
        :return: Directory of the config file
241
        """
242
        return get_config_directory(self.section)
243
244
    def download_cached_file(self, url, filename):
245
        """
246
        Downloads the file if needed and caches it for the next time. If a
247
        download happens, the user will be informed.
248
249
        Take a sane simple bear:
250
251
        >>> from queue import Queue
252
        >>> bear = Bear(Section("a section"), Queue())
253
254
        We can now carelessly query for a neat file that doesn't exist yet:
255
256
        >>> from os import remove
257
        >>> if exists(join(bear.data_dir, "a_file")):
258
        ...     remove(join(bear.data_dir, "a_file"))
259
        >>> file = bear.download_cached_file("http://gitmate.com/", "a_file")
260
261
        If we download it again, it'll be much faster as no download occurs:
262
263
        >>> newfile = bear.download_cached_file("http://gitmate.com/", "a_file")
264
        >>> newfile == file
265
        True
266
267
        :param url: The URL to download the file from.
268
        :param filename: The filename it should get, e.g. "test.txt".
269
        :return: A full path to the file ready for you to use!
270
        """
271
        filename = join(self.data_dir, filename)
272
        if exists(filename):
273
            return filename
274
275
        self.info("Downloading {filename!r} for bear {bearname} from {url}."
276
                  .format(filename=filename, bearname=self.name, url=url))
277
278
        with urlopen(url) as response, open(filename, 'wb') as out_file:
279
            copyfileobj(response, out_file)
280
        return filename
281
282
    @classproperty
283
    def data_dir(cls):
284
        """
285
        Returns a directory that may be used by the bear to store stuff. Every
286
        bear has an own directory dependent on his name.
287
        """
288
        data_dir = abspath(join(user_data_dir('coala-bears'), cls.name))
289
290
        if not isdir(data_dir):  # pragma: no cover
291
            makedirs(data_dir)
292
        return data_dir
293