Files
sublime-text-3/Packages/SublimeLinter/lint/python_linter.py

326 lines
10 KiB
Python

#
# python_linter.py
# Part of SublimeLinter3, a code checking framework for Sublime Text 3
#
# Written by Aparajita Fishman
#
# Project: https://github.com/SublimeLinter/SublimeLinter3
# License: MIT
#
"""This module exports the PythonLinter subclass of Linter."""
import importlib
import os
import re
from . import linter, persist, util
class PythonLinter(linter.Linter):
"""
This Linter subclass provides python-specific functionality.
Linters that check python should inherit from this class.
By doing so, they automatically get the following features:
- comment_re is defined correctly for python.
- A python shebang is returned as the @python:<version> meta setting.
- Execution directly via a module method or via an executable.
If the module attribute is defined and is successfully imported,
whether it is used depends on the following algorithm:
- If the cmd attribute specifies @python and ST's python
satisfies that version, the module will be used. Note that this
check is done during class construction.
- If the check_version attribute is False, the module will be used
because the module is not version-sensitive.
- If the "@python" setting is set and ST's python satisfies
that version, the module will be used.
- Otherwise the executable will be used with the python specified
in the "@python" setting, the cmd attribute, or the default system
python.
"""
SHEBANG_RE = re.compile(r'\s*#!(?:(?:/[^/]+)*[/ ])?python(?P<version>\d(?:\.\d)?)')
comment_re = r'\s*#'
# If the linter wants to import a module and run a method directly,
# it should set this attribute to the module name, suitable for passing
# to importlib.import_module. During class construction, the named module
# will be imported, and if successful, the attribute will be replaced
# with the imported module.
module = None
# Some python-based linters are version-sensitive, i.e. the python version
# they are run with has to match the version of the code they lint.
# If a linter is version-sensitive, this attribute should be set to True.
check_version = False
@staticmethod
def match_shebang(code):
"""Convert and return a python shebang as a @python:<version> setting."""
match = PythonLinter.SHEBANG_RE.match(code)
if match:
return '@python', match.group('version')
else:
return None
shebang_match = match_shebang
@classmethod
def initialize(cls):
"""Perform class-level initialization."""
super().initialize()
persist.import_sys_path()
cls.import_module()
@classmethod
def reinitialize(cls):
"""Perform class-level initialization after plugins have been loaded at startup."""
# Be sure to clear _cmd so that import_module will re-import.
if hasattr(cls, '_cmd'):
delattr(cls, '_cmd')
cls.initialize()
@classmethod
def import_module(cls):
"""
Attempt to import the configured module.
If it could not be imported, use the executable.
"""
if hasattr(cls, '_cmd'):
return
module = getattr(cls, 'module', None)
cls._cmd = None
cmd = cls.cmd
script = None
if isinstance(cls.cmd, (list, tuple)):
cmd = cls.cmd[0]
if module is not None:
try:
module = importlib.import_module(module)
persist.debug('{} imported {}'.format(cls.name, module))
# If the linter specifies a python version, check to see
# if ST's python satisfies that version.
if cmd and not callable(cmd):
match = util.PYTHON_CMD_RE.match(cmd)
if match and match.group('version'):
version, script = match.group('version', 'script')
version = util.find_python(version=version, script=script, module=module)
# If we cannot find a python or script of the right version,
# we cannot use the module.
if version[0] is None or script and version[1] is None:
module = None
except ImportError:
message = '{}import of {} module in {} failed'
if cls.check_version:
warning = 'WARNING: '
message += ', linter will not work with python 3 code'
else:
warning = ''
message += ', linter will not run using built in python'
persist.printf(message.format(warning, module, cls.name))
module = None
except Exception as ex:
persist.printf(
'ERROR: unknown exception in {}: {}'
.format(cls.name, str(ex))
)
module = None
# If no module was specified, or the module could not be imported,
# or ST's python does not satisfy the version specified, see if
# any version of python available satisfies the linter. If not,
# set the cmd to '' to disable the linter.
can_lint = True
if not module and cmd and not callable(cmd):
match = util.PYTHON_CMD_RE.match(cmd)
if match and match.group('version'):
can_lint = False
version, script = match.group('version', 'script')
version = util.find_python(version=version, script=script)
if version[0] is not None and (not script or version[1] is not None):
can_lint = True
if can_lint:
cls._cmd = cls.cmd
# If there is a module, setting cmd to None tells us to
# use the check method.
if module:
cls.cmd = None
else:
persist.printf(
'WARNING: {} deactivated, no available version of python{} satisfies {}'
.format(
cls.name,
' or {}'.format(script) if script else '',
cmd
))
cls.disabled = True
cls.module = module
def context_sensitive_executable_path(self, cmd):
"""
Calculate the context-sensitive executable path, using @python and check_version.
Return a tuple of (have_path, path).
Return have_path == False if not self.check_version.
Return have_path == True if cmd is in [script]@python[version] form.
Return None for path if the desired version of python/script cannot be found.
Return '<builtin>' for path if the built-in python should be used.
"""
if not self.check_version:
return False, None
# Check to see if we have a @python command
match = util.PYTHON_CMD_RE.match(cmd[0])
if match:
settings = self.get_view_settings()
if '@python' in settings:
script = match.group('script') or ''
which = '{}@python{}'.format(script, settings.get('@python'))
path = self.which(which)
if path:
if path[0] == '<builtin>':
return True, '<builtin>'
elif path[0] is None or script and path[1] is None:
return True, None
return True, path
return False, None
@classmethod
def get_module_version(cls):
"""
Return the string version of the imported module, without any prefix/suffix.
This method handles the common case where a module (or one of its parents)
defines a __version__ string. For other cases, subclasses should override
this method and return the version string.
"""
if cls.module:
module = cls.module
while True:
if isinstance(getattr(module, '__version__', None), str):
return module.__version__
if hasattr(module, '__package__'):
try:
module = importlib.import_module(module.__package__)
except ImportError:
return None
else:
return None
def run(self, cmd, code):
"""Run the module checker or executable on code and return the output."""
if self.module is not None:
use_module = False
if not self.check_version:
use_module = True
else:
settings = self.get_view_settings()
version = settings.get('@python')
if version is None:
use_module = cmd is None or cmd[0] == '<builtin>'
else:
version = util.find_python(version=version, module=self.module)
use_module = version[0] == '<builtin>'
if use_module:
if persist.debug_mode():
persist.printf(
'{}: {} <builtin>'.format(
self.name,
os.path.basename(self.filename or '<unsaved>')
)
)
try:
errors = self.check(code, os.path.basename(self.filename or '<unsaved>'))
except Exception as err:
persist.printf(
'ERROR: exception in {}.check: {}'
.format(self.name, str(err))
)
errors = ''
if isinstance(errors, (tuple, list)):
return '\n'.join([str(e) for e in errors])
else:
return errors
else:
cmd = self._cmd
else:
cmd = self.cmd or self._cmd
cmd = self.build_cmd(cmd=cmd)
if cmd:
return super().run(cmd, code)
else:
return ''
def check(self, code, filename):
"""
Run a built-in check of code, returning errors.
Subclasses that provide built in checking must override this method
and return a string with one more lines per error, an array of strings,
or an array of objects that can be converted to strings.
"""
persist.printf(
'{}: subclasses must override the PythonLinter.check method'
.format(self.name)
)
return ''