run.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/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 ClassVar
  11. from pylint import config
  12. from pylint.checkers.clear_lru_cache 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. if Path("/sys/fs/cgroup/cpu.max").is_file():
  39. avail_cpu = _query_cpu_cgroupv2()
  40. else:
  41. avail_cpu = _query_cpu_cgroupsv1()
  42. return _query_cpu_handle_k8s_pods(avail_cpu)
  43. def _query_cpu_cgroupv2() -> int | None:
  44. avail_cpu = None
  45. with open("/sys/fs/cgroup/cpu.max", encoding="utf-8") as file:
  46. line = file.read().rstrip()
  47. fields = line.split()
  48. if len(fields) == 2:
  49. str_cpu_quota = fields[0]
  50. cpu_period = int(fields[1])
  51. # Make sure this is not in an unconstrained cgroup
  52. if str_cpu_quota != "max":
  53. cpu_quota = int(str_cpu_quota)
  54. avail_cpu = int(cpu_quota / cpu_period)
  55. return avail_cpu
  56. def _query_cpu_cgroupsv1() -> int | None:
  57. cpu_quota, avail_cpu = None, None
  58. if Path("/sys/fs/cgroup/cpu/cpu.cfs_quota_us").is_file():
  59. with open("/sys/fs/cgroup/cpu/cpu.cfs_quota_us", encoding="utf-8") as file:
  60. # Not useful for AWS Batch based jobs as result is -1, but works on local linux systems
  61. cpu_quota = int(file.read().rstrip())
  62. if (
  63. cpu_quota
  64. and cpu_quota != -1
  65. and Path("/sys/fs/cgroup/cpu/cpu.cfs_period_us").is_file()
  66. ):
  67. with open("/sys/fs/cgroup/cpu/cpu.cfs_period_us", encoding="utf-8") as file:
  68. cpu_period = int(file.read().rstrip())
  69. # Divide quota by period and you should get num of allotted CPU to the container,
  70. # rounded down if fractional.
  71. avail_cpu = int(cpu_quota / cpu_period)
  72. elif Path("/sys/fs/cgroup/cpu/cpu.shares").is_file():
  73. with open("/sys/fs/cgroup/cpu/cpu.shares", encoding="utf-8") as file:
  74. cpu_shares = int(file.read().rstrip())
  75. # For AWS, gives correct value * 1024.
  76. avail_cpu = int(cpu_shares / 1024)
  77. return avail_cpu
  78. def _query_cpu_handle_k8s_pods(avail_cpu: int | None) -> int | None:
  79. # In K8s Pods also a fraction of a single core could be available
  80. # As multiprocessing is not able to run only a "fraction" of process
  81. # assume we have 1 CPU available
  82. if avail_cpu == 0:
  83. avail_cpu = 1
  84. return avail_cpu
  85. def _cpu_count() -> int:
  86. """Use sched_affinity if available for virtualized or containerized
  87. environments.
  88. """
  89. cpu_share = _query_cpu()
  90. cpu_count = None
  91. sched_getaffinity = getattr(os, "sched_getaffinity", None)
  92. # pylint: disable=not-callable,using-constant-test,useless-suppression
  93. if sched_getaffinity:
  94. cpu_count = len(sched_getaffinity(0))
  95. elif multiprocessing:
  96. cpu_count = multiprocessing.cpu_count()
  97. else:
  98. cpu_count = 1
  99. if sys.platform == "win32":
  100. # See also https://github.com/python/cpython/issues/94242
  101. cpu_count = min(cpu_count, 56) # pragma: no cover
  102. if cpu_share is not None:
  103. return min(cpu_share, cpu_count)
  104. return cpu_count
  105. class Run:
  106. """Helper class to use as main for pylint with 'run(*sys.argv[1:])'."""
  107. LinterClass = PyLinter
  108. option_groups = (
  109. (
  110. "Commands",
  111. "Options which are actually commands. Options in this \
  112. group are mutually exclusive.",
  113. ),
  114. )
  115. _is_pylint_config: ClassVar[bool] = False
  116. """Boolean whether or not this is a 'pylint-config' run.
  117. Used by _PylintConfigRun to make the 'pylint-config' command work.
  118. """
  119. # pylint: disable = too-many-statements, too-many-branches
  120. def __init__(
  121. self,
  122. args: Sequence[str],
  123. reporter: BaseReporter | None = None,
  124. exit: bool = True, # pylint: disable=redefined-builtin
  125. ) -> None:
  126. # Immediately exit if user asks for version
  127. if "--version" in args:
  128. print(full_version)
  129. sys.exit(0)
  130. self._rcfile: str | None = None
  131. self._output: str | None = None
  132. self._plugins: list[str] = []
  133. self.verbose: bool = False
  134. # Pre-process certain options and remove them from args list
  135. try:
  136. args = _preprocess_options(self, args)
  137. except ArgumentPreprocessingError as ex:
  138. print(ex, file=sys.stderr)
  139. sys.exit(32)
  140. # Determine configuration file
  141. if self._rcfile is None:
  142. default_file = next(config.find_default_config_files(), None)
  143. if default_file:
  144. self._rcfile = str(default_file)
  145. self.linter = linter = self.LinterClass(
  146. _make_run_options(self),
  147. option_groups=self.option_groups,
  148. )
  149. # register standard checkers
  150. linter.load_default_plugins()
  151. # load command line plugins
  152. linter.load_plugin_modules(self._plugins)
  153. # Register the options needed for 'pylint-config'
  154. # By not registering them by default they don't show up in the normal usage message
  155. if self._is_pylint_config:
  156. _register_generate_config_options(linter._arg_parser)
  157. args = _config_initialization(
  158. linter, args, reporter, config_file=self._rcfile, verbose_mode=self.verbose
  159. )
  160. # Handle the 'pylint-config' command
  161. if self._is_pylint_config:
  162. warnings.warn(
  163. "NOTE: The 'pylint-config' command is experimental and usage can change",
  164. UserWarning,
  165. stacklevel=2,
  166. )
  167. code = _handle_pylint_config_commands(linter)
  168. if exit:
  169. sys.exit(code)
  170. return
  171. # Display help if there are no files to lint or only internal checks enabled (`--disable=all`)
  172. disable_all_msg_set = set(
  173. msg.symbol for msg in linter.msgs_store.messages
  174. ) - set(msg[1] for msg in linter.default_enabled_messages.values())
  175. if not args or (
  176. len(linter.config.enable) == 0
  177. and set(linter.config.disable) == disable_all_msg_set
  178. ):
  179. print("No files to lint: exiting.")
  180. sys.exit(32)
  181. if linter.config.jobs < 0:
  182. print(
  183. f"Jobs number ({linter.config.jobs}) should be greater than or equal to 0",
  184. file=sys.stderr,
  185. )
  186. sys.exit(32)
  187. if linter.config.jobs > 1 or linter.config.jobs == 0:
  188. if ProcessPoolExecutor is None:
  189. print(
  190. "concurrent.futures module is missing, fallback to single process",
  191. file=sys.stderr,
  192. )
  193. linter.set_option("jobs", 1)
  194. elif linter.config.jobs == 0:
  195. linter.config.jobs = _cpu_count()
  196. if self._output:
  197. try:
  198. with open(self._output, "w", encoding="utf-8") as output:
  199. linter.reporter.out = output
  200. linter.check(args)
  201. score_value = linter.generate_reports(verbose=self.verbose)
  202. except OSError as ex:
  203. print(ex, file=sys.stderr)
  204. sys.exit(32)
  205. else:
  206. linter.check(args)
  207. score_value = linter.generate_reports(verbose=self.verbose)
  208. if linter.config.clear_cache_post_run:
  209. clear_lru_caches()
  210. MANAGER.clear_cache()
  211. if exit:
  212. if linter.config.exit_zero:
  213. sys.exit(0)
  214. elif linter.any_fail_on_issues():
  215. # We need to make sure we return a failing exit code in this case.
  216. # So we use self.linter.msg_status if that is non-zero, otherwise we just return 1.
  217. sys.exit(self.linter.msg_status or 1)
  218. elif score_value is not None:
  219. if score_value >= linter.config.fail_under:
  220. sys.exit(0)
  221. else:
  222. # We need to make sure we return a failing exit code in this case.
  223. # So we use self.linter.msg_status if that is non-zero, otherwise we just return 1.
  224. sys.exit(self.linter.msg_status or 1)
  225. else:
  226. sys.exit(self.linter.msg_status)
  227. class _PylintConfigRun(Run):
  228. """A private wrapper for the 'pylint-config' command."""
  229. _is_pylint_config: ClassVar[bool] = True
  230. """Boolean whether or not this is a 'pylint-config' run.
  231. Used by _PylintConfigRun to make the 'pylint-config' command work.
  232. """