import json
import inspect
from functools import partial
from collections import OrderedDict
from coalib.bears.LocalBear import LocalBear
from coala_utils.decorators import enforce_signature
from coalib.misc.Shell import run_shell_command
from coalib.results.Result import Result
from coalib.results.SourceRange import SourceRange
from coalib.settings.FunctionMetadata import FunctionMetadata
def _prepare_options(options):
"""
Checks for illegal options and raises ValueError.
:param options:
The options dict that contains user/developer inputs.
:raises ValueError:
Raised when illegal options are specified.
"""
allowed_options = {'executable',
'settings'}
# Check for illegal superfluous options.
superfluous_options = options.keys() - allowed_options
if superfluous_options:
raise ValueError(
'Invalid keyword arguments provided: ' +
', '.join(repr(s) for s in sorted(superfluous_options)))
if 'settings' not in options:
options['settings'] = {}
def _create_wrapper(klass, options):
NoDefaultValue = object()
class ExternalBearWrapBase(LocalBear):
@staticmethod
def create_arguments():
"""
This method has to be implemented by the class that uses
the decorator in order to create the arguments needed for
the executable.
"""
return ()
@classmethod
def get_executable(cls):
"""
Returns the executable of this class.
:return:
The executable name.
"""
return options['executable']
@staticmethod
def _normalize_desc(description, setting_type,
default_value=NoDefaultValue):
"""
Normalizes the description of the parameters only if there
is none provided.
:param description:
The parameter description to be modified in case it is empty.
:param setting_type:
The type of the setting. It is needed to create the final
tuple.
:param default_value:
The default value of the setting.
:return:
A value for the OrderedDict in the ``FunctionMetadata`` object.
"""
if description == '':
description = FunctionMetadata.str_nodesc
if default_value is NoDefaultValue:
return (description, setting_type)
else:
return (description + ' ' +
FunctionMetadata.str_optional.format(default_value),
setting_type, default_value)
@classmethod
def get_non_optional_params(cls):
"""
Fetches the non_optional_params from ``options['settings']``
and also normalizes their descriptions.
:return:
An OrderedDict that is used to create a
``FunctionMetadata`` object.
"""
non_optional_params = {}
for setting_name, description in options['settings'].items():
if len(description) == 2:
non_optional_params[
setting_name] = cls._normalize_desc(description[0],
description[1])
return OrderedDict(non_optional_params)
@classmethod
def get_optional_params(cls):
"""
Fetches the optional_params from ``options['settings']``
and also normalizes their descriptions.
:return:
An OrderedDict that is used to create a
``FunctionMetadata`` object.
"""
optional_params = {}
for setting_name, description in options['settings'].items():
if len(description) == 3:
optional_params[
setting_name] = cls._normalize_desc(description[0],
description[1],
description[2])
return OrderedDict(optional_params)
@classmethod
def get_metadata(cls):
metadata = FunctionMetadata(
'run',
optional_params=cls.get_optional_params(),
non_optional_params=cls.get_non_optional_params())
metadata.desc = inspect.getdoc(cls)
return metadata
@classmethod
def _prepare_settings(cls, settings):
"""
Adds the optional settings to the settings dict in-place.
:param settings:
The settings dict.
"""
opt_params = cls.get_optional_params()
for setting_name, description in opt_params.items():
if setting_name not in settings:
settings[setting_name] = description[2]
def parse_output(self, out, filename):
"""
Parses the output JSON into Result objects.
:param out:
Raw output from the given executable (should be JSON).
:param filename:
The filename of the analyzed file. Needed to
create the Result objects.
:return:
An iterator yielding ``Result`` objects.
"""
output = json.loads(out)
for result in output['results']:
affected_code = tuple(
SourceRange.from_values(
code_range['file'],
code_range['start']['line'],
code_range['start'].get('column'),
code_range.get('end', {}).get('line'),
code_range.get('end', {}).get('column'))
for code_range in result['affected_code'])
yield Result(
origin=result['origin'],
message=result['message'],
affected_code=affected_code,
severity=result.get('severity', 1),
debug_msg=result.get('debug_msg', ''),
additional_info=result.get('additional_info', ''))
def run(self, filename, file, **settings):
self._prepare_settings(settings)
json_string = json.dumps({'filename': filename,
'file': file,
'settings': settings})
args = self.create_arguments()
try:
args = tuple(args)
except TypeError:
self.err('The given arguments '
'{!r} are not iterable.'.format(args))
return
shell_command = (self.get_executable(),) + args
out, err = run_shell_command(shell_command, json_string)
return self.parse_output(out, filename)
result_klass = type(klass.__name__, (klass, ExternalBearWrapBase), {})
result_klass.__doc__ = klass.__doc__ or ''
return result_klass
[docs]@enforce_signature
def external_bear_wrap(executable: str, **options):
options['executable'] = executable
_prepare_options(options)
return partial(_create_wrapper, options=options)