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 | |
| ### | |
| ### Constants | |
| ### | |
| 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> | |
| """ | |
| ### | |
| ### Helper classes | |
| ### | |
| 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 | |
| class Error(Exception): | |
| pass | |
| ### | |
| ### Program code | |
| ### | |
| def check_syntax(script_path): | |
| f = open(script_path, 'r') | |
| source = ''.join(f.readlines()+["\n"]) | |
| f.close() | |
| 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(str('<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 = str(escape(stripped.rstrip())) | |
| print('<span class="stderr">%s%s</span><br>' % (pad, line)) | |
| def find_checker_program(): | |
| checkers = ["pychecker", "pyflakes", "pylint", "pep8", "flake8"] | |
| tm_pychecker = os.getenv("TM_PYCHECKER") | |
| opts = filter(None, os.getenv('TM_PYCHECKER_OPTIONS', '').split()) | |
| if tm_pychecker == "builtin": | |
| return ('', None, "Syntax check only") | |
| if tm_pychecker is not None: | |
| 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 (might be | |
| # symlinked) | |
| bindir = os.path.split(sys.executable)[0] | |
| checker = os.path.join(bindir, basename) | |
| if not os.path.isfile(checker): | |
| # look where python is installed | |
| checker = os.path.join(sys.prefix, "bin", basename) | |
| if not os.path.isfile(checker): | |
| # search the PATH | |
| p = os.popen("/usr/bin/which '%s'" % basename) | |
| checker = p.readline().strip() | |
| p.close() | |
| if not os.path.isfile(checker): | |
| continue | |
| if basename == "pychecker": | |
| p = os.popen('"%s" -V 2>/dev/null' % (checker)) | |
| version = p.readline().strip() | |
| status = p.close() | |
| if status is None and version: | |
| version = "PyChecker %s" % version | |
| return (checker, opts, version) | |
| elif basename == "pylint": | |
| p = os.popen('"%s" --version 2>/dev/null' % (checker)) | |
| version = p.readline().strip() | |
| status = p.close() | |
| if status is None and version: | |
| version = re.sub('^pylint\s*', '', version) | |
| version = re.sub(',$', '', version) | |
| version = "Pylint %s" % 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 | |
| p = os.popen('"%s" "%s" 2>&1 >/dev/null' % (checker, checker)) | |
| output = p.readlines() | |
| status = p.close() | |
| if status is None and not output: | |
| return (checker, opts, "PyFlakes") | |
| elif basename == "pep8": | |
| p = os.popen('"%s" --version 2>/dev/null' % (checker)) | |
| version = p.readline().strip() | |
| status = p.close() | |
| if status is None and version: | |
| version = "PEP 8 %s" % version | |
| global PYCHECKER_RE | |
| PYCHECKER_RE = re.compile(r"^(.*?\.pyc?):(\d+):(?:\d+:)?\s+(.*)$") | |
| return (checker, opts, version) | |
| elif basename == "flake8": | |
| p = os.popen('"%s" --version 2>/dev/null' % (checker)) | |
| version = p.readline().strip() | |
| status = p.close() | |
| if status is None and version: | |
| version = "flake8 %s" % 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 = [] | |
| cmd.append(checker_bin) | |
| if checker_opts: | |
| cmd.extend(checker_opts) | |
| cmd.append(script_path) | |
| p = subprocess.Popen(cmd, | |
| stdin=subprocess.PIPE, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| shell=False) | |
| while 1: | |
| stdout, stderr = p.communicate() | |
| if stdout is None: | |
| break | |
| idx = 0 | |
| for line in stdout.decode(UTF8_ENCODING).splitlines(): | |
| # line = str(line.strip()) | |
| global PYCHECKER_RE | |
| match = PYCHECKER_RE.search(line) | |
| # print(str(line), file=sys.stdout) | |
| if match: | |
| filename, lineno, msg = match.groups() | |
| # href = TXMT_URL1_FORMAT % (quote(os.path.abspath(filename.strip())), | |
| # lineno) | |
| global TXMT_URL1_FORMAT | |
| href = TXMT_URL1_FORMAT % (quote(filename), lineno) | |
| # if basepath is not None and str(filename).startswith(basepath): | |
| # filename = str(filename[len(basepath)+1:]) | |
| # naive linewrapping, but it seems to work well-enough | |
| if len(filename) + len(msg) > 80: | |
| add_br = "<br> " | |
| else: | |
| add_br = " " | |
| # line = '<a href="%s">%s:%d</a>%s%s' % ( | |
| # href, filename, int(lineno), add_br, | |
| # escape(msg)) | |
| 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=str(href), | |
| filename=str(filename), | |
| lineno=int(lineno), | |
| whitespace=str(add_br), | |
| message=str(msg), | |
| number=int(idx) + 1), | |
| file=sys.stdout) | |
| idx += 1 | |
| else: | |
| print(f'{line}<br>') | |
| # else: | |
| # line = escape(line) | |
| # print(str("%s<br>" % line)) | |
| 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('<span class="stderr">%s%s</span><br>' % (pad, line), file=sys.stderr) | |
| sys.stderr.flush() | |
| break | |
| returncode = p.returncode | |
| print(str('<div id="exit-status"><br>Exit status: %s</div>' % returncode)) | |
| if returncode is None: | |
| p.terminate() | |
| def main(script_path): | |
| checker_bin, checker_opts, checker_ver = find_checker_program() | |
| version_string = "PyCheckMate %s – %s" % (__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 = "%s — %s" % (escape(script_name), escape(project_dir)) | |
| else: | |
| title = script_path | |
| print(str(HTML_HEADER_FORMAT % (title, version_string, 'xx issues found')), file=sys.stdout) | |
| sys.stdout.flush() | |
| if warning_string: | |
| print(warning_string) | |
| run_checker_program(checker_bin, checker_opts, script_path) | |
| sys.stdout.flush() | |
| # if checker_bin: | |
| # run_checker_program(checker_bin, checker_opts, script_path) | |
| # else: | |
| # check_syntax(script_path) | |
| print(str(HTML_FOOTER), file=sys.stdout) | |
| sys.stdout.flush() | |
| return 0 | |
| if __name__ == "__main__": | |
| if len(sys.argv) == 2: | |
| sys.exit(main(sys.argv[1])) | |
| else: | |
| print("Usage: %s <file.py>" % sys.argv[0], file=sys.stderr) | |
| sys.exit(1) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment