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 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 = "&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:
# 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>&nbsp;&nbsp;"
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 &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()
version_string = f"PyCheckMate {__version__} &ndash; {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)} &mdash; {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