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

Bear.requirements()   A

Complexity

Conditions 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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