Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 57 additions & 5 deletions README.markdown
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Python PEP-8 and PyFlakes checker for SublimeText 2 editor
# Python PEP-8 and PyFlakes checker for SublimeText editor (2 and 3)

This project is a plugin for [SublimeText 2](http://www.sublimetext.com/2) text editor.
It checks all python files you opening and editing through two popular Python checkers - [pep8](http://pypi.python.org/pypi/pep8)
Expand All @@ -13,11 +13,13 @@ Go to your Packages dir (Sublime Text 2 -> Preferences -> Browse Packages). Clon
Go to sublimetext_python_checker/ and create file local_settings.py with list of your preferred checkers:

<pre>
CHECKERS = [('/Users/vorushin/.virtualenvs/checkers/bin/pep8', []),
('/Users/vorushin/.virtualenvs/checkers/bin/pyflakes', [])]
CHECKERS = [('/Users/vorushin/.virtualenvs/checkers/bin/pep8', [], False),
('/Users/vorushin/.virtualenvs/checkers/bin/pyflakes', [], False)]
</pre>

First parameter is path to command, second - optional list of arguments. If you want to disable line length checking in pep8, set second parameter to ['--ignore=E501'].
First parameter is path to command.
Second - optional list of arguments, If you want to disable line length checking in pep8, set second parameter to ['--ignore=E501'].
third - do you want to run this checker on each change. Only works with pyflakes, ATM.

You can also set syntax checkers using sublimetext settings (per file, global,
per project, ...):
Expand All @@ -27,14 +29,64 @@ per project, ...):
"python_syntax_checkers":
[
["/usr/bin/pep8", ["--ignore=E501,E128,E221"] ]
]
],
false
}
</pre>
Both "CHECKERS local_settings" and sublime text settings will be used,
but sublime text settings are prefered. (using syntax checker binary name)

Restart SublimeText 2 and open some *.py file to see check results. You can see additional information in python console of your editor (go View -> Show Console).

You can also set the colloring of the highlights generated by the plugin.
By default it will use the color for "keyword" (for pep8 messages) and "invalid"
(for pyflakes messages).
You can customise it by using the specific strings, though:
keyword.python_checker.outline: outline around lines with pep8 flags
invalid.python_checker.outline: outline around lines with pyflakes flags
keyword.python_checker.underline: column-specific mark for flags which provide it

An example used with a Solarized theme:
```xml
<dict>
<key>name</key>
<string>invalid.python_checker.outline</string>
<key>scope</key>
<string>invalid.python_checker.outline</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#FF4A52</string>
<key>foreground</key>
<string>#FFFFFF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>keyword.python_checker.outline</string>
<key>scope</key>
<string>keyword.python_checker.outline</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#DF9400</string>
<key>foreground</key>
<string>#FFFFFF</string>
</dict>
</dict>
<dict>
<key>name</key>
<string>keyword.python_checker.underline</string>
<key>scope</key>
<string>keyword.python_checker.underline</string>
<key>settings</key>
<dict>
<key>background</key>
<string>#FF0000</string>
</dict>
</dict>
```

## Why not sublimelint

Before creating this project I used [sublimelint](https://github.com/lunixbochs/sublimelint), which is multilanguage
Expand Down
215 changes: 144 additions & 71 deletions python_checker.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
import os
import re
import signal
from subprocess import Popen, PIPE

import sublime
import sublime_plugin


DEFAULT_CHECKERS = [
[
"/usr/bin/pep8",
[
"--ignore="
],
False,
"keyword.python_checker.outline"
],
[
"/usr/bin/pyflakes",
[
],
True,
"invalid.python_checker.outline"
],
[
"/usr/local/bin/pylint",
[
"-fparseable",
"-iy",
"-d C0301,C0302,C0111,C0103,R0911,R0912,R0913,R0914,R0915,W0142"
],
False,
"comment.python_checker.outline"
],
]

try:
from local_settings import CHECKERS
except ImportError as e:
print '''
print ('''
Please create file local_settings.py in the same directory with
python_checker.py. Add to local_settings.py list of your checkers.

Expand All @@ -33,94 +61,138 @@
["/usr/bin/pyflakes", [] ]
]
}
'''
''')


