#!/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 version 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, 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.2" PY3 = False if sys.version_info < (3, 0): from urllib import quote else: from urllib.parse import quote PY3 = True basestring = str unicode = str warning_urls = { "PyChecker" : "http://pychecker.sourceforge.net/", "PyFlakes" : "http://divmod.org/projects/pyflakes", "PyLint" : "http://www.logilab.org/857", "PEP-8" : "http://pypi.python.org/pypi/pep8", "Flake8" : "http://pypi.python.org/pypi/flake8/" } def format_warning_urls(): """ Format the warning URLs as necessary for TextMate to open them: """ out = [] for checker_name, checker_url in warning_urls.items(): out.append(f""" {checker_name} """.strip()) return tuple(out) def warning_link_urls(): """ Compose all formatted warning URLs in a user-facing message: """ one, two, three, four, five = format_warning_urls() return f"""

Please install {one}, {two}, {three}, {four} or {five} to enable extensive code checking.

""".strip() # patterns to match output of checker programs PYCHECKER_RE = re.compile(r"^(?:\s*)(.*?\.pyc?):(\d+):(?:\s*)(.*)(?:\s*)$") def textmate_url(file, line=None, column=None): """ Compose a Textmate callback URL, for sending the cursor to a location within an active Textmate buffer: """ url = f"txmt://open?url=file://{quote(file)}" if type(line) is int: url += f"&line={line}" if type(column) is int: url += f"&column={column}" return url HTML_HEADER_FORMAT = """ PyCheckMate %s """ HTML_HEADER_BODY = """

%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'): prefix_bin = os.path.join(sys.prefix, 'bin') executable_bin = os.path.split(sys.executable)[0] which.pathvar = os.getenv("PATH", DEFAULT_PATH) which.pathvar += f":{prefix_bin}:{executable_bin}" 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: url = textmate_url(script_path, int(e.lineno), int(e.offset)) script = escape(os.path.basename(script_path)) print(f'{script}:{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: 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, version_string): 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'' outlines = stdout.decode(UTF8_ENCODING).splitlines() issue_count = len(outlines) print(HTML_HEADER_BODY % (version_string, f'{issue_count} issues found')) idx = 0 for line in outlines: match = PYCHECKER_RE.search(line) if match: filename, lineno, message = match.groups() url = textmate_url(filename, int(lineno)) if basepath is not None and filename.startswith(basepath): filename = filename[len(basepath)+1:] # naive linewrapping, but it seems to work well-enough whitespace = "" if len(filename) + len(message) > 80: whitespace += "
  " number = int(idx) + 1 print(f'''
{number:02} {filename}:{lineno} {whitespace} {message}
''') idx += 1 else: print(f'{line}
') # THEY TOLD ME TO FLUSH THE PIPES SO I FLUSHED THE PIPES sys.stdout.flush() if stderr: 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() basepath = os.getenv("TM_PROJECT_DIRECTORY") version_string = f"PyCheckMate {__version__} – {checker_ver}" warning_string = "" if not checker_bin: warning_string += warning_link_urls() 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) if warning_string: print(warning_string) run_checker_program(checker_bin, checker_opts, script_path, version_string) 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: {os.path.basename(sys.argv[0])} ", file=sys.stderr) sys.exit(1)