Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 1 | # -*- coding: utf-8; mode: python; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- vim:fenc=utf-8:ft=python:et:sw=4:ts=4:sts=4 |
Sylvain Thénault | ac825be | 2013-06-19 13:58:46 +0200 | [diff] [blame] | 2 | # Copyright (c) 2003-2013 LOGILAB S.A. (Paris, FRANCE). |
Sylvain Thénault | 4b316ba | 2012-09-14 13:25:22 +0200 | [diff] [blame] | 3 | # http://www.logilab.fr/ -- mailto:contact@logilab.fr |
| 4 | # |
| 5 | # This program is free software; you can redistribute it and/or modify it under |
| 6 | # the terms of the GNU General Public License as published by the Free Software |
| 7 | # Foundation; either version 2 of the License, or (at your option) any later |
| 8 | # version. |
| 9 | # |
| 10 | # This program is distributed in the hope that it will be useful, but WITHOUT |
| 11 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
| 12 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details |
| 13 | # |
| 14 | # You should have received a copy of the GNU General Public License along with |
| 15 | # this program; if not, write to the Free Software Foundation, Inc., |
| 16 | # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 17 | """Emacs and Flymake compatible Pylint. |
Sylvain Thénault | 981405e | 2008-12-03 09:57:14 +0100 | [diff] [blame] | 18 | |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 19 | This script is for integration with emacs and is compatible with flymake mode. |
| 20 | |
Emile Anclin | 85f4f8a | 2010-12-15 19:14:21 +0100 | [diff] [blame] | 21 | epylint walks out of python packages before invoking pylint. This avoids |
| 22 | reporting import errors that occur when a module within a package uses the |
| 23 | absolute import path to get another module within this package. |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 24 | |
| 25 | For example: |
| 26 | - Suppose a package is structured as |
| 27 | |
| 28 | a/__init__.py |
| 29 | a/b/x.py |
| 30 | a/c/y.py |
| 31 | |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 32 | - Then if y.py imports x as "from a.b import x" the following produces pylint |
| 33 | errors |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 34 | |
| 35 | cd a/c; pylint y.py |
| 36 | |
Sylvain Thénault | 8746b95 | 2009-11-23 15:15:26 +0100 | [diff] [blame] | 37 | - The following obviously doesn't |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 38 | |
| 39 | pylint a/c/y.py |
| 40 | |
Emile Anclin | 85f4f8a | 2010-12-15 19:14:21 +0100 | [diff] [blame] | 41 | - As this script will be invoked by emacs within the directory of the file |
| 42 | we are checking we need to go out of it to avoid these false positives. |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 43 | |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 44 | |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 45 | You may also use py_run to run pylint with desired options and get back (or not) |
| 46 | its output. |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 47 | """ |
| 48 | |
| 49 | import sys, os, re |
Sylvain Thénault | 99196f2 | 2013-12-22 23:41:57 +0100 | [diff] [blame] | 50 | import os.path as osp |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 51 | from subprocess import Popen, PIPE |
| 52 | |
Manuel Vázquez Acosta | 2aecb5d | 2014-01-06 16:07:33 -0500 | [diff] [blame^] | 53 | def _get_env(): |
| 54 | '''Extracts the environment PYTHONPATH and appends the current sys.path to |
| 55 | those.''' |
| 56 | env = dict(os.environ) |
| 57 | pythonpath = env.get('PYTHONPATH', '') |
| 58 | currentpaths = os.pathsep.join(sys.path) |
| 59 | if pythonpath: |
| 60 | pythonpath += os.pathsep + currentpaths |
| 61 | else: |
| 62 | pythonpath = currentpaths |
| 63 | env['PYTHONPATH'] = pythonpath |
| 64 | return env |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 65 | |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 66 | def lint(filename, options=None): |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 67 | """Pylint the given file. |
| 68 | |
Sylvain Thénault | 177e7e5 | 2013-10-07 12:17:27 +0200 | [diff] [blame] | 69 | When run from emacs we will be in the directory of a file, and passed its |
| 70 | filename. If this file is part of a package and is trying to import other |
| 71 | modules from within its own package or another package rooted in a directory |
| 72 | below it, pylint will classify it as a failed import. |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 73 | |
Sylvain Thénault | 177e7e5 | 2013-10-07 12:17:27 +0200 | [diff] [blame] | 74 | To get around this, we traverse down the directory tree to find the root of |
| 75 | the package this module is in. We then invoke pylint from this directory. |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 76 | |
Sylvain Thénault | 177e7e5 | 2013-10-07 12:17:27 +0200 | [diff] [blame] | 77 | Finally, we must correct the filenames in the output generated by pylint so |
| 78 | Emacs doesn't become confused (it will expect just the original filename, |
| 79 | while pylint may extend it with extra directories if we've traversed down |
| 80 | the tree) |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 81 | """ |
| 82 | # traverse downwards until we are out of a python package |
Sylvain Thénault | 99196f2 | 2013-12-22 23:41:57 +0100 | [diff] [blame] | 83 | full_path = osp.abspath(filename) |
| 84 | parent_path = osp.dirname(full_path) |
| 85 | child_path = osp.basename(full_path) |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 86 | |
Sylvain Thénault | 99196f2 | 2013-12-22 23:41:57 +0100 | [diff] [blame] | 87 | while parent_path != "/" and osp.exists(osp.join(parent_path, '__init__.py')): |
| 88 | child_path = osp.join(osp.basename(parent_path), child_path) |
| 89 | parent_path = osp.dirname(parent_path) |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 90 | |
| 91 | # Start pylint |
Sylvain Thénault | c8e20f7 | 2012-01-09 21:25:10 +0100 | [diff] [blame] | 92 | # Ensure we use the python and pylint associated with the running epylint |
Sylvain Thénault | 99196f2 | 2013-12-22 23:41:57 +0100 | [diff] [blame] | 93 | from pylint import lint as lint_mod |
| 94 | lint_path = lint_mod.__file__ |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 95 | options = options or ['--disable=C,R,I'] |
Sylvain Thénault | 99196f2 | 2013-12-22 23:41:57 +0100 | [diff] [blame] | 96 | cmd = [sys.executable, lint_path] + options + ['--msg-template', |
| 97 | '{path}:{line}: [{symbol}, {obj}] {msg}', '-r', 'n', child_path] |
Manuel Vázquez Acosta | 2aecb5d | 2014-01-06 16:07:33 -0500 | [diff] [blame^] | 98 | process = Popen(cmd, stdout=PIPE, cwd=parent_path, env=_get_env(), |
Manuel Vázquez Acosta | 2da336b | 2014-01-06 15:31:19 -0500 | [diff] [blame] | 99 | universal_newlines=True) |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 100 | |
| 101 | # The parseable line format is '%(path)s:%(line)s: [%(sigle)s%(obj)s] %(msg)s' |
| 102 | # NOTE: This would be cleaner if we added an Emacs reporter to pylint.reporters.text .. |
| 103 | regex = re.compile(r"\[(?P<type>[WE])(?P<remainder>.*?)\]") |
| 104 | |
Sylvain Thénault | 99196f2 | 2013-12-22 23:41:57 +0100 | [diff] [blame] | 105 | def _replacement(match_object): |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 106 | "Alter to include 'Error' or 'Warning'" |
Sylvain Thénault | 99196f2 | 2013-12-22 23:41:57 +0100 | [diff] [blame] | 107 | if match_object.group("type") == "W": |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 108 | replacement = "Warning" |
| 109 | else: |
| 110 | replacement = "Error" |
| 111 | # replace as "Warning (W0511, funcName): Warning Text" |
Sylvain Thénault | 99196f2 | 2013-12-22 23:41:57 +0100 | [diff] [blame] | 112 | return "%s (%s%s):" % (replacement, match_object.group("type"), |
| 113 | match_object.group("remainder")) |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 114 | |
Sylvain Thénault | c8e20f7 | 2012-01-09 21:25:10 +0100 | [diff] [blame] | 115 | for line in process.stdout: |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 116 | # remove pylintrc warning |
| 117 | if line.startswith("No config file found"): |
| 118 | continue |
| 119 | line = regex.sub(_replacement, line, 1) |
| 120 | # modify the file name thats output to reverse the path traversal we made |
| 121 | parts = line.split(":") |
Sylvain Thénault | 99196f2 | 2013-12-22 23:41:57 +0100 | [diff] [blame] | 122 | if parts and parts[0] == child_path: |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 123 | line = ":".join([filename] + parts[1:]) |
| 124 | print line, |
| 125 | |
Sylvain Thénault | c8e20f7 | 2012-01-09 21:25:10 +0100 | [diff] [blame] | 126 | process.wait() |
| 127 | return process.returncode |
Sylvain Thénault | 981405e | 2008-12-03 09:57:14 +0100 | [diff] [blame] | 128 | |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 129 | |
| 130 | def py_run(command_options='', return_std=False, stdout=None, stderr=None, |
Emile Anclin | 85f4f8a | 2010-12-15 19:14:21 +0100 | [diff] [blame] | 131 | script='epylint'): |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 132 | """Run pylint from python |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 133 | |
| 134 | ``command_options`` is a string containing ``pylint`` command line options; |
| 135 | ``return_std`` (boolean) indicates return of created standart output |
| 136 | and error (see below); |
| 137 | ``stdout`` and ``stderr`` are 'file-like' objects in which standart output |
| 138 | could be written. |
| 139 | |
| 140 | Calling agent is responsible for stdout/err management (creation, close). |
| 141 | Default standart output and error are those from sys, |
| 142 | or standalone ones (``subprocess.PIPE``) are used |
| 143 | if they are not set and ``return_std``. |
| 144 | |
| 145 | If ``return_std`` is set to ``True``, this function returns a 2-uple |
| 146 | containing standart output and error related to created process, |
| 147 | as follows: ``(stdout, stderr)``. |
| 148 | |
| 149 | A trivial usage could be as follows: |
| 150 | >>> py_run( '--version') |
| 151 | No config file found, using default configuration |
| 152 | pylint 0.18.1, |
| 153 | ... |
| 154 | |
| 155 | To silently run Pylint on a module, and get its standart output and error: |
| 156 | >>> (pylint_stdout, pylint_stderr) = py_run( 'module_name.py', True) |
| 157 | """ |
| 158 | # Create command line to call pylint |
| 159 | if os.name == 'nt': |
| 160 | script += '.bat' |
| 161 | command_line = script + ' ' + command_options |
| 162 | # Providing standart output and/or error if not set |
| 163 | if stdout is None: |
| 164 | if return_std: |
| 165 | stdout = PIPE |
| 166 | else: |
| 167 | stdout = sys.stdout |
| 168 | if stderr is None: |
| 169 | if return_std: |
| 170 | stderr = PIPE |
| 171 | else: |
| 172 | stderr = sys.stderr |
| 173 | # Call pylint in a subprocess |
Sylvain Thénault | 177e7e5 | 2013-10-07 12:17:27 +0200 | [diff] [blame] | 174 | p = Popen(command_line, shell=True, stdout=stdout, stderr=stderr, |
Manuel Vázquez Acosta | 2aecb5d | 2014-01-06 16:07:33 -0500 | [diff] [blame^] | 175 | env=_get_env(), universal_newlines=True) |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 176 | p.wait() |
| 177 | # Return standart output and error |
| 178 | if return_std: |
| 179 | return (p.stdout, p.stderr) |
| 180 | |
| 181 | |
Sylvain Thénault | 4b316ba | 2012-09-14 13:25:22 +0200 | [diff] [blame] | 182 | def Run(): |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 183 | if len(sys.argv) == 1: |
| 184 | print "Usage: %s <filename> [options]" % sys.argv[0] |
| 185 | sys.exit(1) |
Sylvain Thénault | 99196f2 | 2013-12-22 23:41:57 +0100 | [diff] [blame] | 186 | elif not osp.exists(sys.argv[1]): |
Sylvain Thénault | 177e7e5 | 2013-10-07 12:17:27 +0200 | [diff] [blame] | 187 | print "%s does not exist" % sys.argv[1] |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 188 | sys.exit(1) |
| 189 | else: |
Sylvain Thénault | 177e7e5 | 2013-10-07 12:17:27 +0200 | [diff] [blame] | 190 | sys.exit(lint(sys.argv[1], sys.argv[1:])) |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 191 | |
Sylvain Thénault | 4b316ba | 2012-09-14 13:25:22 +0200 | [diff] [blame] | 192 | |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 193 | if __name__ == '__main__': |
Sylvain Thénault | 4b316ba | 2012-09-14 13:25:22 +0200 | [diff] [blame] | 194 | Run() |