run.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  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. import os
  4. import sys
  5. import warnings
  6. from pylint import __pkginfo__, extensions, interfaces
  7. from pylint.constants import DEFAULT_PYLINT_HOME, OLD_DEFAULT_PYLINT_HOME, full_version
  8. from pylint.lint.pylinter import PyLinter
  9. from pylint.lint.utils import ArgumentPreprocessingError, preprocess_options
  10. from pylint.utils import print_full_documentation, utils
  11. try:
  12. import multiprocessing
  13. from multiprocessing import synchronize # noqa pylint: disable=unused-import
  14. except ImportError:
  15. multiprocessing = None # type: ignore[assignment]
  16. def _cpu_count() -> int:
  17. """Use sched_affinity if available for virtualized or containerized environments."""
  18. sched_getaffinity = getattr(os, "sched_getaffinity", None)
  19. # pylint: disable=not-callable,using-constant-test,useless-suppression
  20. if sched_getaffinity:
  21. return len(sched_getaffinity(0))
  22. if multiprocessing:
  23. return multiprocessing.cpu_count()
  24. return 1
  25. def cb_list_extensions(option, optname, value, parser):
  26. """List all the extensions under pylint.extensions"""
  27. for filename in os.listdir(os.path.dirname(extensions.__file__)):
  28. if filename.endswith(".py") and not filename.startswith("_"):
  29. extension_name, _, _ = filename.partition(".")
  30. print(f"pylint.extensions.{extension_name}")
  31. sys.exit(0)
  32. def cb_list_confidence_levels(option, optname, value, parser):
  33. for level in interfaces.CONFIDENCE_LEVELS:
  34. print(f"%-18s: {level}")
  35. sys.exit(0)
  36. def cb_init_hook(optname, value):
  37. """exec arbitrary code to set sys.path for instance"""
  38. exec(value) # pylint: disable=exec-used
  39. UNUSED_PARAM_SENTINEL = object()
  40. class Run:
  41. """helper class to use as main for pylint :
  42. run(*sys.argv[1:])
  43. """
  44. LinterClass = PyLinter
  45. option_groups = (
  46. (
  47. "Commands",
  48. "Options which are actually commands. Options in this \
  49. group are mutually exclusive.",
  50. ),
  51. )
  52. @staticmethod
  53. def _return_one(*args): # pylint: disable=unused-argument
  54. return 1
  55. def __init__(
  56. self,
  57. args,
  58. reporter=None,
  59. exit=True,
  60. do_exit=UNUSED_PARAM_SENTINEL,
  61. ): # pylint: disable=redefined-builtin
  62. self._rcfile = None
  63. self._output = None
  64. self._version_asked = False
  65. self._plugins = []
  66. self.verbose = None
  67. try:
  68. preprocess_options(
  69. args,
  70. {
  71. # option: (callback, takearg)
  72. "version": (self.version_asked, False),
  73. "init-hook": (cb_init_hook, True),
  74. "rcfile": (self.cb_set_rcfile, True),
  75. "load-plugins": (self.cb_add_plugins, True),
  76. "enable-all-extensions": (self.cb_enable_all_extensions, False),
  77. "verbose": (self.cb_verbose_mode, False),
  78. "output": (self.cb_set_output, True),
  79. },
  80. )
  81. except ArgumentPreprocessingError as ex:
  82. print(ex, file=sys.stderr)
  83. sys.exit(32)
  84. self.linter = linter = self.LinterClass(
  85. (
  86. (
  87. "rcfile",
  88. {
  89. "action": "callback",
  90. "callback": Run._return_one,
  91. "group": "Commands",
  92. "type": "string",
  93. "metavar": "<file>",
  94. "help": "Specify a configuration file to load.",
  95. },
  96. ),
  97. (
  98. "output",
  99. {
  100. "action": "callback",
  101. "callback": Run._return_one,
  102. "group": "Commands",
  103. "type": "string",
  104. "metavar": "<file>",
  105. "help": "Specify an output file.",
  106. },
  107. ),
  108. (
  109. "init-hook",
  110. {
  111. "action": "callback",
  112. "callback": Run._return_one,
  113. "type": "string",
  114. "metavar": "<code>",
  115. "level": 1,
  116. "help": "Python code to execute, usually for sys.path "
  117. "manipulation such as pygtk.require().",
  118. },
  119. ),
  120. (
  121. "help-msg",
  122. {
  123. "action": "callback",
  124. "type": "string",
  125. "metavar": "<msg-id>",
  126. "callback": self.cb_help_message,
  127. "group": "Commands",
  128. "help": "Display a help message for the given message id and "
  129. "exit. The value may be a comma separated list of message ids.",
  130. },
  131. ),
  132. (
  133. "list-msgs",
  134. {
  135. "action": "callback",
  136. "metavar": "<msg-id>",
  137. "callback": self.cb_list_messages,
  138. "group": "Commands",
  139. "level": 1,
  140. "help": "Display a list of all pylint's messages divided by whether "
  141. "they are emittable with the given interpreter.",
  142. },
  143. ),
  144. (
  145. "list-msgs-enabled",
  146. {
  147. "action": "callback",
  148. "metavar": "<msg-id>",
  149. "callback": self.cb_list_messages_enabled,
  150. "group": "Commands",
  151. "level": 1,
  152. "help": "Display a list of what messages are enabled, "
  153. "disabled and non-emittable with the given configuration.",
  154. },
  155. ),
  156. (
  157. "list-groups",
  158. {
  159. "action": "callback",
  160. "metavar": "<msg-id>",
  161. "callback": self.cb_list_groups,
  162. "group": "Commands",
  163. "level": 1,
  164. "help": "List pylint's message groups.",
  165. },
  166. ),
  167. (
  168. "list-conf-levels",
  169. {
  170. "action": "callback",
  171. "callback": cb_list_confidence_levels,
  172. "group": "Commands",
  173. "level": 1,
  174. "help": "Generate pylint's confidence levels.",
  175. },
  176. ),
  177. (
  178. "list-extensions",
  179. {
  180. "action": "callback",
  181. "callback": cb_list_extensions,
  182. "group": "Commands",
  183. "level": 1,
  184. "help": "List available extensions.",
  185. },
  186. ),
  187. (
  188. "full-documentation",
  189. {
  190. "action": "callback",
  191. "metavar": "<msg-id>",
  192. "callback": self.cb_full_documentation,
  193. "group": "Commands",
  194. "level": 1,
  195. "help": "Generate pylint's full documentation.",
  196. },
  197. ),
  198. (
  199. "generate-rcfile",
  200. {
  201. "action": "callback",
  202. "callback": self.cb_generate_config,
  203. "group": "Commands",
  204. "help": "Generate a sample configuration file according to "
  205. "the current configuration. You can put other options "
  206. "before this one to get them in the generated "
  207. "configuration.",
  208. },
  209. ),
  210. (
  211. "generate-man",
  212. {
  213. "action": "callback",
  214. "callback": self.cb_generate_manpage,
  215. "group": "Commands",
  216. "help": "Generate pylint's man page.",
  217. "hide": True,
  218. },
  219. ),
  220. (
  221. "errors-only",
  222. {
  223. "action": "callback",
  224. "callback": self.cb_error_mode,
  225. "short": "E",
  226. "help": "In error mode, checkers without error messages are "
  227. "disabled and for others, only the ERROR messages are "
  228. "displayed, and no reports are done by default.",
  229. },
  230. ),
  231. (
  232. "verbose",
  233. {
  234. "action": "callback",
  235. "callback": self.cb_verbose_mode,
  236. "short": "v",
  237. "help": "In verbose mode, extra non-checker-related info "
  238. "will be displayed.",
  239. },
  240. ),
  241. (
  242. "enable-all-extensions",
  243. {
  244. "action": "callback",
  245. "callback": self.cb_enable_all_extensions,
  246. "help": "Load and enable all available extensions. "
  247. "Use --list-extensions to see a list all available extensions.",
  248. },
  249. ),
  250. ),
  251. option_groups=self.option_groups,
  252. pylintrc=self._rcfile,
  253. )
  254. # register standard checkers
  255. if self._version_asked:
  256. print(full_version)
  257. sys.exit(0)
  258. linter.load_default_plugins()
  259. # load command line plugins
  260. linter.load_plugin_modules(self._plugins)
  261. # add some help section
  262. linter.add_help_section(
  263. "Environment variables",
  264. f"""
  265. The following environment variables are used:
  266. * PYLINTHOME
  267. Path to the directory where persistent data for the run will be stored. If
  268. not found, it defaults to '{DEFAULT_PYLINT_HOME}' or '{OLD_DEFAULT_PYLINT_HOME}'
  269. (in the current working directory).
  270. * PYLINTRC
  271. Path to the configuration file. See the documentation for the method used
  272. to search for configuration file.
  273. """,
  274. level=1,
  275. )
  276. linter.add_help_section(
  277. "Output",
  278. "Using the default text output, the message format is : \n"
  279. " \n"
  280. " MESSAGE_TYPE: LINE_NUM:[OBJECT:] MESSAGE \n"
  281. " \n"
  282. "There are 5 kind of message types : \n"
  283. " * (C) convention, for programming standard violation \n"
  284. " * (R) refactor, for bad code smell \n"
  285. " * (W) warning, for python specific problems \n"
  286. " * (E) error, for probable bugs in the code \n"
  287. " * (F) fatal, if an error occurred which prevented pylint from doing further\n"
  288. "processing.\n",
  289. level=1,
  290. )
  291. linter.add_help_section(
  292. "Output status code",
  293. "Pylint should leave with following status code: \n"
  294. " * 0 if everything went fine \n"
  295. " * 1 if a fatal message was issued \n"
  296. " * 2 if an error message was issued \n"
  297. " * 4 if a warning message was issued \n"
  298. " * 8 if a refactor message was issued \n"
  299. " * 16 if a convention message was issued \n"
  300. " * 32 on usage error \n"
  301. " \n"
  302. "status 1 to 16 will be bit-ORed so you can know which different categories has\n"
  303. "been issued by analysing pylint output status code\n",
  304. level=1,
  305. )
  306. # read configuration
  307. linter.disable("I")
  308. linter.enable("c-extension-no-member")
  309. try:
  310. linter.read_config_file(verbose=self.verbose)
  311. except OSError as ex:
  312. print(ex, file=sys.stderr)
  313. sys.exit(32)
  314. config_parser = linter.cfgfile_parser
  315. # run init hook, if present, before loading plugins
  316. if config_parser.has_option("MASTER", "init-hook"):
  317. cb_init_hook(
  318. "init-hook", utils._unquote(config_parser.get("MASTER", "init-hook"))
  319. )
  320. # is there some additional plugins in the file configuration, in
  321. if config_parser.has_option("MASTER", "load-plugins"):
  322. plugins = utils._splitstrip(config_parser.get("MASTER", "load-plugins"))
  323. linter.load_plugin_modules(plugins)
  324. # now we can load file config and command line, plugins (which can
  325. # provide options) have been registered
  326. linter.load_config_file()
  327. if reporter:
  328. # if a custom reporter is provided as argument, it may be overridden
  329. # by file parameters, so re-set it here, but before command line
  330. # parsing so it's still overrideable by command line option
  331. linter.set_reporter(reporter)
  332. try:
  333. args = linter.load_command_line_configuration(args)
  334. except SystemExit as exc:
  335. if exc.code == 2: # bad options
  336. exc.code = 32
  337. raise
  338. if not args:
  339. print(linter.help())
  340. sys.exit(32)
  341. if linter.config.jobs < 0:
  342. print(
  343. f"Jobs number ({linter.config.jobs}) should be greater than or equal to 0",
  344. file=sys.stderr,
  345. )
  346. sys.exit(32)
  347. if linter.config.jobs > 1 or linter.config.jobs == 0:
  348. if multiprocessing is None:
  349. print(
  350. "Multiprocessing library is missing, fallback to single process",
  351. file=sys.stderr,
  352. )
  353. linter.set_option("jobs", 1)
  354. elif linter.config.jobs == 0:
  355. linter.config.jobs = _cpu_count()
  356. # We have loaded configuration from config file and command line. Now, we can
  357. # load plugin specific configuration.
  358. linter.load_plugin_configuration()
  359. # Now that plugins are loaded, get list of all fail_on messages, and enable them
  360. linter.enable_fail_on_messages()
  361. if self._output:
  362. try:
  363. with open(self._output, "w", encoding="utf-8") as output:
  364. linter.reporter.out = output
  365. linter.check(args)
  366. score_value = linter.generate_reports()
  367. except OSError as ex:
  368. print(ex, file=sys.stderr)
  369. sys.exit(32)
  370. else:
  371. linter.check(args)
  372. score_value = linter.generate_reports()
  373. if do_exit is not UNUSED_PARAM_SENTINEL:
  374. warnings.warn(
  375. "do_exit is deprecated and it is going to be removed in a future version.",
  376. DeprecationWarning,
  377. )
  378. exit = do_exit
  379. if exit:
  380. if linter.config.exit_zero:
  381. sys.exit(0)
  382. elif linter.any_fail_on_issues():
  383. # We need to make sure we return a failing exit code in this case.
  384. # So we use self.linter.msg_status if that is non-zero, otherwise we just return 1.
  385. sys.exit(self.linter.msg_status or 1)
  386. elif score_value is not None:
  387. if score_value >= linter.config.fail_under:
  388. sys.exit(0)
  389. else:
  390. # We need to make sure we return a failing exit code in this case.
  391. # So we use self.linter.msg_status if that is non-zero, otherwise we just return 1.
  392. sys.exit(self.linter.msg_status or 1)
  393. else:
  394. sys.exit(self.linter.msg_status)
  395. def version_asked(self, _, __):
  396. """callback for version (i.e. before option parsing)"""
  397. self._version_asked = True
  398. def cb_set_rcfile(self, name, value):
  399. """callback for option preprocessing (i.e. before option parsing)"""
  400. self._rcfile = value
  401. def cb_set_output(self, name, value):
  402. """callback for option preprocessing (i.e. before option parsing)"""
  403. self._output = value
  404. def cb_add_plugins(self, name, value):
  405. """callback for option preprocessing (i.e. before option parsing)"""
  406. self._plugins.extend(utils._splitstrip(value))
  407. def cb_error_mode(self, *args, **kwargs):
  408. """error mode:
  409. * disable all but error messages
  410. * disable the 'miscellaneous' checker which can be safely deactivated in
  411. debug
  412. * disable reports
  413. * do not save execution information
  414. """
  415. self.linter.error_mode()
  416. def cb_generate_config(self, *args, **kwargs):
  417. """optik callback for sample config file generation"""
  418. self.linter.generate_config(skipsections=("COMMANDS",))
  419. sys.exit(0)
  420. def cb_generate_manpage(self, *args, **kwargs):
  421. """optik callback for sample config file generation"""
  422. self.linter.generate_manpage(__pkginfo__)
  423. sys.exit(0)
  424. def cb_help_message(self, option, optname, value, parser):
  425. """optik callback for printing some help about a particular message"""
  426. self.linter.msgs_store.help_message(utils._splitstrip(value))
  427. sys.exit(0)
  428. def cb_full_documentation(self, option, optname, value, parser):
  429. """optik callback for printing full documentation"""
  430. print_full_documentation(self.linter)
  431. sys.exit(0)
  432. def cb_list_messages(self, option, optname, value, parser):
  433. """optik callback for printing available messages"""
  434. self.linter.msgs_store.list_messages()
  435. sys.exit(0)
  436. def cb_list_messages_enabled(self, option, optname, value, parser):
  437. """optik callback for printing available messages"""
  438. self.linter.list_messages_enabled()
  439. sys.exit(0)
  440. def cb_list_groups(self, *args, **kwargs):
  441. """List all the check groups that pylint knows about
  442. These should be useful to know what check groups someone can disable
  443. or enable.
  444. """
  445. for check in self.linter.get_checker_names():
  446. print(check)
  447. sys.exit(0)
  448. def cb_verbose_mode(self, *args, **kwargs):
  449. self.verbose = True
  450. def cb_enable_all_extensions(self, option_name: str, value: None) -> None:
  451. """Callback to load and enable all available extensions"""
  452. for filename in os.listdir(os.path.dirname(extensions.__file__)):
  453. # pylint: disable=fixme
  454. # TODO: Remove the check for deprecated check_docs after the extension has been removed
  455. if (
  456. filename.endswith(".py")
  457. and not filename.startswith("_")
  458. and not filename.startswith("check_docs")
  459. ):
  460. extension_name = f"pylint.extensions.{filename[:-3]}"
  461. if extension_name not in self._plugins:
  462. self._plugins.append(extension_name)