run.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
  4. from __future__ import annotations
  5. import os
  6. import sys
  7. import warnings
  8. from collections.abc import Sequence
  9. from pathlib import Path
  10. from typing import Any, ClassVar
  11. from pylint import config
  12. from pylint.checkers.utils import clear_lru_caches
  13. from pylint.config._pylint_config import (
  14. _handle_pylint_config_commands,
  15. _register_generate_config_options,
  16. )
  17. from pylint.config.config_initialization import _config_initialization
  18. from pylint.config.exceptions import ArgumentPreprocessingError
  19. from pylint.config.utils import _preprocess_options
  20. from pylint.constants import full_version
  21. from pylint.lint.base_options import _make_run_options
  22. from pylint.lint.pylinter import MANAGER, PyLinter
  23. from pylint.reporters.base_reporter import BaseReporter
  24. try:
  25. import multiprocessing
  26. from multiprocessing import synchronize # noqa pylint: disable=unused-import
  27. except ImportError:
  28. multiprocessing = None # type: ignore[assignment]
  29. try:
  30. from concurrent.futures import ProcessPoolExecutor
  31. except ImportError:
  32. ProcessPoolExecutor = None # type: ignore[assignment,misc]
  33. def _query_cpu() -> int | None:
  34. """Try to determine number of CPUs allotted in a docker container.
  35. This is based on discussion and copied from suggestions in
  36. https://bugs.python.org/issue36054.
  37. """
  38. cpu_quota, avail_cpu = None, None
  39. if Path("/sys/fs/cgroup/cpu/cpu.cfs_quota_us").is_file():
  40. with open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", encoding="utf-8") as file:
  41. # Not useful for AWS Batch based jobs as result is -1, but works on local linux systems
  42. cpu_quota = int(file.read().rstrip())
  43. if (
  44. cpu_quota
  45. and cpu_quota != -1
  46. and Path("/sys/fs/cgroup/cpu/cpu.cfs_period_us").is_file()
  47. ):
  48. with open("/sys/fs/cgroup/cpu/cpu.cfs_period_us", encoding="utf-8") as file:
  49. cpu_period = int(file.read().rstrip())
  50. # Divide quota by period and you should get num of allotted CPU to the container,
  51. # rounded down if fractional.
  52. avail_cpu = int(cpu_quota / cpu_period)
  53. elif Path("/sys/fs/cgroup/cpu/cpu.shares").is_file():
  54. with open("/sys/fs/cgroup/cpu/cpu.shares", encoding="utf-8") as file:
  55. cpu_shares = int(file.read().rstrip())
  56. # For AWS, gives correct value * 1024.
  57. avail_cpu = int(cpu_shares / 1024)
  58. # In K8s Pods also a fraction of a single core could be available
  59. # As multiprocessing is not able to run only a "fraction" of process
  60. # assume we have 1 CPU available
  61. if avail_cpu == 0:
  62. avail_cpu = 1
  63. return avail_cpu
  64. def _cpu_count() -> int:
  65. """Use sched_affinity if available for virtualized or containerized
  66. environments.
  67. """
  68. cpu_share = _query_cpu()
  69. cpu_count = None
  70. sched_getaffinity = getattr(os, "sched_getaffinity", None)
  71. # pylint: disable=not-callable,using-constant-test,useless-suppression
  72. if sched_getaffinity:
  73. cpu_count = len(sched_getaffinity(0))
  74. elif multiprocessing:
  75. cpu_count = multiprocessing.cpu_count()
  76. else:
  77. cpu_count = 1
  78. if sys.platform == "win32":
  79. # See also https://github.com/python/cpython/issues/94242
  80. cpu_count = min(cpu_count, 56) # pragma: no cover
  81. if cpu_share is not None:
  82. return min(cpu_share, cpu_count)
  83. return cpu_count
  84. UNUSED_PARAM_SENTINEL = object()
  85. class Run:
  86. """Helper class to use as main for pylint with 'run(*sys.argv[1:])'."""
  87. LinterClass = PyLinter
  88. option_groups = (
  89. (
  90. "Commands",
  91. "Options which are actually commands. Options in this \
  92. group are mutually exclusive.",
  93. ),
  94. )
  95. _is_pylint_config: ClassVar[bool] = False
  96. """Boolean whether or not this is a 'pylint-config' run.
  97. Used by _PylintConfigRun to make the 'pylint-config' command work.
  98. """
  99. # pylint: disable = too-many-statements, too-many-branches
  100. def __init__(
  101. self,
  102. args: Sequence[str],
  103. reporter: BaseReporter | None = None,
  104. exit: bool = True, # pylint: disable=redefined-builtin
  105. do_exit: Any = UNUSED_PARAM_SENTINEL,
  106. ) -> None:
  107. # Immediately exit if user asks for version
  108. if "--version" in args:
  109. print(full_version)
  110. sys.exit(0)
  111. self._rcfile: str | None = None
  112. self._output: str | None = None
  113. self._plugins: list[str] = []
  114. self.verbose: bool = False
  115. # Pre-process certain options and remove them from args list
  116. try:
  117. args = _preprocess_options(self, args)
  118. except ArgumentPreprocessingError as ex:
  119. print(ex, file=sys.stderr)
  120. sys.exit(32)
  121. # Determine configuration file
  122. if self._rcfile is None:
  123. default_file = next(config.find_default_config_files(), None)
  124. if default_file:
  125. self._rcfile = str(default_file)
  126. self.linter = linter = self.LinterClass(
  127. _make_run_options(self),
  128. option_groups=self.option_groups,
  129. pylintrc=self._rcfile,
  130. )
  131. # register standard checkers
  132. linter.load_default_plugins()
  133. # load command line plugins
  134. linter.load_plugin_modules(self._plugins)
  135. linter.disable("I")
  136. linter.enable("c-extension-no-member")
  137. # Register the options needed for 'pylint-config'
  138. # By not registering them by default they don't show up in the normal usage message
  139. if self._is_pylint_config:
  140. _register_generate_config_options(linter._arg_parser)
  141. args = _config_initialization(
  142. linter, args, reporter, config_file=self._rcfile, verbose_mode=self.verbose
  143. )
  144. # Handle the 'pylint-config' command
  145. if self._is_pylint_config:
  146. warnings.warn(
  147. "NOTE: The 'pylint-config' command is experimental and usage can change",
  148. UserWarning,
  149. )
  150. code = _handle_pylint_config_commands(linter)
  151. if exit:
  152. sys.exit(code)
  153. return
  154. # Display help messages if there are no files to lint
  155. if not args:
  156. print(linter.help())
  157. sys.exit(32)
  158. if linter.config.jobs < 0:
  159. print(
  160. f"Jobs number ({linter.config.jobs}) should be greater than or equal to 0",
  161. file=sys.stderr,
  162. )
  163. sys.exit(32)
  164. if linter.config.jobs > 1 or linter.config.jobs == 0:
  165. if ProcessPoolExecutor is None:
  166. print(
  167. "concurrent.futures module is missing, fallback to single process",
  168. file=sys.stderr,
  169. )
  170. linter.set_option("jobs", 1)
  171. elif linter.config.jobs == 0:
  172. linter.config.jobs = _cpu_count()
  173. if self._output:
  174. try:
  175. with open(self._output, "w", encoding="utf-8") as output:
  176. linter.reporter.out = output
  177. linter.check(args)
  178. score_value = linter.generate_reports()
  179. except OSError as ex:
  180. print(ex, file=sys.stderr)
  181. sys.exit(32)
  182. else:
  183. linter.check(args)
  184. score_value = linter.generate_reports()
  185. if do_exit is not UNUSED_PARAM_SENTINEL:
  186. warnings.warn(
  187. "do_exit is deprecated and it is going to be removed in a future version.",
  188. DeprecationWarning,
  189. )
  190. exit = do_exit
  191. if linter.config.clear_cache_post_run:
  192. clear_lru_caches()
  193. MANAGER.clear_cache()
  194. if exit:
  195. if linter.config.exit_zero:
  196. sys.exit(0)
  197. elif linter.any_fail_on_issues():
  198. # We need to make sure we return a failing exit code in this case.
  199. # So we use self.linter.msg_status if that is non-zero, otherwise we just return 1.
  200. sys.exit(self.linter.msg_status or 1)
  201. elif score_value is not None:
  202. if score_value >= linter.config.fail_under:
  203. sys.exit(0)
  204. else:
  205. # We need to make sure we return a failing exit code in this case.
  206. # So we use self.linter.msg_status if that is non-zero, otherwise we just return 1.
  207. sys.exit(self.linter.msg_status or 1)
  208. else:
  209. sys.exit(self.linter.msg_status)
  210. class _PylintConfigRun(Run):
  211. """A private wrapper for the 'pylint-config' command."""
  212. _is_pylint_config: ClassVar[bool] = True
  213. """Boolean whether or not this is a 'pylint-config' run.
  214. Used by _PylintConfigRun to make the 'pylint-config' command work.
  215. """