global view_messages
view_messages = {}
VIEW_MESSAGES = {}
VIEW_LINES = {}
VIEW_TOTALS = {}


class PythonCheckerCommand(sublime_plugin.EventListener):
def on_activated(self, view):
signal.signal(signal.SIGALRM, lambda s, f: check_and_mark(view))
signal.alarm(1)
def on_activated_async(self, view):
if view.id() not in VIEW_LINES: # TODO use change_count()
check_and_mark(view)

def on_deactivated(self, view):
signal.alarm(0)
def on_modified_async(self, view):
if view.id() in VIEW_LINES:
del VIEW_LINES[view.id()]
check_and_mark(view, True)

def on_post_save(self, view):
def on_post_save_async(self, view):
check_and_mark(view)

def on_close(self, view):
if view.id() in VIEW_MESSAGES:
VIEW_MESSAGES[view.id()].clear()
del VIEW_MESSAGES[view.id()]
del VIEW_LINES[view.id()]
view.erase_status('python_checker')

def on_selection_modified(self, view):
global view_messages
lineno = view.rowcol(view.sel()[0].end())[0]
if view.id() in view_messages and lineno in view_messages[view.id()]:
view.set_status('python_checker', view_messages[view.id()][lineno])
lineno = view.rowcol(view.sel()[0].begin())[0]
_message = ''
if view.id() in VIEW_LINES and lineno in VIEW_LINES[view.id()]:
for _, basename_lines in VIEW_MESSAGES[view.id()].items():
if lineno in basename_lines:
_message += (basename_lines[lineno]).decode('utf-8') + ';'
if _message or VIEW_TOTALS.get(view.id(), ''):
view.set_status('python_checker', '{} ({} )'.format(_message, VIEW_TOTALS.get(view.id(), '')))
else:
view.erase_status('python_checker')
view.set_status('python_checker', 'OK')


def check_and_mark(view):
if not 'python' in view.settings().get('syntax').lower():
def check_and_mark(view, is_buffer=False):
if view.settings().get('syntax', None) and \
not 'python' in view.settings().get('syntax', '').lower():
return
if not view.file_name(): # we check files (not buffers)
if not view.file_name() and not is_buffer:
return

mesg_quick = '' if is_buffer else '(everything)'
view.set_status('python_checker_running', 'Checking Python {}...'.format(mesg_quick))
checkers = view.settings().get('python_syntax_checkers', [])
checkers_basenames = [
os.path.basename(checker[0]) for checker in checkers]

# Append "local_settings.CHECKERS" to checkers from settings
# TODO: improve settings and default handling
# TODO: just use the checkers in path
if 'CHECKERS' in globals():
checkers_basenames = [
os.path.basename(checker[0]) for checker in checkers]
checkers.extend([checker for checker in CHECKERS
if os.path.basename(checker[0]) not in checkers_basenames])

messages = []
for checker, args in checkers:
checker_messages = []
try:
p = Popen([checker, view.file_name()] + args, stdout=PIPE,
stderr=PIPE)
stdout, stderr = p.communicate(None)
checker_messages += parse_messages(stdout)
checker_messages += parse_messages(stderr)
for line in checker_messages:
print "[%s] %s:%s:%s %s" % (
checker.split('/')[-1], view.file_name(),
line['lineno'] + 1, line['col'] + 1, line['text'])
messages += checker_messages
except OSError:
print "Checker could not be found:", checker

outlines = [view.full_line(view.text_point(m['lineno'], 0))
for m in messages]
view.erase_regions('python_checker_outlines')
view.add_regions('python_checker_outlines',
outlines,
'keyword',
sublime.DRAW_EMPTY | sublime.DRAW_OUTLINED)

underlines = []
for m in messages:
if m['col']:
a = view.text_point(m['lineno'], m['col'])
underlines.append(sublime.Region(a, a))

