Claudiu Popa | ec09cc7 | 2016-07-23 00:22:28 +0300 | [diff] [blame] | 1 | # mode: python; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 |
Mihai Balint | e0f2c50 | 2015-02-28 17:13:21 +0200 | [diff] [blame] | 2 | # -*- vim:fenc=utf-8:ft=python:et:sw=4:ts=4:sts=4 |
Claudiu Popa | 8322781 | 2016-06-01 16:11:29 +0100 | [diff] [blame] | 3 | |
Claudiu Popa | ec09cc7 | 2016-07-23 00:22:28 +0300 | [diff] [blame] | 4 | # Copyright (c) 2008-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr> |
Claudiu Popa | 9ff44de | 2017-12-15 12:24:15 +0100 | [diff] [blame] | 5 | # Copyright (c) 2014 Jakob Normark <jakobnormark@gmail.com> |
| 6 | # Copyright (c) 2014 Brett Cannon <brett@python.org> |
Claudiu Popa | ec09cc7 | 2016-07-23 00:22:28 +0300 | [diff] [blame] | 7 | # Copyright (c) 2014 Manuel Vázquez Acosta <mva.led@gmail.com> |
Claudiu Popa | 9ff44de | 2017-12-15 12:24:15 +0100 | [diff] [blame] | 8 | # Copyright (c) 2014 Derek Harland <derek.harland@finq.co.nz> |
| 9 | # Copyright (c) 2014 Arun Persaud <arun@nubati.net> |
hippo91 | 1f7c29c | 2020-08-20 18:40:19 +0200 | [diff] [blame] | 10 | # Copyright (c) 2015-2020 Claudiu Popa <pcmanticore@gmail.com> |
Claudiu Popa | 9ff44de | 2017-12-15 12:24:15 +0100 | [diff] [blame] | 11 | # Copyright (c) 2015 Mihai Balint <balint.mihai@gmail.com> |
| 12 | # Copyright (c) 2015 Ionel Cristian Maries <contact@ionelmc.ro> |
Pierre Sassoulas | ac85223 | 2021-02-21 15:13:06 +0100 | [diff] [blame] | 13 | # Copyright (c) 2017, 2020 hippo91 <guillaume.peillex@gmail.com> |
Claudiu Popa | 9ff44de | 2017-12-15 12:24:15 +0100 | [diff] [blame] | 14 | # Copyright (c) 2017 Daniela Plascencia <daplascen@gmail.com> |
Claudiu Popa | 3a8635a | 2018-07-15 11:36:36 +0200 | [diff] [blame] | 15 | # Copyright (c) 2018 Sushobhit <31987769+sushobhit27@users.noreply.github.com> |
| 16 | # Copyright (c) 2018 Ryan McGuire <ryan@enigmacurry.com> |
| 17 | # Copyright (c) 2018 thernstig <30827238+thernstig@users.noreply.github.com> |
| 18 | # Copyright (c) 2018 Radostin Stoyanov <rst0git@users.noreply.github.com> |
Pierre Sassoulas | ac85223 | 2021-02-21 15:13:06 +0100 | [diff] [blame] | 19 | # Copyright (c) 2019, 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com> |
Claudiu Popa | 369d952 | 2020-04-27 11:04:14 +0200 | [diff] [blame] | 20 | # Copyright (c) 2019 Hugo van Kemenade <hugovk@users.noreply.github.com> |
hippo91 | 1f7c29c | 2020-08-20 18:40:19 +0200 | [diff] [blame] | 21 | # Copyright (c) 2020 Damien Baty <damien.baty@polyconseil.fr> |
Claudiu Popa | 369d952 | 2020-04-27 11:04:14 +0200 | [diff] [blame] | 22 | # Copyright (c) 2020 Anthony Sottile <asottile@umich.edu> |
Pierre Sassoulas | aa688de | 2021-07-01 14:33:09 +0200 | [diff] [blame] | 23 | # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com> |
Pierre Sassoulas | 7dece5b | 2021-04-24 21:26:46 +0200 | [diff] [blame] | 24 | # Copyright (c) 2021 Andreas Finkler <andi.finkler@gmail.com> |
Claudiu Popa | ec09cc7 | 2016-07-23 00:22:28 +0300 | [diff] [blame] | 25 | |
Claudiu Popa | 8322781 | 2016-06-01 16:11:29 +0100 | [diff] [blame] | 26 | # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html |
Marc Mueller | 2129086 | 2021-07-01 12:47:58 +0200 | [diff] [blame] | 27 | # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE |
Claudiu Popa | 8322781 | 2016-06-01 16:11:29 +0100 | [diff] [blame] | 28 | |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 29 | """Emacs and Flymake compatible Pylint. |
Sylvain Thénault | 981405e | 2008-12-03 09:57:14 +0100 | [diff] [blame] | 30 | |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 31 | This script is for integration with emacs and is compatible with flymake mode. |
| 32 | |
Emile Anclin | 85f4f8a | 2010-12-15 19:14:21 +0100 | [diff] [blame] | 33 | epylint walks out of python packages before invoking pylint. This avoids |
| 34 | reporting import errors that occur when a module within a package uses the |
| 35 | absolute import path to get another module within this package. |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 36 | |
| 37 | For example: |
| 38 | - Suppose a package is structured as |
| 39 | |
| 40 | a/__init__.py |
| 41 | a/b/x.py |
| 42 | a/c/y.py |
| 43 | |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 44 | - Then if y.py imports x as "from a.b import x" the following produces pylint |
| 45 | errors |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 46 | |
| 47 | cd a/c; pylint y.py |
| 48 | |
Sylvain Thénault | 8746b95 | 2009-11-23 15:15:26 +0100 | [diff] [blame] | 49 | - The following obviously doesn't |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 50 | |
| 51 | pylint a/c/y.py |
| 52 | |
Emile Anclin | 85f4f8a | 2010-12-15 19:14:21 +0100 | [diff] [blame] | 53 | - As this script will be invoked by emacs within the directory of the file |
| 54 | 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] | 55 | |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 56 | |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 57 | You may also use py_run to run pylint with desired options and get back (or not) |
| 58 | its output. |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 59 | """ |
Claudiu Popa | 28bd68d | 2015-10-02 18:09:09 +0300 | [diff] [blame] | 60 | import os |
Claudiu Popa | bcd687e | 2016-07-19 16:23:00 +0300 | [diff] [blame] | 61 | import shlex |
Pierre Sassoulas | 5ab140a | 2019-03-09 11:22:36 +0100 | [diff] [blame] | 62 | import sys |
Sushobhit | 8c5256d | 2018-05-27 10:42:14 +0530 | [diff] [blame] | 63 | from io import StringIO |
Pierre Sassoulas | 5ab140a | 2019-03-09 11:22:36 +0100 | [diff] [blame] | 64 | from subprocess import PIPE, Popen |
Claudiu Popa | 1cdb8bc | 2016-03-28 18:46:48 +0100 | [diff] [blame] | 65 | |
| 66 | |
Manuel Vázquez Acosta | 2aecb5d | 2014-01-06 16:07:33 -0500 | [diff] [blame] | 67 | def _get_env(): |
Claudiu Popa | 3f28424 | 2018-09-16 17:33:50 +0200 | [diff] [blame] | 68 | """Extracts the environment PYTHONPATH and appends the current sys.path to |
| 69 | those.""" |
Manuel Vázquez Acosta | 2aecb5d | 2014-01-06 16:07:33 -0500 | [diff] [blame] | 70 | env = dict(os.environ) |
Claudiu Popa | 3f28424 | 2018-09-16 17:33:50 +0200 | [diff] [blame] | 71 | env["PYTHONPATH"] = os.pathsep.join(sys.path) |
Manuel Vázquez Acosta | 2aecb5d | 2014-01-06 16:07:33 -0500 | [diff] [blame] | 72 | return env |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 73 | |
Claudiu Popa | 3f28424 | 2018-09-16 17:33:50 +0200 | [diff] [blame] | 74 | |
Ryan McGuire | 39338dc | 2018-04-07 03:34:51 -0400 | [diff] [blame] | 75 | def lint(filename, options=()): |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 76 | """Pylint the given file. |
| 77 | |
Sylvain Thénault | 177e7e5 | 2013-10-07 12:17:27 +0200 | [diff] [blame] | 78 | When run from emacs we will be in the directory of a file, and passed its |
| 79 | filename. If this file is part of a package and is trying to import other |
| 80 | modules from within its own package or another package rooted in a directory |
| 81 | below it, pylint will classify it as a failed import. |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 82 | |
Sylvain Thénault | 177e7e5 | 2013-10-07 12:17:27 +0200 | [diff] [blame] | 83 | To get around this, we traverse down the directory tree to find the root of |
| 84 | 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] | 85 | |
Sylvain Thénault | 177e7e5 | 2013-10-07 12:17:27 +0200 | [diff] [blame] | 86 | Finally, we must correct the filenames in the output generated by pylint so |
| 87 | Emacs doesn't become confused (it will expect just the original filename, |
| 88 | while pylint may extend it with extra directories if we've traversed down |
| 89 | the tree) |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 90 | """ |
| 91 | # traverse downwards until we are out of a python package |
Pierre Sassoulas | 3a2a579 | 2021-03-30 23:40:38 +0200 | [diff] [blame] | 92 | full_path = os.path.abspath(filename) |
| 93 | parent_path = os.path.dirname(full_path) |
| 94 | child_path = os.path.basename(full_path) |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 95 | |
Pierre Sassoulas | 3a2a579 | 2021-03-30 23:40:38 +0200 | [diff] [blame] | 96 | while parent_path != "/" and os.path.exists( |
| 97 | os.path.join(parent_path, "__init__.py") |
| 98 | ): |
| 99 | child_path = os.path.join(os.path.basename(parent_path), child_path) |
| 100 | parent_path = os.path.dirname(parent_path) |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 101 | |
| 102 | # Start pylint |
Sylvain Thénault | c8e20f7 | 2012-01-09 21:25:10 +0100 | [diff] [blame] | 103 | # Ensure we use the python and pylint associated with the running epylint |
Claudiu Popa | 9aa5aa5 | 2016-06-23 16:25:44 +0100 | [diff] [blame] | 104 | run_cmd = "import sys; from pylint.lint import Run; Run(sys.argv[1:])" |
Claudiu Popa | 3f28424 | 2018-09-16 17:33:50 +0200 | [diff] [blame] | 105 | cmd = ( |
| 106 | [sys.executable, "-c", run_cmd] |
| 107 | + [ |
| 108 | "--msg-template", |
| 109 | "{path}:{line}: {category} ({msg_id}, {symbol}, {obj}) {msg}", |
| 110 | "-r", |
| 111 | "n", |
| 112 | child_path, |
| 113 | ] |
| 114 | + list(options) |
| 115 | ) |
DudeNr33 | 922f389 | 2021-04-23 20:31:21 +0200 | [diff] [blame] | 116 | |
| 117 | with Popen( |
Claudiu Popa | 3f28424 | 2018-09-16 17:33:50 +0200 | [diff] [blame] | 118 | cmd, stdout=PIPE, cwd=parent_path, env=_get_env(), universal_newlines=True |
DudeNr33 | 922f389 | 2021-04-23 20:31:21 +0200 | [diff] [blame] | 119 | ) as process: |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 120 | |
DudeNr33 | 922f389 | 2021-04-23 20:31:21 +0200 | [diff] [blame] | 121 | for line in process.stdout: |
| 122 | # remove pylintrc warning |
| 123 | if line.startswith("No config file found"): |
| 124 | continue |
Manuel Vázquez Acosta | f8fdbc3 | 2014-04-14 16:57:25 -0400 | [diff] [blame] | 125 | |
DudeNr33 | 922f389 | 2021-04-23 20:31:21 +0200 | [diff] [blame] | 126 | # modify the file name thats output to reverse the path traversal we made |
| 127 | parts = line.split(":") |
| 128 | if parts and parts[0] == child_path: |
| 129 | line = ":".join([filename] + parts[1:]) |
| 130 | print(line, end=" ") |
Sylvain Thénault | 6a77063 | 2009-08-06 10:24:42 +0200 | [diff] [blame] | 131 | |
DudeNr33 | 922f389 | 2021-04-23 20:31:21 +0200 | [diff] [blame] | 132 | process.wait() |
| 133 | return process.returncode |
Sylvain Thénault | 981405e | 2008-12-03 09:57:14 +0100 | [diff] [blame] | 134 | |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 135 | |
Claudiu Popa | 3f28424 | 2018-09-16 17:33:50 +0200 | [diff] [blame] | 136 | def py_run(command_options="", return_std=False, stdout=None, stderr=None): |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 137 | """Run pylint from python |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 138 | |
| 139 | ``command_options`` is a string containing ``pylint`` command line options; |
Jakob Normark | 962ade1 | 2014-11-06 18:56:20 +0100 | [diff] [blame] | 140 | ``return_std`` (boolean) indicates return of created standard output |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 141 | and error (see below); |
Jakob Normark | 962ade1 | 2014-11-06 18:56:20 +0100 | [diff] [blame] | 142 | ``stdout`` and ``stderr`` are 'file-like' objects in which standard output |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 143 | could be written. |
| 144 | |
| 145 | Calling agent is responsible for stdout/err management (creation, close). |
Jakob Normark | 962ade1 | 2014-11-06 18:56:20 +0100 | [diff] [blame] | 146 | Default standard output and error are those from sys, |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 147 | or standalone ones (``subprocess.PIPE``) are used |
| 148 | if they are not set and ``return_std``. |
| 149 | |
| 150 | If ``return_std`` is set to ``True``, this function returns a 2-uple |
Jakob Normark | 962ade1 | 2014-11-06 18:56:20 +0100 | [diff] [blame] | 151 | containing standard output and error related to created process, |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 152 | as follows: ``(stdout, stderr)``. |
| 153 | |
Jakob Normark | 962ade1 | 2014-11-06 18:56:20 +0100 | [diff] [blame] | 154 | To silently run Pylint on a module, and get its standard output and error: |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 155 | >>> (pylint_stdout, pylint_stderr) = py_run( 'module_name.py', True) |
| 156 | """ |
Claudiu Popa | 95232ca | 2019-03-28 09:33:20 +0100 | [diff] [blame] | 157 | # Detect if we use Python as executable or not, else default to `python` |
| 158 | executable = sys.executable if "python" in sys.executable else "python" |
| 159 | |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 160 | # Create command line to call pylint |
Claudiu Popa | 95232ca | 2019-03-28 09:33:20 +0100 | [diff] [blame] | 161 | epylint_part = [executable, "-c", "from pylint import epylint;epylint.Run()"] |
Claudiu Popa | 3f28424 | 2018-09-16 17:33:50 +0200 | [diff] [blame] | 162 | options = shlex.split(command_options, posix=not sys.platform.startswith("win")) |
Claudiu Popa | bcd687e | 2016-07-19 16:23:00 +0300 | [diff] [blame] | 163 | cli = epylint_part + options |
| 164 | |
Jakob Normark | 962ade1 | 2014-11-06 18:56:20 +0100 | [diff] [blame] | 165 | # Providing standard output and/or error if not set |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 166 | if stdout is None: |
| 167 | if return_std: |
| 168 | stdout = PIPE |
| 169 | else: |
| 170 | stdout = sys.stdout |
| 171 | if stderr is None: |
| 172 | if return_std: |
| 173 | stderr = PIPE |
| 174 | else: |
| 175 | stderr = sys.stderr |
| 176 | # Call pylint in a subprocess |
DudeNr33 | 922f389 | 2021-04-23 20:31:21 +0200 | [diff] [blame] | 177 | with Popen( |
Claudiu Popa | 3f28424 | 2018-09-16 17:33:50 +0200 | [diff] [blame] | 178 | cli, |
| 179 | shell=False, |
| 180 | stdout=stdout, |
| 181 | stderr=stderr, |
| 182 | env=_get_env(), |
| 183 | universal_newlines=True, |
DudeNr33 | 922f389 | 2021-04-23 20:31:21 +0200 | [diff] [blame] | 184 | ) as process: |
| 185 | proc_stdout, proc_stderr = process.communicate() |
| 186 | # Return standard output and error |
| 187 | if return_std: |
| 188 | return StringIO(proc_stdout), StringIO(proc_stderr) |
| 189 | return None |
Sylvain Thénault | 1c06c67 | 2009-12-18 11:34:04 +0100 | [diff] [blame] | 190 | |
| 191 | |
Sylvain Thénault | 4b316ba | 2012-09-14 13:25:22 +0200 | [diff] [blame] | 192 | def Run(): |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 193 | if len(sys.argv) == 1: |
Brett Cannon | 29bb89f | 2014-08-29 11:16:29 -0400 | [diff] [blame] | 194 | print("Usage: %s <filename> [options]" % sys.argv[0]) |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 195 | sys.exit(1) |
Pierre Sassoulas | 3a2a579 | 2021-03-30 23:40:38 +0200 | [diff] [blame] | 196 | elif not os.path.exists(sys.argv[1]): |
Brett Cannon | 29bb89f | 2014-08-29 11:16:29 -0400 | [diff] [blame] | 197 | print("%s does not exist" % sys.argv[1]) |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 198 | sys.exit(1) |
| 199 | else: |
Manuel Vázquez Acosta | f8fdbc3 | 2014-04-14 16:57:25 -0400 | [diff] [blame] | 200 | sys.exit(lint(sys.argv[1], sys.argv[2:])) |
Sylvain Thénault | ba979be | 2013-09-03 12:22:43 +0200 | [diff] [blame] | 201 | |
Sylvain Thénault | 4b316ba | 2012-09-14 13:25:22 +0200 | [diff] [blame] | 202 | |
Claudiu Popa | 3f28424 | 2018-09-16 17:33:50 +0200 | [diff] [blame] | 203 | if __name__ == "__main__": |
Sylvain Thénault | 4b316ba | 2012-09-14 13:25:22 +0200 | [diff] [blame] | 204 | Run() |