diff --git a/README.markdown b/README.markdown
index d81df48..418fd08 100644
--- a/README.markdown
+++ b/README.markdown
@@ -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)
@@ -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:
- 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)]
-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, ...):
@@ -27,7 +29,8 @@ per project, ...):
"python_syntax_checkers":
[
["/usr/bin/pep8", ["--ignore=E501,E128,E221"] ]
- ]
+ ],
+ false
}
Both "CHECKERS local_settings" and sublime text settings will be used,
@@ -35,6 +38,55 @@ 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
+
+ name
+ invalid.python_checker.outline
+ scope
+ invalid.python_checker.outline
+ settings
+
+ background
+ #FF4A52
+ foreground
+ #FFFFFF
+
+
+
+ name
+ keyword.python_checker.outline
+ scope
+ keyword.python_checker.outline
+ settings
+
+ background
+ #DF9400
+ foreground
+ #FFFFFF
+
+
+
+ name
+ keyword.python_checker.underline
+ scope
+ keyword.python_checker.underline
+ settings
+
+ background
+ #FF0000
+
+
+```
+
## Why not sublimelint
Before creating this project I used [sublimelint](https://github.com/lunixbochs/sublimelint), which is multilanguage
diff --git a/python_checker.py b/python_checker.py
index bab8586..dbe5d0c 100644
--- a/python_checker.py
+++ b/python_checker.py
@@ -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.
@@ -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):
@@ -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()):
@@ -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