#!/usr/bin/env python # encoding: utf-8 # # PyCheckMate, a PyChecker output beautifier for TextMate. # Copyright (c) Jay Soffian, 2005. # Inspired by Domenico Carbotta's PyMate. # Extensively overhauled for verions 2.0 by Alexander Böhn # # License: Artistic. # # Usage: # - Out of the box, pycheckmate.py will perform only a basic syntax check # by attempting to compile the python code. # - Install PyChecker or PyFlakes for more extensive checking. If both are # installed, PyChecker will be used. # - TM_PYCHECKER may be set to control which checker is used. Set it to just # "pychecker", "pyflakes", "pep8", "flake8", or "pylint", or "frosted" to # locate these programs in the default python bin directory or to a full # path if the checker program is installed elsewhere. # - If for some reason you want to use the built-in sytax check when either # pychecker or pyflakes are installed, you may set TM_PYCHECKER to # "builtin". from __future__ import absolute_import, print_function import os import re import sys import traceback from html import escape __version__ = "2.0" PY3 = False if sys.version_info < (3, 0): from urllib import quote else: from urllib.parse import quote PY3 = True basestring = str unicode = str PYCHECKER_URL = "http://pychecker.sourceforge.net/" PYFLAKES_URL = "http://divmod.org/projects/pyflakes" PYLINT_URL = "http://www.logilab.org/857" PEP8_URL = "http://pypi.python.org/pypi/pep8" FLAKE8_URL = "http://pypi.python.org/pypi/flake8/" # patterns to match output of checker programs PYCHECKER_RE = re.compile(r"^(?:\s*)(.*?\.pyc?):(\d+):(?:\s*)(.*)(?:\s*)$") # careful editing these, they are format strings TXMT_URL1_FORMAT = "txmt://open?url=file://%s&line=%s" TXMT_URL2_FORMAT = "txmt://open?url=file://%s&line=%s&column=%s" HTML_HEADER_FORMAT = """ PyCheckMate %s

%s

%s


