mirror of
https://github.com/sstent/sublime-text-3.git
synced 2026-01-25 14:41:38 +00:00
326 lines
10 KiB
Python
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 ''
|