├── .gitignore ├── README.markdown ├── local_settings.py.example ├── packages.json └── python_checker.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | local_settings.py -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Python PEP-8 and PyFlakes checker for SublimeText 2 editor 2 | 3 | This project is a plugin for [SublimeText 2](http://www.sublimetext.com/2) text editor. 4 | It checks all python files you opening and editing through two popular Python checkers - [pep8](http://pypi.python.org/pypi/pep8) 5 | and [PyFlakes](http://pypi.python.org/pypi/pyflakes). 6 | 7 | ## Installation 8 | 9 | Go to your Packages dir (Sublime Text 2 -> Preferences -> Browse Packages). Clone this repository into Packages subdirectory: 10 | 11 | git clone git://github.com/vorushin/sublimetext_python_checker.git 12 | 13 | Go to sublimetext_python_checker/ and create file local_settings.py with list of your preferred checkers: 14 | 15 |
16 |     CHECKERS = [('/Users/vorushin/.virtualenvs/checkers/bin/pep8', []),
17 |                 ('/Users/vorushin/.virtualenvs/checkers/bin/pyflakes', [])]
18 | 
19 | 20 | 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']. 21 | 22 | You can also set syntax checkers using sublimetext settings (per file, global, 23 | per project, ...): 24 |
25 |     "settings":
26 |     {
27 |         "python_syntax_checkers":
28 |         [
29 |             ["/usr/bin/pep8", ["--ignore=E501,E128,E221"] ]
30 |         ]
31 |     }
32 | 
33 | Both "CHECKERS local_settings" and sublime text settings will be used, 34 | but sublime text settings are prefered. (using syntax checker binary name) 35 | 36 | 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). 37 | 38 | ## Why not sublimelint 39 | 40 | Before creating this project I used [sublimelint](https://github.com/lunixbochs/sublimelint), which is multilanguage 41 | checker/linter. I described pros and cons of both below. 42 | 43 | ### sublimelint 44 | - can't use your Python version or your virtualenv 45 | - don't check with pep8 46 | - do checks on every edit 47 | - do checks for Python (derivative of pyflakes), PHP, Perl, Ruby 48 | - works on Windows/Linux/MacOSX 49 | 50 | ### sublimetext_python_checker 51 | - can use your version of Python and your virtualenv 52 | - do checks only on opening and saving files 53 | - works only on Linux and Mac OS X 54 | - checks only Python files 55 | - checks with pep8 and pyflakes 56 | - all this in a few screens of code 57 | -------------------------------------------------------------------------------- /local_settings.py.example: -------------------------------------------------------------------------------- 1 | ''' 2 | First parameter is path to command, second - optional list of arguments. 3 | If you want to disable line length checking in pep8, set second parameter 4 | to ['--ignore=E501']. Look at output of pep8 --help for more options. 5 | ''' 6 | CHECKERS = [('/Users/vorushin/.virtualenvs/checkers/bin/pep8', []), 7 | ('/Users/vorushin/.virtualenvs/checkers/bin/pyflakes', [])] 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": "1.2", 3 | "packages": [ 4 | { 5 | "name": "python_syntax_checker", 6 | "description": "Check python syntax using pep8 and pyFlakes", 7 | "author": "zehome", 8 | "homepage": "https://github.com/vorushin/sublimetext_python_checker", 9 | "platforms": { 10 | "osx": [ 11 | { 12 | "version": "1.0", 13 | "url": "https://nodeload.github.com/vorushin/sublimetext_python_checker/zipball/v1.0" 14 | } 15 | ], 16 | "linux": [ 17 | { 18 | "version": "1.0", 19 | "url": "https://nodeload.github.com/vorushin/sublimetext_python_checker/zipball/v1.0" 20 | } 21 | ] 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /python_checker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import signal 4 | from subprocess import Popen, PIPE 5 | 6 | import sublime 7 | import sublime_plugin 8 | 9 | try: 10 | from local_settings import CHECKERS 11 | except ImportError as e: 12 | print ''' 13 | Please create file local_settings.py in the same directory with 14 | python_checker.py. Add to local_settings.py list of your checkers. 15 | 16 | Example: 17 | 18 | CHECKERS = [('/Users/vorushin/.virtualenvs/checkers/bin/pep8', []), 19 | ('/Users/vorushin/.virtualenvs/checkers/bin/pyflakes', [])] 20 | 21 | First parameter is path to command, second - optional list of arguments. 22 | If you want to disable line length checking in pep8, set second parameter 23 | to ['--ignore=E501']. 24 | 25 | You can also insert checkers using sublime settings. 26 | 27 | For example in your project settings, add: 28 | "settings": 29 | { 30 | "python_syntax_checkers": 31 | [ 32 | ["/usr/bin/pep8", ["--ignore=E501,E128,E221"] ], 33 | ["/usr/bin/pyflakes", [] ] 34 | ] 35 | } 36 | ''' 37 | 38 | 39 | global view_messages 40 | view_messages = {} 41 | 42 | 43 | class PythonCheckerCommand(sublime_plugin.EventListener): 44 | def on_activated(self, view): 45 | signal.signal(signal.SIGALRM, lambda s, f: check_and_mark(view)) 46 | signal.alarm(1) 47 | 48 | def on_deactivated(self, view): 49 | signal.alarm(0) 50 | 51 | def on_post_save(self, view): 52 | check_and_mark(view) 53 | 54 | def on_selection_modified(self, view): 55 | global view_messages 56 | lineno = view.rowcol(view.sel()[0].end())[0] 57 | if view.id() in view_messages and lineno in view_messages[view.id()]: 58 | view.set_status('python_checker', view_messages[view.id()][lineno]) 59 | else: 60 | view.erase_status('python_checker') 61 | 62 | 63 | def check_and_mark(view): 64 | if not 'python' in view.settings().get('syntax').lower(): 65 | return 66 | if not view.file_name(): # we check files (not buffers) 67 | return 68 | 69 | checkers = view.settings().get('python_syntax_checkers', []) 70 | 71 | # Append "local_settings.CHECKERS" to checkers from settings 72 | if 'CHECKERS' in globals(): 73 | checkers_basenames = [ 74 | os.path.basename(checker[0]) for checker in checkers] 75 | checkers.extend([checker for checker in CHECKERS 76 | if os.path.basename(checker[0]) not in checkers_basenames]) 77 | 78 | messages = [] 79 | for checker, args in checkers: 80 | checker_messages = [] 81 | try: 82 | p = Popen([checker, view.file_name()] + args, stdout=PIPE, 83 | stderr=PIPE) 84 | stdout, stderr = p.communicate(None) 85 | checker_messages += parse_messages(stdout) 86 | checker_messages += parse_messages(stderr) 87 | for line in checker_messages: 88 | print "[%s] %s:%s:%s %s" % ( 89 | checker.split('/')[-1], view.file_name(), 90 | line['lineno'] + 1, line['col'] + 1, line['text']) 91 | messages += checker_messages 92 | except OSError: 93 | print "Checker could not be found:", checker 94 | 95 | outlines = [view.full_line(view.text_point(m['lineno'], 0)) 96 | for m in messages] 97 | view.erase_regions('python_checker_outlines') 98 | view.add_regions('python_checker_outlines', 99 | outlines, 100 | 'keyword', 101 | sublime.DRAW_EMPTY | sublime.DRAW_OUTLINED) 102 | 103 | underlines = [] 104 | for m in messages: 105 | if m['col']: 106 | a = view.text_point(m['lineno'], m['col']) 107 | underlines.append(sublime.Region(a, a)) 108 | 109 | view.erase_regions('python_checker_underlines') 110 | view.add_regions('python_checker_underlines', 111 | underlines, 112 | 'keyword', 113 | sublime.DRAW_EMPTY_AS_OVERWRITE | sublime.DRAW_OUTLINED) 114 | 115 | line_messages = {} 116 | for m in (m for m in messages if m['text']): 117 | if m['lineno'] in line_messages: 118 | line_messages[m['lineno']] += ';' + m['text'] 119 | else: 120 | line_messages[m['lineno']] = m['text'] 121 | 122 | global view_messages 123 | view_messages[view.id()] = line_messages 124 | 125 | 126 | def parse_messages(checker_output): 127 | ''' 128 | Examples of lines in checker_output 129 | 130 | pep8 on *nix 131 | /Users/vorushin/Python/answers/urls.py:24:80: E501 line too long \ 132 | (96 characters) 133 | 134 | pyflakes on *nix 135 | /Users/vorushin/Python/answers/urls.py:4: 'from django.conf.urls.defaults \ 136 | import *' used; unable to detect undefined names 137 | 138 | pyflakes, invalid syntax message (3 lines) 139 | /Users/vorushin/Python/answers/urls.py:14: invalid syntax 140 | dict_test = {key: value for (key, value) in [('one', 1), ('two': 2)]} 141 | ^ 142 | 143 | pyflakes on Windows 144 | c:\Python26\Scripts\pildriver.py:208: 'ImageFilter' imported but unused 145 | ''' 146 | 147 | pep8_re = re.compile(r'.*:(\d+):(\d+):\s+(.*)') 148 | pyflakes_re = re.compile(r'.*:(\d+):\s+(.*)') 149 | 150 | messages = [] 151 | for i, line in enumerate(checker_output.splitlines()): 152 | if pep8_re.match(line): 153 | lineno, col, text = pep8_re.match(line).groups() 154 | elif pyflakes_re.match(line): 155 | lineno, text = pyflakes_re.match(line).groups() 156 | col = 1 157 | if text == 'invalid syntax': 158 | col = invalid_syntax_col(checker_output, i) 159 | else: 160 | continue 161 | messages.append({'lineno': int(lineno) - 1, 162 | 'col': int(col) - 1, 163 | 'text': text}) 164 | 165 | return messages 166 | 167 | 168 | def invalid_syntax_col(checker_output, line_index): 169 | ''' 170 | For error messages like this: 171 | 172 | /Users/vorushin/Python/answers/urls.py:14: invalid syntax 173 | dict_test = {key: value for (key, value) in [('one', 1), ('two': 2)]} 174 | ^ 175 | ''' 176 | for line in checker_output.splitlines()[line_index + 1:]: 177 | if line.startswith(' ') and line.find('^') != -1: 178 | return line.find('^') 179 | --------------------------------------------------------------------------------