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