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
![]() |
|||
16 | from coalib.bears.requirements.PipRequirement import PipRequirement |
||
0 ignored issues
–
show
|
|||
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 | return set(cls.BEAR_DEPS) - set(lst) |
||
313 | |||
314 | @classmethod |
||
315 | def get_non_optional_settings(cls): |
||
316 | """ |
||
317 | This method has to determine which settings are needed by this bear. |
||
318 | The user will be prompted for needed settings that are not available |
||
319 | in the settings file so don't include settings where a default value |
||
320 | would do. |
||
321 | |||
322 | :return: A dictionary of needed settings as keys and a tuple of help |
||
323 | text and annotation as values |
||
324 | """ |
||
325 | return cls.get_metadata().non_optional_params |
||
326 | |||
327 | @staticmethod |
||
328 | def setup_dependencies(): |
||
329 | """ |
||
330 | This is a user defined function that can download and set up |
||
331 | dependencies (via download_cached_file or arbitrary other means) in an |
||
332 | OS independent way. |
||
333 | """ |
||
334 | |||
335 | @classmethod |
||
336 | def check_prerequisites(cls): |
||
337 | """ |
||
338 | Checks whether needed runtime prerequisites of the bear are satisfied. |
||
339 | |||
340 | This function gets executed at construction and returns True by |
||
341 | default. |
||
342 | |||
343 | Section value requirements shall be checked inside the ``run`` method. |
||
344 | |||
345 | :return: True if prerequisites are satisfied, else False or a string |
||
346 | that serves a more detailed description of what's missing. |
||
347 | """ |
||
348 | return True |
||
349 | |||
350 | def get_config_dir(self): |
||
351 | """ |
||
352 | Gives the directory where the configuration file is |
||
353 | |||
354 | :return: Directory of the config file |
||
355 | """ |
||
356 | return get_config_directory(self.section) |
||
357 | |||
358 | def download_cached_file(self, url, filename): |
||
359 | """ |
||
360 | Downloads the file if needed and caches it for the next time. If a |
||
361 | download happens, the user will be informed. |
||
362 | |||
363 | Take a sane simple bear: |
||
364 | |||
365 | >>> from queue import Queue |
||
366 | >>> bear = Bear(Section("a section"), Queue()) |
||
367 | |||
368 | We can now carelessly query for a neat file that doesn't exist yet: |
||
369 | |||
370 | >>> from os import remove |
||
371 | >>> if exists(join(bear.data_dir, "a_file")): |
||
372 | ... remove(join(bear.data_dir, "a_file")) |
||
373 | >>> file = bear.download_cached_file("http://gitmate.com/", "a_file") |
||
374 | |||
375 | If we download it again, it'll be much faster as no download occurs: |
||
376 | |||
377 | >>> newfile = bear.download_cached_file("http://gitmate.com/", "a_file") |
||
378 | >>> newfile == file |
||
379 | True |
||
380 | |||
381 | :param url: The URL to download the file from. |
||
382 | :param filename: The filename it should get, e.g. "test.txt". |
||
383 | :return: A full path to the file ready for you to use! |
||
384 | """ |
||
385 | filename = join(self.data_dir, filename) |
||
386 | if exists(filename): |
||
387 | return filename |
||
388 | |||
389 | self.info("Downloading {filename!r} for bear {bearname} from {url}." |
||
390 | .format(filename=filename, bearname=self.name, url=url)) |
||
391 | |||
392 | with urlopen(url) as response, open(filename, 'wb') as out_file: |
||
393 | copyfileobj(response, out_file) |
||
394 | return filename |
||
395 | |||
396 | @classproperty |
||
397 | def data_dir(cls): |
||
398 | """ |
||
399 | Returns a directory that may be used by the bear to store stuff. Every |
||
400 | bear has an own directory dependent on their name. |
||
401 | """ |
||
402 | data_dir = abspath(join(user_data_dir('coala-bears'), cls.name)) |
||
403 | |||
404 | makedirs(data_dir, exist_ok=True) |
||
405 | return data_dir |
||
406 | |||
407 | @property |
||
408 | def new_result(self): |
||
409 | """ |
||
410 | Returns a partial for creating a result with this bear already bound. |
||
411 | """ |
||
412 | return partial(Result.from_values, self) |
||
413 |