view.erase_regions('python_checker_underlines')
view.add_regions('python_checker_underlines',
underlines,
'keyword',
sublime.DRAW_EMPTY_AS_OVERWRITE | sublime.DRAW_OUTLINED)
checkers_basenames = [
os.path.basename(checker[0]) for checker in checkers]
checkers.extend([checker for checker in DEFAULT_CHECKERS
if os.path.basename(checker[0]) not in checkers_basenames])

line_messages = {}
for m in (m for m in messages if m['text']):
if m['lineno'] in line_messages:
line_messages[m['lineno']] += ';' + m['text']
else:
line_messages[m['lineno']] = m['text']

global view_messages
view_messages[view.id()] = line_messages
for checker, args, run_in_buffer, checker_scope in checkers:
checker_messages = []
line_messages = {}
if not is_buffer or is_buffer and run_in_buffer:
try:
if not is_buffer:
params = [checker, view.file_name()]
for arg in args:
params.insert(1, arg)
p = Popen(params, stdout=PIPE,
stderr=PIPE)
stdout, stderr = p.communicate(None)
else:
p = Popen([checker] + args, stdin=PIPE, stdout=PIPE,
stderr=PIPE)
stdout, stderr = p.communicate(bytes(view.substr(sublime.Region(0, view.size())), 'utf-8'))
checker_messages += parse_messages(stdout)
checker_messages += parse_messages(stderr)
except OSError:
print ("Checker could not be found:", checker)
except Exception as e:
print ("Generic error while running checker:", e)
else:
basename = os.path.basename(checker)
outline_name = 'python_checker_outlines_{}'.format(basename)
underline_name = 'python_checker_underlines_{}'.format(basename)
outline_scope = checker_scope
outlines = []
underlines = []
for m in checker_messages:
# print ("[%s] %s:%s:%s %s" % (
# checker.split('/')[-1], view.file_name(),
# m['lineno'] + 1, m['col'] + 1, m['text']))
outlines.append(view.full_line(view.text_point(m['lineno'], 0)))
if m['col']:
a = view.text_point(m['lineno'], m['col'])
underlines.append(sublime.Region(a, a))
if m['text']:
if m['lineno'] in line_messages:
line_messages[m['lineno']] += b';' + m['text']
else:
line_messages[m['lineno']] = m['text']
view.erase_regions(outline_name)
view.add_regions(outline_name, outlines, outline_scope,
icon='circle',
flags=sublime.DRAW_EMPTY | sublime.DRAW_OUTLINED)
view.erase_regions(underline_name)
view.add_regions(underline_name, underlines,
'keyword.python_checker.underline', flags=
sublime.DRAW_EMPTY_AS_OVERWRITE | sublime.DRAW_OUTLINED)
checker_messages.clear()
add_messages(view.id(), basename, line_messages)
view.erase_status('python_checker_running')


def add_messages(view_id, basename, basename_lines):
if view_id not in VIEW_MESSAGES:
VIEW_MESSAGES[view_id] = {}
VIEW_MESSAGES[view_id][basename] = basename_lines
lines = set()
VIEW_TOTALS[view_id] = ''
for basename, basename_lines in VIEW_MESSAGES[view_id].items():
lines.update(basename_lines.keys())
if basename_lines.keys():
VIEW_TOTALS[view_id] += ' {}:{}'.format(basename, len(basename_lines.keys()))

if lines:
VIEW_LINES[view_id] = lines


def parse_messages(checker_output):
Expand All @@ -144,8 +216,8 @@ def parse_messages(checker_output):
c:\Python26\Scripts\pildriver.py:208: 'ImageFilter' imported but unused
'''

pep8_re = re.compile(r'.*:(\d+):(\d+):\s+(.*)')
pyflakes_re = re.compile(r'.*:(\d+):\s+(.*)')
pep8_re = re.compile(b'.*:(\d+):(\d+):\s+(.*)')
pyflakes_re = re.compile(b'.*:(\d+):\s+(.*)')

messages = []
for i, line in enumerate(checker_output.splitlines()):
Expand All @@ -160,7 +232,8 @@ def parse_messages(checker_output):
continue
messages.append({'lineno': int(lineno) - 1,
'col': int(col) - 1,
'text': text})
'text': text,
})

return messages

Expand Down