epylint.py 7.3 KB

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