""" HTML_FOOTER = """
""" CHECKERS = ["pychecker", "pyflakes", "pylint", "pep8", "flake8"] DEFAULT_TIMEOUT = 60 # seconds DEFAULT_PATH = ":".join(filter(os.path.exists, ("/usr/local/bin", "/bin", "/usr/bin", "/sbin", "/usr/sbin"))) def which(binary_name, pathvar=None): """ Deduces the path corresponding to an executable name, as per the UNIX command `which`. Optionally takes an override for the $PATH environment variable. Always returns a string - an empty one for those executables that cannot be found. """ from distutils.spawn import find_executable if not hasattr(which, 'pathvar'): which.pathvar = os.getenv("PATH", DEFAULT_PATH) return find_executable(binary_name, pathvar or which.pathvar) or "" UTF8_ENCODING = 'UTF-8' def utf8_encode(source): """ Encode a source as bytes using the UTF-8 codec """ if PY3: if type(source) is bytes: return source return bytes(source, encoding=UTF8_ENCODING) if type(source) is unicode: return source.encode(UTF8_ENCODING) return source def check_syntax(script_path): with open(script_path, 'r') as handle: source = ''.join(handle.readlines() + ["\n"]) try: print("Syntax Errors...

") compile(source, script_path, "exec") print("None
") except SyntaxError as e: href = TXMT_URL2_FORMAT % (quote(script_path), e.lineno, e.offset) print('%s:%s %s' % (href, escape(os.path.basename(script_path)), e.lineno, e.msg)) except: for line in traceback.format_exception(sys.exc_info()): stripped = line.lstrip() pad = " " * (len(line) - len(stripped)) line = escape(stripped.rstrip()) print(f'{pad}{line}
') def find_checker_program(): tm_pychecker = os.getenv("TM_PYCHECKER") opts = filter(None, os.getenv('TM_PYCHECKER_OPTIONS', '').split()) version = '' if tm_pychecker == "builtin": return ('', None, "Syntax check only") if tm_pychecker is not None: if not tm_pychecker in CHECKERS: CHECKERS.insert(0, tm_pychecker) for checker in CHECKERS: basename = os.path.split(checker)[1] if checker == basename: # look for checker in same bin directory as python -- # it might be symlinked: bindir = os.path.split(sys.executable)[0] checker = os.path.join(bindir, basename) if not os.path.isfile(checker): # continue searching python's installation directory: checker = os.path.join(sys.prefix, "bin", basename) if not os.path.isfile(checker): # search the PATH checker = which(basename) if not os.path.isfile(checker): continue if basename == "pychecker": with os.popen(f'"{checker}" -V 2>/dev/null') as p: version = p.readline().strip() if version: version = f"PyChecker {version}" return (checker, opts, version) elif basename == "pylint": with os.popen(f'"{checker}" --version 2>/dev/null') as p: version = p.readline().strip() if version: version = re.sub('^pylint\s*', '', version) version = re.sub(',$', '', version) version = f"Pylint {version}" opts += ('--output-format=parseable',) return (checker, opts, version) elif basename == "pyflakes": # pyflakes doesn't have a version string embedded anywhere, # so run it against itself to make sure it's functional with os.popen(f'"{checker}" "{checker}" 2>&1 >/dev/null') as p: output = p.readlines() if not output: return (checker, opts, "PyFlakes") elif basename == "pep8": with os.popen(f'"{checker}" --version 2>/dev/null') as p: version = p.readline().strip() if version: version = f"PEP 8 {version}" global PYCHECKER_RE PYCHECKER_RE = re.compile(r"^(.*?\.pyc?):(\d+):(?:\d+:)?\s+(.*)$") return (checker, opts, version) elif basename == "flake8": with os.popen(f'"{checker}" --version 2>/dev/null') as p: version = p.readline().strip() if version: version = f"flake8 {version}" PYCHECKER_RE = re.compile(r"^(.*?\.pyc?):(\d+):(?:\d+:)?\s+(.*)$") return (checker, opts, version) return ('', None, "Syntax check only") def run_checker_program(checker_bin, checker_opts, script_path): import subprocess basepath = os.getenv("TM_PROJECT_DIRECTORY") cmd = [checker_bin] if checker_opts: cmd.extend(checker_opts) cmd.append(script_path) p = subprocess.Popen(cmd, shell=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) try: stdout, stderr = p.communicate(timeout=DEFAULT_TIMEOUT) except subprocess.TimeoutExpired: p.kill() stdout, stderr = p.communicate(timeout=None) if stdout is None: stdout = b'' if stderr is None: stderr = b'' idx = 0 for line in stdout.decode(UTF8_ENCODING).splitlines(): match = PYCHECKER_RE.search(line) if match: filename, lineno, msg = match.groups() href = TXMT_URL1_FORMAT % (quote(filename), lineno) if basepath is not None and filename.startswith(basepath): filename = filename[len(basepath)+1:] # naive linewrapping, but it seems to work well-enough add_br = "" if len(filename) + len(msg) > 80: add_br += "
  " tpl = '''
%(number)02d %(filename)s:%(lineno)d %(whitespace)s %(message)s
''' print(tpl % dict(href=href, filename=filename, lineno=int(lineno), whitespace=add_br, message=msg, number=int(idx) + 1)) idx += 1 else: print(f'{line}
') # THEY TOLD ME TO FLUSH THE PIPES SO I FLUSHED THE PIPES sys.stdout.flush() for line in stderr.decode(UTF8_ENCODING).splitlines(): # strip whitespace off front and replace with   so that # we can allow the browser to wrap long lines but we don't lose # leading indentation otherwise. stripped = line.lstrip() pad = " " * (len(line) - len(stripped)) line = escape(stripped.rstrip()) print(f'{pad}{line}
') sys.stdout.flush() returncode = p.returncode if returncode is None: returncode = 'NULL' p.terminate() print(f'''

Exit status: {returncode}
''') def main(script_path): checker_bin, checker_opts, checker_ver = find_checker_program() version_string = f"PyCheckMate {__version__} – {checker_ver}" warning_string = "" if not checker_bin: href_format = \ "%s" pychecker_url = href_format % (PYCHECKER_URL, "PyChecker") pyflakes_url = href_format % (PYFLAKES_URL, "PyFlakes") pylint_url = href_format % (PYLINT_URL, "Pylint") pep8_url = href_format % (PEP8_URL, "PEP 8") flake8_url = href_format % (FLAKE8_URL, "flake8") warning_string = \ "

Please install %s, %s, %s, %s or %s for more extensive code checking." \ "


" % (pychecker_url, pyflakes_url, pylint_url, pep8_url, flake8_url) basepath = os.getenv("TM_PROJECT_DIRECTORY") if basepath: project_dir = os.path.basename(basepath) script_name = os.path.basename(script_path) title = f"{escape(script_name)} — {escape(project_dir)}" else: title = escape(script_path) print(HTML_HEADER_FORMAT % (title, version_string, 'xx issues found')) sys.stdout.flush() if warning_string: print(warning_string) run_checker_program(checker_bin, checker_opts, script_path) print(HTML_FOOTER) sys.stdout.flush() return 0 if __name__ == "__main__": if len(sys.argv) == 2: sys.exit(main(sys.argv[1])) else: print(f"Usage: {sys.argv[0]} ", file=sys.stderr) sys.exit(1)