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 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""" | |
<a href="javascript:TextMate.system('open {checker_url}', null)">{checker_name}</a> | |
""".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""" | |
<p class="warning">Please install {one}, {two}, {three}, {four} or {five} to enable extensive code checking.</p> | |
""".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 = """ | |
<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; } | |
p.warning { | |
padding: 0px; | |
font-family: Consolas, Monaco; | |
font-size: 13pt; | |
border-top: 1px solid #333; | |
border-bottom: 1px solid #333; | |
border-left: 0px none; | |
border-right: 0px none; | |
margin: 1em; | |
margin-left: 0; | |
margin-right: 0; | |
background-color: #EFEFEF; | |
} | |
div#output p { | |
padding: 2px 0; | |
} | |
</style> | |
</head> | |
<body>""" | |
HTML_HEADER_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'): | |
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...<br><br>") | |
compile(source, script_path, "exec") | |
print("None<br>") | |
except SyntaxError as e: | |
url = textmate_url(script_path, int(e.lineno), | |
int(e.offset)) | |
script = escape(os.path.basename(script_path)) | |
print(f'<a href="{url}">{script}:{e.lineno}</a> {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: | |
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 += "<br> " | |
number = int(idx) + 1 | |
print(f'''<div class="message"> | |
<span class="number">{number:02}</span> | |
<a href="{url}">{filename}:{lineno}</a> | |
{whitespace} | |
<span class="message-text">{message}</a> | |
</div>''') | |
idx += 1 | |
else: | |
print(f'{line}<br>') | |
# 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'<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() | |
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.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