# # 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: 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\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: 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 '' 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] == '': return True, '' 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] == '' else: version = util.find_python(version=version, module=self.module) use_module = version[0] == '' if use_module: if persist.debug_mode(): persist.printf( '{}: {} '.format( self.name, os.path.basename(self.filename or '') ) ) try: errors = self.check(code, os.path.basename(self.filename or '')) 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 ''