Completed
Pull Request — master (#2592)
by Udayan
02:37
created

Bear.get_non_optional_settings()   A

Complexity

Conditions 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
1
import traceback
2
from functools import partial
3
from os import makedirs
4
from os.path import join, abspath, exists
5
from shutil import copyfileobj
6
from urllib.request import urlopen
7
8
from appdirs import user_data_dir
9
10
from pyprint.Printer import Printer
11
12
from coala_utils.decorators import (enforce_signature, classproperty,
13
                                    get_public_members)
14
15
from coalib.bears.requirements.PackageRequirement import PackageRequirement
0 ignored issues
show
Unused Code introduced by
Unused PackageRequirement imported from coalib.bears.requirements.PackageRequirement
Loading history...
16
from coalib.bears.requirements.PipRequirement import PipRequirement
0 ignored issues
show
Unused Code introduced by
Unused PipRequirement imported from coalib.bears.requirements.PipRequirement
Loading history...
17
from coalib.output.printers.LogPrinter import LogPrinter
18
from coalib.results.Result import Result
19
from coalib.settings.FunctionMetadata import FunctionMetadata
20
from coalib.settings.Section import Section
21
from coalib.settings.ConfigurationGathering import get_config_directory
22
23
24
class Bear(Printer, LogPrinter):
25
    """
26
    A bear contains the actual subroutine that is responsible for checking
27
    source code for certain specifications. However it can actually do
28
    whatever it wants with the files it gets. If you are missing some Result
29
    type, feel free to contact us and/or help us extending the coalib.
30
31
    This is the base class for every bear. If you want to write an bear, you
32
    will probably want to look at the GlobalBear and LocalBear classes that
33
    inherit from this class. In any case you'll want to overwrite at least the
34
    run method. You can send debug/warning/error messages through the
35
    debug(), warn(), err() functions. These will send the
36
    appropriate messages so that they are outputted. Be aware that if you use
37
    err(), you are expected to also terminate the bear run-through
38
    immediately.
39
40
    If you need some setup or teardown for your bear, feel free to overwrite
41
    the set_up() and tear_down() functions. They will be invoked
42
    before/after every run invocation.
43
44
    Settings are available at all times through self.section.
45
46
    To indicate which languages your bear supports, just give it the
47
    ``LANGUAGES`` value which should be a set of string(s):
48
49
    >>> class SomeBear(Bear):
50
    ...     LANGUAGES = {'C', 'CPP','C#', 'D'}
51
52
    To indicate the requirements of the bear, assign ``REQUIREMENTS`` a set
53
    with instances of ``PackageRequirements``.
54
55
    >>> class SomeBear(Bear):
56
    ...     REQUIREMENTS = {
57
    ...         PackageRequirement('pip', 'coala_decorators', '0.2.1')}
58
59
    If your bear uses requirements from a manager we have a subclass from,
60
    you can use the subclass, such as ``PipRequirement``, without specifying
61
    manager:
62
63
    >>> class SomeBear(Bear):
64
    ...     REQUIREMENTS = {PipRequirement('coala_decorators', '0.2.1')}
65
66
    To specify multiple requirements using ``pip``, you can use the multiple
67
    method. This can receive both tuples of strings, in case you want a specific
68
    version, or a simple string, in case you want the latest version to be
69
    specified.
70
71
    >>> class SomeBear(Bear):
72
    ...     REQUIREMENTS = PipRequirement.multiple(
73
    ...         ('colorama', '0.1'), 'coala_decorators')
74
75
    To specify additional attributes to your bear, use the following:
76
77
    >>> class SomeBear(Bear):
78
    ...     AUTHORS = {'Jon Snow'}
79
    ...     AUTHORS_EMAILS = {'[email protected]'}
80
    ...     MAINTAINERS = {'Catelyn Stark'}
81
    ...     MAINTAINERS_EMAILS = {'[email protected]'}
82
    ...     LICENSE = 'AGPL-3.0'
83
    ...     ASCIINEMA_URL = 'https://asciinema.org/a/80761'
84
85
    If the maintainers are the same as the authors, they can be omitted:
86
87
    >>> class SomeBear(Bear):
88
    ...     AUTHORS = {'Jon Snow'}
89
    ...     AUTHORS_EMAILS = {'[email protected]'}
90
    >>> SomeBear.maintainers
91
    {'Jon Snow'}
92
    >>> SomeBear.maintainers_emails
93
    {'[email protected]'}
94
95
    If your bear needs to include local files, then specify it giving strings
96
    containing relative file paths to the INCLUDE_LOCAL_FILES set:
97
98
    >>> class SomeBear(Bear):
99
    ...     INCLUDE_LOCAL_FILES = {'checkstyle.jar', 'google_checks.xml'}
100
101
    To keep track easier of what a bear can do, simply tell it to the CAN_FIX
102
    and the CAN_DETECT sets. Possible values:
103
104
    >>> CAN_DETECT = {'Syntax', 'Formatting', 'Security', 'Complexity', 'Smell',
105
    ... 'Unused Code', 'Redundancy', 'Variable Misuse', 'Spelling',
106
    ... 'Memory Leak', 'Documentation', 'Duplication', 'Commented Code',
107
    ... 'Grammar', 'Missing Import', 'Unreachable Code', 'Undefined Element',
108
    ... 'Code Simplification'}
109
    >>> CAN_FIX = {'Syntax', ...}
110
111
    Specifying something to CAN_FIX makes it obvious that it can be detected
112
    too, so it may be omitted:
113
114
    >>> class SomeBear(Bear):
115
    ...     CAN_DETECT = {'Syntax', 'Security'}
116
    ...     CAN_FIX = {'Redundancy'}
117
    >>> list(sorted(SomeBear.can_detect))
118
    ['Redundancy', 'Security', 'Syntax']
119
120
    Every bear has a data directory which is unique to that particular bear:
121
122
    >>> class SomeBear(Bear): pass
123
    >>> class SomeOtherBear(Bear): pass
124
    >>> SomeBear.data_dir == SomeOtherBear.data_dir
125
    False
126
127
    BEAR_DEPS contains bear classes that are to be executed before this bear
128
    gets executed. The results of these bears will then be passed to the
129
    run method as a dict via the dependency_results argument. The dict
130
    will have the name of the Bear as key and the list of its results as
131
    results:
132
133
    >>> class SomeBear(Bear): pass
134
    >>> class SomeOtherBear(Bear):
135
    ...     BEAR_DEPS = {SomeBear}
136
    >>> SomeOtherBear.BEAR_DEPS
137
    {<class 'coalib.bears.Bear.SomeBear'>}
138
    """
139
140
    LANGUAGES = set()
141
    REQUIREMENTS = set()
142
    AUTHORS = set()
143
    AUTHORS_EMAILS = set()
144
    MAINTAINERS = set()
145
    MAINTAINERS_EMAILS = set()
146
    PLATFORMS = {'any'}
147
    LICENSE = ''
148
    INCLUDE_LOCAL_FILES = set()
149
    CAN_DETECT = set()
150
    CAN_FIX = set()
151
    ASCIINEMA_URL = ''
152
    BEAR_DEPS = set()
153
154
    @classproperty
155
    def name(cls):
156
        """
157
        :return: The name of the bear
158
        """
159
        return cls.__name__
160
161
    @classproperty
162
    def can_detect(cls):
163
        """
164
        :return: A set that contains everything a bear can detect, gathering
165
                 information from what it can fix too.
166
        """
167
        return cls.CAN_DETECT | cls.CAN_FIX
168
169
    @classproperty
170
    def maintainers(cls):
171
        """
172
        :return: A set containing ``MAINTAINERS`` if specified, else takes
173
                 ``AUTHORS`` by default.
174
        """
175
        return cls.AUTHORS if cls.MAINTAINERS == set() else cls.MAINTAINERS
176
177
    @classproperty
178
    def maintainers_emails(cls):
179
        """
180
        :return: A set containing ``MAINTAINERS_EMAILS`` if specified, else
181
                 takes ``AUTHORS_EMAILS`` by default.
182
        """
183
        return (cls.AUTHORS_EMAILS if cls.MAINTAINERS_EMAILS == set()
184
                else cls.MAINTAINERS)
185
186
    @enforce_signature
187
    def __init__(self,
188
                 section: Section,
189
                 message_queue,
190
                 timeout=0):
191
        """
192
        Constructs a new bear.
193
194
        :param section:       The section object where bear settings are
195
                              contained.
196
        :param message_queue: The queue object for messages. Can be ``None``.
197
        :param timeout:       The time the bear is allowed to run. To set no
198
                              time limit, use 0.
199
        :raises TypeError:    Raised when ``message_queue`` is no queue.
200
        :raises RuntimeError: Raised when bear requirements are not fulfilled.
201
        """
202
        Printer.__init__(self)
203
        LogPrinter.__init__(self, self)
204
205
        if message_queue is not None and not hasattr(message_queue, "put"):
206
            raise TypeError("message_queue has to be a Queue or None.")
207
208
        self.section = section
209
        self.message_queue = message_queue
210
        self.timeout = timeout
211
212
        self.setup_dependencies()
213
        cp = type(self).check_prerequisites()
214
        if cp is not True:
215
            error_string = ("The bear " + self.name +
216
                            " does not fulfill all requirements.")
217
            if cp is not False:
218
                error_string += " " + cp
219
220
            self.warn(error_string)
221
            raise RuntimeError(error_string)
222
223
    def _print(self, output, **kwargs):
224
        self.debug(output)
225
226
    def log_message(self, log_message, timestamp=None, **kwargs):
227
        if self.message_queue is not None:
228
            self.message_queue.put(log_message)
229
230
    def run(self, *args, dependency_results=None, **kwargs):
231
        raise NotImplementedError
232
233
    def run_bear_from_section(self, args, kwargs):
234
        try:
235
            kwargs.update(
236
                self.get_metadata().create_params_from_section(self.section))
237
        except ValueError as err:
238
            self.warn("The bear {} cannot be executed.".format(
239
                self.name), str(err))
240
            return
241
242
        return self.run(*args, **kwargs)
243
244
    def execute(self, *args, **kwargs):
245
        name = self.name
246
        try:
247
            self.debug("Running bear {}...".format(name))
248
            # If it's already a list it won't change it
249
            result = self.run_bear_from_section(args, kwargs)
250
            return [] if result is None else list(result)
251
        except:
252
            self.warn(
253
                "Bear {} failed to run. Take a look at debug messages (`-L "
254
                "DEBUG`) for further information.".format(name))
255
            self.debug(
256
                "The bear {bear} raised an exception. If you are the writer "
257
                "of this bear, please make sure to catch all exceptions. If "
258
                "not and this error annoys you, you might want to get in "
259
                "contact with the writer of this bear.\n\nTraceback "
260
                "information is provided below:\n\n{traceback}"
261
                "\n".format(bear=name, traceback=traceback.format_exc()))
262
263
    @staticmethod
264
    def kind():
265
        """
266
        :return: The kind of the bear
267
        """
268
        raise NotImplementedError
269
270
    @classmethod
271
    def get_metadata(cls):
272
        """
273
        :return: Metadata for the run function. However parameters like
274
                 ``self`` or parameters implicitly used by coala (e.g.
275
                 filename for local bears) are already removed.
276
        """
277
        return FunctionMetadata.from_function(
278
            cls.run,
279
            omit={"self", "dependency_results"})
280
281
    @classmethod
282
    def __json__(cls):
283
        """
284
        Override JSON export of ``Bear`` object.
285
        """
286
        _dict = get_public_members(cls)
287
        metadata = cls.get_metadata()
288
        non_optional_params = metadata.non_optional_params
289
        optional_params = metadata.optional_params
290
        _dict["metadata"] = {
291
            "desc": metadata.desc,
292
            "non_optional_params": ({param: non_optional_params[param][0]}
293
                                    for param in non_optional_params),
294
            "optional_params": ({param: optional_params[param][0]}
295
                                for param in optional_params)}
296
297
        # Delete attributes that cannot be serialized
298
        unserializable_attributes = ["new_result", "printer"]
299
        for attribute in unserializable_attributes:
300
            _dict.pop(attribute, None)
301
        return _dict
302
303
    @classmethod
304
    def missing_dependencies(cls, lst):
305
        """
306
        Checks if the given list contains all dependencies.
307
308
        :param lst: A list of all already resolved bear classes (not
309
                    instances).
310
        :return:    A set of missing dependencies.
311
        """
312
        dep_classes = cls.BEAR_DEPS.copy()
313
314
        for item in lst:
315
            if item in dep_classes:
316
                dep_classes.remove(item)
317
318
        return dep_classes
319
320
    @classmethod
321
    def get_non_optional_settings(cls):
322
        """
323
        This method has to determine which settings are needed by this bear.
324
        The user will be prompted for needed settings that are not available
325
        in the settings file so don't include settings where a default value
326
        would do.
327
328
        :return: A dictionary of needed settings as keys and a tuple of help
329
                 text and annotation as values
330
        """
331
        return cls.get_metadata().non_optional_params
332
333
    @staticmethod
334
    def setup_dependencies():
335
        """
336
        This is a user defined function that can download and set up
337
        dependencies (via download_cached_file or arbitary other means) in an OS
338
        independent way.
339
        """
340
341
    @classmethod
342
    def check_prerequisites(cls):
343
        """
344
        Checks whether needed runtime prerequisites of the bear are satisfied.
345
346
        This function gets executed at construction and returns True by
347
        default.
348
349
        Section value requirements shall be checked inside the ``run`` method.
350
351
        :return: True if prerequisites are satisfied, else False or a string
352
                 that serves a more detailed description of what's missing.
353
        """
354
        return True
355
356
    def get_config_dir(self):
357
        """
358
        Gives the directory where the configuration file is
359
360
        :return: Directory of the config file
361
        """
362
        return get_config_directory(self.section)
363
364
    def download_cached_file(self, url, filename):
365
        """
366
        Downloads the file if needed and caches it for the next time. If a
367
        download happens, the user will be informed.
368
369
        Take a sane simple bear:
370
371
        >>> from queue import Queue
372
        >>> bear = Bear(Section("a section"), Queue())
373
374
        We can now carelessly query for a neat file that doesn't exist yet:
375
376
        >>> from os import remove
377
        >>> if exists(join(bear.data_dir, "a_file")):
378
        ...     remove(join(bear.data_dir, "a_file"))
379
        >>> file = bear.download_cached_file("http://gitmate.com/", "a_file")
380
381
        If we download it again, it'll be much faster as no download occurs:
382
383
        >>> newfile = bear.download_cached_file("http://gitmate.com/", "a_file")
384
        >>> newfile == file
385
        True
386
387
        :param url:      The URL to download the file from.
388
        :param filename: The filename it should get, e.g. "test.txt".
389
        :return:         A full path to the file ready for you to use!
390
        """
391
        filename = join(self.data_dir, filename)
392
        if exists(filename):
393
            return filename
394
395
        self.info("Downloading {filename!r} for bear {bearname} from {url}."
396
                  .format(filename=filename, bearname=self.name, url=url))
397
398
        with urlopen(url) as response, open(filename, 'wb') as out_file:
399
            copyfileobj(response, out_file)
400
        return filename
401
402
    @classproperty
403
    def data_dir(cls):
404
        """
405
        Returns a directory that may be used by the bear to store stuff. Every
406
        bear has an own directory dependent on their name.
407
        """
408
        data_dir = abspath(join(user_data_dir('coala-bears'), cls.name))
409
410
        makedirs(data_dir, exist_ok=True)
411
        return data_dir
412
413
    @property
414
    def new_result(self):
415
        """
416
        Returns a partial for creating a result with this bear already bound.
417
        """
418
        return partial(Result.from_values, self)
419