Skip to content

Instantly share code, notes, and snippets.

@fish2000
Last active December 5, 2018 17:53
Show Gist options
  • Save fish2000/103061d3242ce9075f63a7ef2ffbff07 to your computer and use it in GitHub Desktop.
Save fish2000/103061d3242ce9075f63a7ef2ffbff07 to your computer and use it in GitHub Desktop.
Overhaul of PyCheckMate.py for Python 3.7 circa late 2018
#!/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 = "&nbsp;" * (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>&nbsp;&nbsp;"
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 &nbsp; so that
# we can allow the browser to wrap long lines but we don't lose
# leading indentation otherwise.
stripped = line.lstrip()
pad = "&nbsp;" * (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__} &ndash; {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)} &mdash; {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