├── .gitignore ├── .no-sublime-package ├── icons ├── error.png ├── fatal.png ├── refactor.png ├── warning.png └── convention.png ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Pylinter.sublime-settings ├── Main.sublime-menu ├── multiconf.py ├── README.rst ├── MonokaiPylinter.tmTheme └── pylinter.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /.no-sublime-package: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biermeester/Pylinter/HEAD/icons/error.png -------------------------------------------------------------------------------- /icons/fatal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biermeester/Pylinter/HEAD/icons/fatal.png -------------------------------------------------------------------------------- /icons/refactor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biermeester/Pylinter/HEAD/icons/refactor.png -------------------------------------------------------------------------------- /icons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biermeester/Pylinter/HEAD/icons/warning.png -------------------------------------------------------------------------------- /icons/convention.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/biermeester/Pylinter/HEAD/icons/convention.png -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+alt+z"], "command": "pylinter"}, 3 | { "keys": ["ctrl+alt+x"], "command": "pylinter", "args": {"action": "toggle"} }, 4 | { "keys": ["ctrl+alt+c"], "command": "pylinter", "args": {"action": "list"} }, 5 | { "keys": ["ctrl+alt+i"], "command": "pylinter", "args": {"action": "ignore"} } 6 | ] 7 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["super+alt+z"], "command": "pylinter"}, 3 | { "keys": ["super+alt+x"], "command": "pylinter", "args": {"action": "toggle"} }, 4 | { "keys": ["super+alt+c"], "command": "pylinter", "args": {"action": "list"} }, 5 | { "keys": ["super+alt+i"], "command": "pylinter", "args": {"action": "ignore"} } 6 | ] 7 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+alt+z"], "command": "pylinter"}, 3 | { "keys": ["ctrl+alt+x"], "command": "pylinter", "args": {"action": "toggle"} }, 4 | { "keys": ["ctrl+alt+c"], "command": "pylinter", "args": {"action": "list"} }, 5 | { "keys": ["ctrl+alt+i"], "command": "pylinter", "args": {"action": "ignore"} } 6 | ] 7 | -------------------------------------------------------------------------------- /Pylinter.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // When versbose is 'true', various messages will be written to the console. 3 | // values: true or false 4 | "verbose": false, 5 | // The full path to the Python executable you want to 6 | // run Pylint with or simply use 'python'. 7 | "python_bin": "python", 8 | // The following paths will be added Pylint's Python path 9 | "python_path": [ 10 | 11 | ], 12 | // Optionally set the working directory 13 | "working_dir": null, 14 | // Full path to the lint.py module in the pylint package 15 | "pylint_path": null, 16 | // Optional full path to a Pylint configuration file 17 | "pylint_rc": null, 18 | // Set to true to automtically run Pylint on save 19 | "run_on_save": true, 20 | // Set to true to use graphical error icons 21 | "use_icons": false, 22 | "disable_outline": false, 23 | // Status messages stay as long as cursor is on an error line 24 | "message_stay": false, 25 | // Ignore Pylint error types. Possible values: 26 | // "R" : Refactor for a "good practice" metric violation 27 | // "C" : Convention for coding standard violation 28 | // "W" : Warning for stylistic problems, or minor programming issues 29 | // "E" : Error for important programming issues (i.e. most probably bug) 30 | // "F" : Fatal for errors which prevented further processing 31 | "ignore": [], 32 | // a list of strings of individual errors to disable, ex: ["C0301"] 33 | "disable": [], 34 | "plugins": [] 35 | } 36 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "mnemonic": "n", 5 | "id": "preferences", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "mnemonic": "P", 11 | "id": "package-settings", 12 | "children": 13 | [ 14 | { 15 | "caption": "Pylinter", 16 | "children": 17 | [ 18 | // Pylinter settings 19 | { 20 | "command": "open_file", "args": 21 | { 22 | "file": "${packages}/Pylinter/Pylinter.sublime-settings" 23 | }, 24 | "caption": "Settings – Default" 25 | }, 26 | { 27 | "command": "open_file", "args": 28 | { 29 | "file": "${packages}/User/Pylinter.sublime-settings" 30 | }, 31 | "caption": "Settings – User" 32 | }, 33 | { "caption": "-" }, 34 | // Pylinter Default Key mappings 35 | { 36 | "command": "open_file", 37 | "args": { 38 | "file": "${packages}/Pylinter/Default (Windows).sublime-keymap", 39 | "platform": "Windows" 40 | }, 41 | "caption": "Key Bindings – Default" 42 | }, 43 | { 44 | "command": "open_file", 45 | "args": { 46 | "file": "${packages}/Pylinter/Default (OSX).sublime-keymap", 47 | "platform": "OSX" 48 | }, 49 | "caption": "Key Bindings – Default" 50 | }, 51 | { 52 | "command": "open_file", 53 | "args": { 54 | "file": "${packages}/Pylinter/Default (Linux).sublime-keymap", 55 | "platform": "Linux" 56 | }, 57 | "caption": "Key Bindings – Default" 58 | }, 59 | // Pylinter User Key mappings 60 | { 61 | "command": "open_file", 62 | "args": { 63 | "file": "${packages}/User/Default (Windows).sublime-keymap", 64 | "platform": "Windows" 65 | }, 66 | "caption": "Key Bindings – User" 67 | }, 68 | { 69 | "command": "open_file", 70 | "args": { 71 | "file": "${packages}/User/Default (OSX).sublime-keymap", 72 | "platform": "OSX" 73 | }, 74 | "caption": "Key Bindings – User" 75 | }, 76 | { 77 | "command": "open_file", 78 | "args": { 79 | "file": "${packages}/User/Default (Linux).sublime-keymap", 80 | "platform": "Linux" 81 | }, 82 | "caption": "Key Bindings – User" 83 | } 84 | ] 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | ] 91 | -------------------------------------------------------------------------------- /multiconf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import socket 4 | import sublime 5 | import re 6 | 7 | """ Multiconf is a module that allows you to read platforma and/or host 8 | specific configuration values to be used by Sublime Text 2 plugins. 9 | 10 | Using this module's `get` function, allows the user to replace any settings 11 | value in a '.settings' file with a dictionary containing multiple values. 12 | 13 | Mulitconf does this by using a dictionary with a special identifier 14 | "#multiconf#" and a list of dictionaries identified by a qualifier of the form 15 | 16 | ":[;:]..." 17 | 18 | For example, the following setting 19 | 20 | "user_home": "/home" 21 | 22 | would result in `get("user_home")` returning the value "/home" but it could also 23 | be replaced with 24 | 25 | "user_home": { 26 | "#multiconf#": [ 27 | {"os:windows": "C:\\Users"}, 28 | {"os:linux;host:his_pc": "/home"}, 29 | {"os:linux;host:her_pc": "/home/her/special"} 30 | ] 31 | } 32 | 33 | Now the same configuration file will provide different values depending on the 34 | machine it's on. On an MS Windows machine the value returned by `get` will be 35 | "C:\\Users", and on a Linux machine with the host name 'his_pc' the value will be 36 | "/home". 37 | """ 38 | 39 | __version__ = "1.0" 40 | 41 | __CURRENT_HOSTNAME = socket.gethostname().lower() 42 | __CURRENT_PLATFORM = sublime.platform() 43 | 44 | QUALIFIERS = r"""([A-Za-z\d_]*):([^;]*)(?:;|$)""" 45 | 46 | def isstr(s): 47 | try: 48 | return isinstance(s, basestring) 49 | except NameError: 50 | return isinstance(s, str) 51 | 52 | def get(settings_obj, key, default=None, callback=None): 53 | """ 54 | Return a Sublime Text plugin setting value 55 | 56 | Parameters: 57 | settings_obj - a sublime.Settings object or a dictionary containing 58 | settings 59 | key - the name of the setting 60 | default - the default value to return if the key value is not found. 61 | callback - a callback function that, if provided, will be called with 62 | the found and default values as parameters. 63 | 64 | """ 65 | # Parameter validation 66 | if not isinstance(settings_obj, (dict, sublime.Settings)): 67 | raise AttributeError("Invalid settings object") 68 | if not isstr(key): 69 | raise AttributeError("Invalid callback function") 70 | if callback != None and not hasattr(callback, '__call__'): 71 | raise AttributeError("Invalid callback function") 72 | 73 | setting = settings_obj.get(key, default) 74 | final_val = None 75 | 76 | if isinstance(setting, dict) and "#multiconf#" in setting: 77 | reject_item = False 78 | for entry in setting["#multiconf#"]: 79 | reject_item = False if isinstance(entry, dict) and len(entry) else True 80 | 81 | k, v = entry.popitem() 82 | 83 | if reject_item: 84 | continue 85 | 86 | for qual in re.compile(QUALIFIERS).finditer(k): 87 | if Qualifications.exists(qual.group(1)): 88 | reject_item = not Qualifications.eval_qual(qual.group(1), qual.group(2)) 89 | else: 90 | reject_item = True 91 | if reject_item: 92 | break 93 | 94 | if not reject_item: 95 | final_val = v 96 | break 97 | 98 | if reject_item: 99 | final_val = default 100 | else: 101 | final_val = setting 102 | 103 | return callback(final_val, default) if callback else final_val 104 | 105 | 106 | class QualException(Exception): 107 | pass 108 | 109 | 110 | class Qualifications(object): 111 | __qualifiers = {} 112 | 113 | @classmethod 114 | def add_qual(cls, key, callback): 115 | if isstr(key) and re.match(r"^[a-zA-Z][a-zA-Z\d_]*$", key) == None: 116 | raise QualException("'%s' is not a valid function name." % key) 117 | if not hasattr(callback, '__call__'): 118 | raise QualException("Bad function callback.") 119 | if key in cls.__qualifiers: 120 | raise QualException("'%s' qualifier already exists." % key) 121 | 122 | cls.__qualifiers[key] = callback 123 | 124 | @classmethod 125 | def exists(cls, key): 126 | return (key in cls.__qualifiers) 127 | 128 | @classmethod 129 | def eval_qual(cls, key, value): 130 | try: 131 | return cls.__qualifiers[key](value) 132 | except: 133 | raise QualException("Failed to execute %s qualifier" % key) 134 | 135 | 136 | def __host_match(h): 137 | return (h.lower() == __CURRENT_HOSTNAME) 138 | 139 | 140 | def __os_match(os): 141 | return (os == __CURRENT_PLATFORM) 142 | 143 | 144 | Qualifications.add_qual("host", __host_match) 145 | Qualifications.add_qual("os", __os_match) 146 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pylinter Sublime Text 2/3 Plugin 2 | ------------------------------ 3 | 4 | Introduction 5 | ============ 6 | 7 | This is a small plugin for Sublime Text 2 and 3 that allows automatic Python 8 | source code checking by Pylint. 9 | 10 | Since Pylint can take a while before it has completed its task (multiple 11 | seconds), it is run from a separate thread, so the plugin won't lock up Sublime 12 | Text. 13 | 14 | The plugin can be automatically invoked *on save* or by a keyboard shortcut. 15 | 16 | Support for Pylint configuration files is included. 17 | 18 | **Note**:: 19 | 20 | ** Pylint needs to be installed separately!!! ** 21 | 22 | If you have installed Pylint into a Virtualenv, you need to launch Sublime 23 | Text from that Virtualenv for everything to work correctly. This might be 24 | resolved in the future. 25 | 26 | Latest changes 27 | ============== 28 | 29 | **2014-03-03** 30 | 31 | Added support for Pylint plugins. You can add a list of plugin module names into 32 | your configuration using the `plugins` setting. 33 | 34 | **2013-11-15** 35 | 36 | Some refactoring has been done to make sure Pylinter works better under ST3. 37 | Also, the error handling, in case `Pylint` cannot be found, is improved. 38 | 39 | **2013-09-06** 40 | 41 | Improved Pylint detection and a Pylint version check bug fix. 42 | 43 | **2013-09-01** 44 | 45 | This is the first version of Pylinter that is both compatible with Sublime 46 | Text 2 and 3. Please feel free to report any issues. And many thank-yous to 47 | everyone reporting issues and supplying solutions in regards to Python 3 48 | compatibility. 49 | 50 | **2013-08-24** 51 | 52 | Thanks to dbader for the Pylint 1.0 support 53 | 54 | * Pylinter now automatically detects what version of Pylint is used and is both 55 | compatible with the new 1.0.0 version and the older ones. 56 | 57 | **2013-01-20** 58 | 59 | Thanks to KristoforMaynard for the following additions: 60 | 61 | * When the ``message_stay`` setting is set to ``true``, the error messages will 62 | be displayed as long as the cursor stays on the offending line. 63 | * The ``disable_outline`` setting can be set to ``true`` to hide the outlines of 64 | errors. 65 | * The ``disable`` setting can be assigned a list or errors to ignore. E.g. 66 | ["C0301", "E1011"] 67 | 68 | **2012-09-12** 69 | 70 | * Pylinter will now try and automatically find the path to Pylint. 71 | 72 | **2012-09-06** 73 | 74 | * Pylinter now allows for platform and/or host specific configuration to be 75 | stored in a single configuration file. This is particulary useful for the 76 | ``pylint_path`` setting. 77 | 78 | Simply change a setting like 79 | 80 | ``"pylint_path": ""`` 81 | 82 | into something like this:: 83 | 84 | "pylint_path": { 85 | "#multiconf#": [ 86 | {"os:windows": ""}, 87 | {"os:linux;host:"}, 88 | {"os:linux;host:"} 89 | ] 90 | } 91 | 92 | For more information you can have a look at the following `gist`_. 93 | 94 | **2012-08-31** 95 | 96 | * Added icons for different message types. You can use these icons by 97 | setting the option ``use_icons`` to ``true`` (Icons by `Yusuke Kamiyamane`_). 98 | 99 | **2012-08-29** 100 | 101 | * Added an 'ignore' function, allowing for easy insertion of 102 | ``#pylint: disable=`` statements/comments. 103 | * Included wuub's error colouring. Either use the included 104 | ``MonokaiPylinter.tmTheme`` file, or have a look at it to see how you can 105 | colour the different erros and warnings. 106 | 107 | 108 | 109 | Configuration 110 | ============= 111 | 112 | Pylinter will try and determine the path to Pylint. If it fails you *must* 113 | provide a full path to the ``lint.py`` module of your Pylint installation! 114 | 115 | * **python_bin**: The full path to the Python executable you want to use for 116 | running Pylint (e.g. when you are using virtualenv) or simply ``python`` 117 | if you want to use your default python installation. 118 | 119 | * **python_path**: An optional list of paths that will be added to Pylint's 120 | Python path. 121 | 122 | * **working_dir**: An optional path to the working directory from which Pylint 123 | will be run. 124 | 125 | * **pylint_path**: The full path to the ``lint.py`` module. 126 | 127 | * **pylint_rc**: The full path to the Pylint configuration file you want to use, 128 | if any. 129 | 130 | * **run_on_save**: If this setting is set to ``true``, Pylint will be invoked 131 | each time you save a Python source code file. 132 | 133 | * **ignore**: A list of Pylint error types which you wish to ignore. 134 | 135 | Possible values: 136 | 137 | * "R" : Refactor for a "good practice" metric violation 138 | * "C" : Convention for coding standard violation 139 | * "W" : Warning for stylistic problems, or minor programming issues 140 | * "E" : Error for important programming issues (i.e. most probably bug) 141 | * "F" : Fatal for errors which prevented further processing 142 | 143 | * **use_icons**: Set to ``true`` if you want to display icons instead of dots in 144 | the margin. 145 | 146 | Multiconf 147 | ~~~~~~~~~ 148 | 149 | Any setting can be replaced by a Multiconf structure :: 150 | 151 | "pylint_path": { 152 | "#multiconf#": [ 153 | {"os:windows": ""}, 154 | {"os:linux;host:"}, 155 | {"os:linux;host:"} 156 | ] 157 | } 158 | 159 | For more information you can have a look at the following `gist`_. 160 | 161 | Project settings 162 | ~~~~~~~~~~~~~~~~ 163 | 164 | You may also store settings in your *.sublime-project files. Create a 165 | ``"pylinter"`` section as shown below and override any or all of the described 166 | settings:: 167 | 168 | { 169 | "folders": 170 | [ 171 | { 172 | "path": "/N/development/fabrix" 173 | } 174 | ], 175 | "settings": 176 | { 177 | "pylinter": 178 | { 179 | } 180 | } 181 | } 182 | 183 | 184 | Commands & Keyboard Shortcuts 185 | ============================= 186 | 187 | **Run** 188 | 189 | The plugin can be invoked by a keyboard shortcut: 190 | 191 | * **OS X**: ``Command+Alt+z`` 192 | * **Linux, Windows**: ``Control+Alt+z`` 193 | 194 | **Add pylint ignore comment/statement** 195 | 196 | Add a 'Pylint disable' comment to the end of the line with an error code in it, 197 | so it will be ignored on the next check. 198 | 199 | * **OS X**: ``Command+Alt+i`` 200 | * **Linux, Windows**: ``Control+Alt+i`` 201 | 202 | **Toggle Marking** 203 | 204 | The marking of the errors in the file can be toggled off and on: 205 | 206 | * **OS X**: ``Command+Alt+x`` 207 | * **Linux, Windows**: ``Control+Alt+x`` 208 | 209 | **Quick List** 210 | 211 | To see a quick list of all the Pylint errors use: 212 | 213 | * **OS X**: ``Command+Alt+c`` 214 | * **Linux, Windows**: ``Control+Alt+c`` 215 | 216 | .. _gist: https://gist.github.com/3646966 217 | .. _Yusuke Kamiyamane: http://p.yusukekamiyamane.com/ 218 | -------------------------------------------------------------------------------- /MonokaiPylinter.tmTheme: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Monokai 7 | settings 8 | 9 | 10 | name 11 | Pylinter 12 | scope 13 | pylinter 14 | settings 15 | 16 | foreground 17 | #AE81FF 18 | 19 | 20 | 21 | name 22 | PylinterW 23 | scope 24 | pylinter.W 25 | settings 26 | 27 | foreground 28 | #FFAE00 29 | 30 | 31 | 32 | name 33 | PylinterR 34 | scope 35 | pylinter.R 36 | settings 37 | 38 | foreground 39 | #6E6E6E 40 | 41 | 42 | 43 | name 44 | PylintereE 45 | scope 46 | pylinter.E 47 | settings 48 | 49 | foreground 50 | #ED2D2D 51 | 52 | 53 | 54 | name 55 | PylintereF 56 | scope 57 | pylinter.F 58 | settings 59 | 60 | foreground 61 | #5CCBD1 62 | 63 | 64 | 65 | settings 66 | 67 | background 68 | #272822 69 | caret 70 | #F8F8F0 71 | foreground 72 | #F8F8F2 73 | invisibles 74 | #3B3A32 75 | lineHighlight 76 | #3E3D32 77 | selection 78 | #49483E 79 | findHighlight 80 | #FFE792 81 | findHighlightForeground 82 | #000000 83 | selectionBorder 84 | #222218 85 | activeGuide 86 | #9D550FB0 87 | 88 | bracketsForeground 89 | #F8F8F2A5 90 | bracketsOptions 91 | underline 92 | 93 | bracketContentsForeground 94 | #F8F8F2A5 95 | bracketContentsOptions 96 | underline 97 | 98 | tagsOptions 99 | stippled_underline 100 | 101 | 102 | 103 | name 104 | Comment 105 | scope 106 | comment 107 | settings 108 | 109 | foreground 110 | #75715E 111 | 112 | 113 | 114 | name 115 | String 116 | scope 117 | string 118 | settings 119 | 120 | foreground 121 | #E6DB74 122 | 123 | 124 | 125 | name 126 | Number 127 | scope 128 | constant.numeric 129 | settings 130 | 131 | foreground 132 | #AE81FF 133 | 134 | 135 | 136 | 137 | name 138 | Built-in constant 139 | scope 140 | constant.language 141 | settings 142 | 143 | foreground 144 | #AE81FF 145 | 146 | 147 | 148 | name 149 | User-defined constant 150 | scope 151 | constant.character, constant.other 152 | settings 153 | 154 | foreground 155 | #AE81FF 156 | 157 | 158 | 159 | name 160 | Variable 161 | scope 162 | variable 163 | settings 164 | 165 | fontStyle 166 | 167 | 168 | 169 | 170 | name 171 | Keyword 172 | scope 173 | keyword 174 | settings 175 | 176 | foreground 177 | #F92672 178 | 179 | 180 | 181 | name 182 | Storage 183 | scope 184 | storage 185 | settings 186 | 187 | fontStyle 188 | 189 | foreground 190 | #F92672 191 | 192 | 193 | 194 | name 195 | Storage type 196 | scope 197 | storage.type 198 | settings 199 | 200 | fontStyle 201 | italic 202 | foreground 203 | #66D9EF 204 | 205 | 206 | 207 | name 208 | Class name 209 | scope 210 | entity.name.class 211 | settings 212 | 213 | fontStyle 214 | underline 215 | foreground 216 | #A6E22E 217 | 218 | 219 | 220 | name 221 | Inherited class 222 | scope 223 | entity.other.inherited-class 224 | settings 225 | 226 | fontStyle 227 | italic underline 228 | foreground 229 | #A6E22E 230 | 231 | 232 | 233 | name 234 | Function name 235 | scope 236 | entity.name.function 237 | settings 238 | 239 | fontStyle 240 | 241 | foreground 242 | #A6E22E 243 | 244 | 245 | 246 | name 247 | Function argument 248 | scope 249 | variable.parameter 250 | settings 251 | 252 | fontStyle 253 | italic 254 | foreground 255 | #FD971F 256 | 257 | 258 | 259 | name 260 | Tag name 261 | scope 262 | entity.name.tag 263 | settings 264 | 265 | fontStyle 266 | 267 | foreground 268 | #F92672 269 | 270 | 271 | 272 | name 273 | Tag attribute 274 | scope 275 | entity.other.attribute-name 276 | settings 277 | 278 | fontStyle 279 | 280 | foreground 281 | #A6E22E 282 | 283 | 284 | 285 | name 286 | Library function 287 | scope 288 | support.function 289 | settings 290 | 291 | fontStyle 292 | 293 | foreground 294 | #66D9EF 295 | 296 | 297 | 298 | name 299 | Library constant 300 | scope 301 | support.constant 302 | settings 303 | 304 | fontStyle 305 | 306 | foreground 307 | #66D9EF 308 | 309 | 310 | 311 | name 312 | Library class/type 313 | scope 314 | support.type, support.class 315 | settings 316 | 317 | fontStyle 318 | italic 319 | foreground 320 | #66D9EF 321 | 322 | 323 | 324 | name 325 | Library variable 326 | scope 327 | support.other.variable 328 | settings 329 | 330 | fontStyle 331 | 332 | 333 | 334 | 335 | name 336 | Invalid 337 | scope 338 | invalid 339 | settings 340 | 341 | background 342 | #F92672 343 | fontStyle 344 | 345 | foreground 346 | #F8F8F0 347 | 348 | 349 | 350 | name 351 | Invalid deprecated 352 | scope 353 | invalid.deprecated 354 | settings 355 | 356 | background 357 | #AE81FF 358 | foreground 359 | #F8F8F0 360 | 361 | 362 | 363 | name 364 | JSON String 365 | scope 366 | meta.structure.dictionary.json string.quoted.double.json 367 | settings 368 | 369 | foreground 370 | #CFCFC2 371 | 372 | 373 | 374 | 375 | name 376 | diff.header 377 | scope 378 | meta.diff, meta.diff.header 379 | settings 380 | 381 | foreground 382 | #75715E 383 | 384 | 385 | 386 | name 387 | diff.deleted 388 | scope 389 | markup.deleted 390 | settings 391 | 392 | foreground 393 | #F92672 394 | 395 | 396 | 397 | name 398 | diff.inserted 399 | scope 400 | markup.inserted 401 | settings 402 | 403 | foreground 404 | #A6E22E 405 | 406 | 407 | 408 | name 409 | diff.changed 410 | scope 411 | markup.changed 412 | settings 413 | 414 | foreground 415 | #E6DB74 416 | 417 | 418 | 419 | 420 | scope 421 | constant.numeric.line-number.find-in-files - match 422 | settings 423 | 424 | foreground 425 | #AE81FFA0 426 | 427 | 428 | 429 | scope 430 | entity.name.filename.find-in-files 431 | settings 432 | 433 | foreground 434 | #E6DB74 435 | 436 | 437 | 438 | 439 | uuid 440 | D8D5E82E-3D5B-46B5-B38E-8C841C21347D 441 | 442 | 443 | -------------------------------------------------------------------------------- /pylinter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ PyLinter Sublime Text Plugin 4 | 5 | This is a Pylint plugin for Sublime Text. 6 | 7 | Copyright R. de Laat, Elit 2011-2013 8 | 9 | For more information, go to https://github.com/biermeester/Pylinter#readme 10 | """ 11 | 12 | import os.path 13 | import sys 14 | import re 15 | import threading 16 | import subprocess 17 | import collections 18 | import sublime 19 | import sublime_plugin 20 | 21 | #pylint: disable=E1101 22 | 23 | # Constant to differentiate between ST2 and ST3 24 | ST3 = int(sublime.version()) > 3000 25 | 26 | if ST3: 27 | from . import multiconf 28 | else: 29 | import multiconf 30 | 31 | # The version of Python that SublimeText is using 32 | PYTHON_VERSION = sys.version_info[0] 33 | 34 | # To override this, set the 'verbose' setting in the configuration file 35 | PYLINTER_VERBOSE = True 36 | 37 | # Prevent the console from popping up in Windows 38 | if os.name == "nt": 39 | STARTUPINFO = subprocess.STARTUPINFO() 40 | STARTUPINFO.dwFlags |= subprocess.STARTF_USESHOWWINDOW 41 | else: 42 | STARTUPINFO = None 43 | 44 | # The output format we want PyLint's error messages to be in 45 | PYLINT_FORMAT = '--msg-template={path}:{line}:{msg_id}:{msg}' 46 | # Pylint error cache 47 | PYLINTER_ERRORS = {} 48 | PATH_SEPERATOR = ';' if os.name == "nt" else ':' 49 | SEPERATOR_PATTERN = ';' if os.name == "nt" else '[:;]' 50 | 51 | # The last line selected (i.e. the one we need to display status info for) 52 | LAST_SELECTED_LINE = -1 53 | # Indicates if we're displaying info in the status line 54 | STATUS_ACTIVE = False 55 | 56 | # The followig global values will be set by the `set_globals` function 57 | PYLINT_VERSION = None 58 | PYLINT_SETTINGS = None 59 | # Regular expression to disect Pylint error messages 60 | P_PYLINT_ERROR = None 61 | 62 | # The default Pylint command will be stored in this variable. It will either be 63 | # ["pylint"] or [, ] if the former is not found. 64 | DEFAULT_PYLINT_COMMAND = None 65 | 66 | def speak(*msg): 67 | """ Log messages to the console if PYLINTER_VERBOSE is True """ 68 | if PYLINTER_VERBOSE: 69 | print(" - PyLinter: " + " ".join(msg)) 70 | 71 | def plugin_loaded(): 72 | """ Set all global values """ 73 | 74 | global PYLINT_VERSION, PYLINT_SETTINGS, P_PYLINT_ERROR, DEFAULT_PYLINT_COMMAND 75 | 76 | PYLINT_SETTINGS = sublime.load_settings('Pylinter.sublime-settings') 77 | DEFAULT_PYLINT_COMMAND = PylSet.get_default_pylint_command() 78 | PYLINT_VERSION = PylSet.get_lint_version() 79 | 80 | # Pylint version < 1.0 81 | if PYLINT_VERSION[0] == 0: 82 | # Regular expression to disect Pylint error messages 83 | P_PYLINT_ERROR = re.compile(r""" 84 | ^(?P.+?):(?P[0-9]+):\ # file name and line number 85 | \[(?P[a-z])(?P\d+) # message type and error number 86 | # e.g. E0101 87 | (,\ (?P.+))?\]\ # optional class or function name 88 | (?P.*) # finally, the error message 89 | """, re.IGNORECASE | re.VERBOSE) 90 | # Pylint version 1.0 or greater 91 | else: 92 | P_PYLINT_ERROR = re.compile(r""" 93 | ^(?P.+?):(?P[0-9]+): # file name and line number 94 | (?P[a-z])(?P\d+): # message type and error number, 95 | # e.g. E0101 96 | (?P.*) # finally, the error message 97 | """, re.IGNORECASE | re.VERBOSE) 98 | 99 | class PylSet(object): 100 | """ Pylinter Settings class""" 101 | @classmethod 102 | def _get_settings_obj(cls): 103 | try: 104 | view_settings = sublime.active_window().active_view().settings() 105 | view_settings = view_settings.get('pylinter') 106 | if view_settings: 107 | return view_settings 108 | except AttributeError: 109 | pass 110 | 111 | return PYLINT_SETTINGS 112 | 113 | @classmethod 114 | def get(cls, setting_name): 115 | value = cls.get_or(setting_name, None) 116 | if value is None: 117 | raise PylSetException("No value found for '%s'" % setting_name) 118 | return value 119 | 120 | @classmethod 121 | def get_or(cls, setting_name, default): 122 | settings_obj = cls._get_settings_obj() 123 | 124 | if isinstance(settings_obj, collections.Iterable): 125 | if not setting_name in settings_obj: 126 | settings_obj = PYLINT_SETTINGS 127 | return multiconf.get(settings_obj, setting_name, default) 128 | 129 | @classmethod 130 | def read_settings(cls): 131 | global PYLINTER_VERBOSE 132 | 133 | PYLINTER_VERBOSE = cls.get_or('verbose', False) 134 | 135 | speak("Verbose is", str(PYLINTER_VERBOSE)) 136 | python_bin = cls.get_or('python_bin', 'python') 137 | python_path = cls.get_or('python_path', []) 138 | python_path = PATH_SEPERATOR.join([str(p) for p in python_path]) 139 | working_dir = cls.get_or('working_dir', None) 140 | pylint_path = cls.get_or('pylint_path', None) 141 | pylint_rc = cls.get_or('pylint_rc', None) or "" 142 | ignore = [t.lower() for t in cls.get_or('ignore', [])] 143 | plugins = cls.get_or('plugins', None) 144 | 145 | # Add custom runtime settings 146 | pylint_extra = PylSet.get_or('pylint_extra', None) 147 | 148 | disable = cls.get_or('disable', []) 149 | # Added ignore for trailing whitespace (false positives bug in 150 | # pylint 1.0.0) 151 | if PYLINT_VERSION[0] != 0: 152 | disable.append('C0303') 153 | disable_msgs = ",".join(disable) 154 | 155 | if pylint_rc and not os.path.exists(pylint_rc): 156 | msg = "Pylint configuration not found at '%s'." % pylint_rc 157 | sublime.error_message(msg) 158 | return False 159 | 160 | return (python_bin, 161 | python_path, 162 | working_dir, 163 | pylint_path, 164 | pylint_rc, 165 | ignore, 166 | disable_msgs, 167 | pylint_extra, 168 | plugins) 169 | 170 | @classmethod 171 | def get_default_pylint_command(cls): 172 | """ This class method will check if the `pylint` command is available. 173 | 174 | If it is not, it will try and determine the path to the `lint.py` file 175 | directly. 176 | """ 177 | 178 | try: 179 | python_bin = cls.get_or('python_bin', 'python') 180 | pylint_path = cls.get_or('pylint_path', None) 181 | if pylint_path is None: 182 | _ = subprocess.Popen("pylint", 183 | stdout=subprocess.PIPE, 184 | stderr=subprocess.PIPE, 185 | startupinfo=STARTUPINFO) 186 | speak("Pylint executable found") 187 | return ["pylint"] 188 | else: 189 | return [python_bin, pylint_path] 190 | except OSError: 191 | speak("Pylint executable *not* found") 192 | speak("Seaching for lint.py module...") 193 | 194 | cmd = ["python", "-c"] 195 | 196 | if PYTHON_VERSION == 2: 197 | cmd.append("import pylint; print pylint.__path__[0]") 198 | else: 199 | cmd.append("import pylint; print(pylint.__path__[0])") 200 | 201 | proc = subprocess.Popen(cmd, 202 | stdout=subprocess.PIPE, 203 | stderr=subprocess.PIPE, 204 | startupinfo=STARTUPINFO) 205 | 206 | out, _ = proc.communicate() 207 | 208 | pylint_path = None 209 | if out != b"": 210 | pylint_path = os.path.join(out.strip(), 211 | b"lint.py").decode("utf-8") 212 | 213 | if not pylint_path: 214 | msg = ("Pylinter could not automatically determined the path to `lint.py`.\n\n" 215 | "Please provide one in the settings file using the `pylint_path` variable.\n\n" 216 | "NOTE:\nIf you are using a Virtualenv, the problem might be resolved by " 217 | "launching Sublime Text from correct Virtualenv.") 218 | sublime.error_message(msg) 219 | elif not os.path.exists(pylint_path): 220 | msg = ("Pylinter could not find `lint.py` at the given path:\n\n'{}'.".format(pylint_path)) 221 | sublime.error_message(msg) 222 | else: 223 | speak("Pylint path {0} found".format(pylint_path)) 224 | python_bin = cls.get_or('python_bin', 'python') 225 | return [python_bin, pylint_path] 226 | 227 | @classmethod 228 | def get_lint_version(cls): 229 | """ Return the Pylint version as a (x, y, z) tuple """ 230 | pylint_path = cls.get_or('pylint_path', None) 231 | python_bin = cls.get_or('python_bin', 'python') 232 | found = None 233 | 234 | regex = re.compile(b"[lint.py|pylint] ([0-9]+).([0-9]+).([0-9]+)") 235 | 236 | if pylint_path: 237 | command = [python_bin, pylint_path] 238 | else: 239 | command = list(DEFAULT_PYLINT_COMMAND) 240 | 241 | command.append("--version") 242 | 243 | try: 244 | p = subprocess.Popen(command, 245 | stdout=subprocess.PIPE, 246 | stderr=subprocess.PIPE, 247 | startupinfo=STARTUPINFO) 248 | output, _ = p.communicate() 249 | found = regex.search(output) 250 | except OSError: 251 | msg = "Pylinter could not find '%s'" % command[-2] 252 | sublime.error_message(msg) 253 | 254 | if found: 255 | found = found.groups() 256 | if len(found) == 3: 257 | version = tuple(int(v) for v in found) 258 | speak("Pylint version %s found" % str(version)) 259 | return version 260 | 261 | speak("Could not determine Pylint version") 262 | return (1, 0, 0) 263 | 264 | 265 | class PylSetException(Exception): 266 | pass 267 | 268 | 269 | class PylinterCommand(sublime_plugin.TextCommand): 270 | 271 | def run(self, edit, **kwargs): 272 | """ Run a Pylinter command """ 273 | settings = PylSet.read_settings() 274 | 275 | if not settings: 276 | return 277 | 278 | action = kwargs.get('action', None) 279 | 280 | if action == 'toggle': 281 | self.toggle_regions() 282 | elif action == 'list': 283 | self.popup_error_list() 284 | elif action == 'dump': 285 | self.dump_errors() 286 | elif action == 'ignore': 287 | if not ST3: 288 | edit = self.view.begin_edit() 289 | self.add_ignore(edit) 290 | else: 291 | speak("Running Pylinter on %s" % self.view.file_name()) 292 | 293 | if self.view.file_name().endswith('.py'): 294 | thread = PylintThread(self.view, *settings) 295 | thread.start() 296 | self.progress_tracker(thread) 297 | 298 | def dump_errors(self): 299 | """ Print the found pylint errors """ 300 | import pprint 301 | pprint.pprint(PYLINTER_ERRORS) 302 | 303 | @classmethod 304 | def show_errors(cls, view): 305 | """ Display the errors for the given view """ 306 | # Icons to be used in the margin 307 | if PylSet.get_or('use_icons', False): 308 | if ST3: 309 | icons = {"C": "Packages/Pylinter/icons/convention.png", 310 | "E": "Packages/Pylinter/icons/error.png", 311 | "F": "Packages/Pylinter/icons/fatal.png", 312 | "I": "Packages/Pylinter/icons/convention.png", 313 | "R": "Packages/Pylinter/icons/refactor.png", 314 | "W": "Packages/Pylinter/icons/warning.png"} 315 | else: 316 | icons = {"C": "../Pylinter/icons/convention", 317 | "E": "../Pylinter/icons/error", 318 | "F": "../Pylinter/icons/fatal", 319 | "I": "../Pylinter/icons/convention", 320 | "R": "../Pylinter/icons/refactor", 321 | "W": "../Pylinter/icons/warning"} 322 | else: 323 | icons = {"C": "dot", 324 | "E": "dot", 325 | "F": "dot", 326 | "I": "dot", 327 | "R": "dot", 328 | "W": "dot"} 329 | 330 | if PylSet.get_or('disable_outline', False): 331 | region_flag = sublime.HIDDEN 332 | else: 333 | region_flag = sublime.DRAW_OUTLINED 334 | 335 | outlines = {"C": [], "E": [], "F": [], "I": [], "R": [], "W": []} 336 | 337 | for line_num, error in PYLINTER_ERRORS[view.id()].items(): 338 | if not isinstance(line_num, int): 339 | continue 340 | line = view.line(view.text_point(line_num, 0)) 341 | outlines[error[0]].append(line) 342 | 343 | for key, regions in outlines.items(): 344 | view.add_regions('pylinter.' + key, regions, 345 | 'pylinter.' + key, icons[key], 346 | region_flag) 347 | 348 | def popup_error_list(self): 349 | """ Display a popup list of the errors found """ 350 | view_id = self.view.id() 351 | 352 | if not view_id in PYLINTER_ERRORS: 353 | return 354 | 355 | # No errors were found 356 | if len(PYLINTER_ERRORS[view_id]) == 1: 357 | sublime.message_dialog("No Pylint errors found") 358 | return 359 | 360 | errors = [(key + 1, value) 361 | for key, value in PYLINTER_ERRORS[view_id].items() 362 | if key != 'visible'] 363 | line_nums, panel_items = zip(*sorted(errors, 364 | key=lambda error: error[1])) 365 | 366 | def on_done(selected_item): 367 | """ Jump to the line of the item that was selected from the list """ 368 | if selected_item == -1: 369 | return 370 | self.view.run_command("goto_line", 371 | {"line": line_nums[selected_item]}) 372 | 373 | self.view.window().show_quick_panel(list(panel_items), on_done) 374 | 375 | def progress_tracker(self, thread, i=0): 376 | """ Display spinner while Pylint is running """ 377 | icons = [u"◐", u"◓", u"◑", u"◒"] 378 | sublime.status_message("PyLinting %s" % icons[i]) 379 | if thread.is_alive(): 380 | i = (i + 1) % 4 381 | sublime.set_timeout(lambda: self.progress_tracker(thread, i), 100) 382 | else: 383 | sublime.status_message("") 384 | 385 | def toggle_regions(self): 386 | """ Show/hide the errors found """ 387 | view_id = self.view.id() 388 | try: 389 | if PYLINTER_ERRORS[view_id]['visible']: 390 | speak("Hiding errors") 391 | for category in ["C", "E", "F", "I", "R", "W"]: 392 | self.view.erase_regions('pylinter.' + category) 393 | else: 394 | speak("Showing errors") 395 | self.show_errors(self.view) 396 | PYLINTER_ERRORS[view_id]['visible'] ^= True 397 | except KeyError: 398 | pass 399 | 400 | def add_ignore(self, edit): 401 | """ Make pylint ignore the line that the carret is on """ 402 | global PYLINTER_ERRORS 403 | 404 | view_id = self.view.id() 405 | point = self.view.sel()[0].end() 406 | position = self.view.rowcol(point) 407 | current_line = position[0] 408 | 409 | pylint_statement = "".join(("#", "pyl", "int: ", "disable=")) 410 | 411 | # If an error is registered for that line 412 | if current_line in PYLINTER_ERRORS[view_id]: 413 | #print position 414 | line_region = self.view.line(point) 415 | line_txt = self.view.substr(line_region) 416 | 417 | err_code = PYLINTER_ERRORS[view_id][current_line] 418 | err_code = err_code[:err_code.find(':')] 419 | 420 | if pylint_statement not in line_txt: 421 | line_txt += " " + pylint_statement + err_code 422 | else: 423 | line_txt += "," + err_code 424 | 425 | self.view.replace(edit, line_region, line_txt) 426 | self.view.end_edit(edit) 427 | 428 | def is_enabled(self): 429 | """ This plugin is only enabled for Python modules """ 430 | file_name = self.view.file_name() 431 | if file_name: 432 | return file_name.endswith('.py') 433 | return False 434 | 435 | 436 | class PylintThread(threading.Thread): 437 | """ This class creates a seperate thread to run Pylint in """ 438 | 439 | def __init__(self, view, pbin, ppath, cwd, lpath, lrc, ignore, 440 | disable_msgs, extra_pylint_args, plugins): 441 | self.view = view 442 | # Grab the file name here, since view cannot be accessed 443 | # from anywhere but the main application thread 444 | self.file_name = view.file_name() 445 | self.python_bin = pbin 446 | self.python_path = ppath 447 | self.working_dir = cwd 448 | self.pylint_path = lpath 449 | self.pylint_rc = lrc 450 | self.ignore = ignore 451 | self.disable_msgs = disable_msgs 452 | self.extra_pylint_args = extra_pylint_args 453 | self.plugins = plugins 454 | 455 | threading.Thread.__init__(self) 456 | 457 | def run(self): 458 | """ Run the pylint command """ 459 | if self.pylint_path: 460 | command = [self.python_bin, self.pylint_path] 461 | else: 462 | command = list(DEFAULT_PYLINT_COMMAND) 463 | 464 | if PYLINT_VERSION[0] == 0: 465 | options = ['--output-format=parseable', 466 | '--include-ids=y'] 467 | else: 468 | options = ['--reports=n', 469 | PYLINT_FORMAT] 470 | 471 | if self.plugins: 472 | options.extend(["--load-plugins", 473 | ",".join(self.plugins)]) 474 | 475 | if self.pylint_rc: 476 | options.append('--rcfile=%s' % self.pylint_rc) 477 | 478 | if self.disable_msgs: 479 | options.append('--disable=%s' % self.disable_msgs) 480 | 481 | options.append(self.file_name) 482 | command.extend(options) 483 | 484 | self.set_path() 485 | 486 | speak("Running command with Pylint", str(PYLINT_VERSION)) 487 | speak(" ".join(command)) 488 | 489 | p = subprocess.Popen(command, 490 | stdout=subprocess.PIPE, 491 | stderr=subprocess.PIPE, 492 | startupinfo=STARTUPINFO, 493 | cwd=self.working_dir) 494 | output, eoutput = p.communicate() 495 | 496 | if PYTHON_VERSION == 2: 497 | lines = [line for line in output.split('\n')] # pylint: disable=E1103 498 | elines = [line for line in eoutput.split('\n')] # pylint:disable=E1103 499 | else: 500 | lines = [line for line in output.decode().split('\n')] # pylint: disable=E1103 501 | elines = [line for line in eoutput.decode().split('\n')] # pylint:disable=E1103 502 | 503 | # Call set_timeout to have the error processing done 504 | # from the main thread 505 | sublime.set_timeout(lambda: self.process_errors(lines, elines), 100) 506 | 507 | def set_path(self): 508 | """ Adjust the PYTHONPATH variable for this thread """ 509 | original = os.environ.get('PYTHONPATH', '') 510 | 511 | speak("Current PYTHONPATH is '%s'" % original) 512 | 513 | org_path_lst = [p for p in re.split(SEPERATOR_PATTERN, original) if p] 514 | pyl_path_lst = [p for p in re.split(SEPERATOR_PATTERN, 515 | self.python_path) if p] 516 | 517 | pythonpaths = set(org_path_lst + pyl_path_lst) 518 | os.environ['PYTHONPATH'] = PATH_SEPERATOR.join(pythonpaths) 519 | 520 | speak("Updated PYTHONPATH is '{0}'".format(os.environ['PYTHONPATH'])) 521 | 522 | 523 | def process_errors(self, lines, errlines): 524 | """ Process the error found """ 525 | view_id = self.view.id() 526 | PYLINTER_ERRORS[view_id] = {"visible": True} 527 | 528 | # if pylint raised any exceptions, propogate those to the user, for 529 | # instance, trying to disable a messaage id that does not exist 530 | if len(errlines) > 1: 531 | err = errlines[-2] 532 | if not err.startswith("No config file found"): 533 | sublime.error_message("Fatal pylint error:\n%s" % (errlines[-2])) 534 | 535 | for line in lines: 536 | mdic = re.match(P_PYLINT_ERROR, line) 537 | if mdic: 538 | m = mdic.groupdict() 539 | line_num = int(m['line']) - 1 540 | if m['type'].lower() not in self.ignore: 541 | PYLINTER_ERRORS[view_id][line_num] = \ 542 | "%s%s: %s " % (m['type'], m['errno'], 543 | m['msg'].strip()) 544 | speak(PYLINTER_ERRORS[view_id][line_num]) 545 | 546 | if len(PYLINTER_ERRORS[view_id]) <= 1: 547 | speak("No errors found") 548 | 549 | PylinterCommand.show_errors(self.view) 550 | 551 | 552 | class BackgroundPylinter(sublime_plugin.EventListener): 553 | """ Process Sublime Text events """ 554 | def _last_selected_lineno(self, view): 555 | return view.rowcol(view.sel()[0].end())[0] 556 | 557 | def on_post_save(self, view): 558 | """ Run Pylint on file save """ 559 | if (view.file_name().endswith('.py') and 560 | PylSet.get_or('run_on_save', False)): 561 | view.run_command('pylinter') 562 | 563 | def on_selection_modified(self, view): 564 | """ Show errors in the status line when the carret/selection moves """ 565 | global LAST_SELECTED_LINE, STATUS_ACTIVE 566 | view_id = view.id() 567 | if view_id in PYLINTER_ERRORS: 568 | new_selected_line = self._last_selected_lineno(view) 569 | if new_selected_line != LAST_SELECTED_LINE: 570 | LAST_SELECTED_LINE = new_selected_line 571 | if LAST_SELECTED_LINE in PYLINTER_ERRORS[view_id]: 572 | err_str = PYLINTER_ERRORS[view_id][LAST_SELECTED_LINE] 573 | if PylSet.get_or("message_stay", False): 574 | view.set_status('Pylinter', err_str) 575 | STATUS_ACTIVE = True 576 | else: 577 | sublime.status_message(err_str) 578 | elif STATUS_ACTIVE: 579 | view.erase_status('Pylinter') 580 | STATUS_ACTIVE = False 581 | 582 | # In SublimeText 2, we need to call this manually. 583 | if not ST3: 584 | plugin_loaded() 585 | --------------------------------------------------------------------------------