Last active
December 5, 2018 17:53
-
-
Save fish2000/103061d3242ce9075f63a7ef2ffbff07 to your computer and use it in GitHub Desktop.
Overhaul of PyCheckMate.py for Python 3.7 circa late 2018
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| # encoding: utf-8 | |
| # | |
| # PyCheckMate, a PyChecker output beautifier for TextMate. | |
| # Copyright (c) Jay Soffian, 2005. <jay at soffian dot org> | |
| # 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 = """ | |
| <html> | |
| <head> | |
| <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |
| <title>PyCheckMate %s</title> | |
| <style type="text/css"> | |
| body { | |
| background-color: #D8E2F1; | |
| margin: 0; | |
| } | |
| div#body { | |
| border-style: dotted; | |
| border-width: 1px 0; | |
| border-color: #666; | |
| margin: 10px 0; | |
| padding: 10px; | |
| background-color: #C9D9F0; | |
| } | |
| div#output { | |
| padding: 0; | |
| margin: 0; | |
| color: #121212; | |
| font-family: Consolas, Monaco; | |
| font-size: 11pt; | |
| } | |
| div#output div.message { | |
| vertical-align: middle; | |
| display: inline-block; | |
| margin: 0.5em; | |
| padding: 0.5em; | |
| margin-left: 0px; | |
| margin-right: 1em; | |
| padding-left: 2px; | |
| padding-right: 1em; | |
| margin-top: 10px; | |
| padding-top: 0px; | |
| border-radius: 10px; | |
| background-color: #D9E9FF; | |
| color: #121212; | |
| font-family: Consolas, Monaco; | |
| font-size: 11pt; | |
| } | |
| div#output div.message span.number { | |
| padding: 0; | |
| margin: 0; | |
| margin-left: 10px; | |
| color: #121212; | |
| font-family: Georgia, Times New Roman; | |
| font-size: 3em; | |
| } | |
| div#output div.message span.message-text { | |
| padding: 0; | |
| margin: 0; | |
| margin-left: 2.5em; | |
| } | |
| div#output div.message a { | |
| color: darkorange; | |
| } | |
| div#exit-status { | |
| padding: 0; | |
| margin: 0; | |
| padding-top: 1em; | |
| font-family: Consolas, Monaco; | |
| font-size: 11pt; | |
| } | |
| strong { | |
| margin-left: 3.0em; | |
| font-family: Aksidenz-Grotesk, Helvetica Neue, Helvetica, Arial; | |
| text-transform: uppercase; | |
| } | |
| strong.title { | |
| margin-top: 1em; | |
| font-size: 18pt; | |
| text-transform: uppercase; | |
| } | |
| span.stderr { color: red; } | |
| p { margin: 0; padding: 2px 0; } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="body"> | |
| <p><strong class="title">%s</strong></p> | |
| <p><strong>%s</strong></p> | |
| <br> | |
| <div id="output"> | |
| """ | |
| HTML_FOOTER = """ | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| 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...<br><br>") | |
| compile(source, script_path, "exec") | |
| print("None<br>") | |
| except SyntaxError as e: | |
| href = TXMT_URL2_FORMAT % (quote(script_path), | |
| e.lineno, | |
| e.offset) | |
| print('<a href="%s">%s:%s</a> %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'<span class="stderr">{pad}{line}</span><br>') | |
| 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 += "<br> " | |
| tpl = ''' | |
| <div class="message"> | |
| <span class="number">%(number)02d</span> | |
| <a href="%(href)s">%(filename)s:%(lineno)d</a> | |
| %(whitespace)s | |
| <span class="message-text">%(message)s</a> | |
| </div> | |
| ''' | |
| 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}<br>') | |
| # 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'<span class="stderr">{pad}{line}</span><br>') | |
| sys.stdout.flush() | |
| returncode = p.returncode | |
| if returncode is None: | |
| returncode = 'NULL' | |
| p.terminate() | |
| print(f''' | |
| <div id="exit-status"> | |
| <br>Exit status: {returncode} | |
| </div> | |
| ''') | |
| 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 = \ | |
| "<a href=\"javascript:TextMate.system('open %s', null)\">%s</a>" | |
| 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 = \ | |
| "<p>Please install %s, %s, %s, %s or %s for more extensive code checking." \ | |
| "</p><br>" % (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.py>", file=sys.stderr) | |
| sys.exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment