application.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. """Module containing the application logic for Flake8."""
  2. import argparse
  3. import configparser
  4. import json
  5. import logging
  6. import time
  7. from typing import Dict
  8. from typing import List
  9. from typing import Optional
  10. from typing import Sequence
  11. from typing import Set
  12. from typing import Tuple
  13. import flake8
  14. from flake8 import checker
  15. from flake8 import defaults
  16. from flake8 import exceptions
  17. from flake8 import style_guide
  18. from flake8 import utils
  19. from flake8.formatting.base import BaseFormatter
  20. from flake8.main import debug
  21. from flake8.main import options
  22. from flake8.options import aggregator
  23. from flake8.options import config
  24. from flake8.options import manager
  25. from flake8.plugins import finder
  26. from flake8.plugins import reporter
  27. LOG = logging.getLogger(__name__)
  28. class Application:
  29. """Abstract our application into a class."""
  30. def __init__(self) -> None:
  31. """Initialize our application."""
  32. #: The timestamp when the Application instance was instantiated.
  33. self.start_time = time.time()
  34. #: The timestamp when the Application finished reported errors.
  35. self.end_time: Optional[float] = None
  36. #: The prelimary argument parser for handling options required for
  37. #: obtaining and parsing the configuration file.
  38. self.prelim_arg_parser = options.stage1_arg_parser()
  39. #: The instance of :class:`flake8.options.manager.OptionManager` used
  40. #: to parse and handle the options and arguments passed by the user
  41. self.option_manager: Optional[manager.OptionManager] = None
  42. self.plugins: Optional[finder.Plugins] = None
  43. #: The user-selected formatter from :attr:`formatting_plugins`
  44. self.formatter: Optional[BaseFormatter] = None
  45. #: The :class:`flake8.style_guide.StyleGuideManager` built from the
  46. #: user's options
  47. self.guide: Optional[style_guide.StyleGuideManager] = None
  48. #: The :class:`flake8.checker.Manager` that will handle running all of
  49. #: the checks selected by the user.
  50. self.file_checker_manager: Optional[checker.Manager] = None
  51. #: The user-supplied options parsed into an instance of
  52. #: :class:`argparse.Namespace`
  53. self.options: Optional[argparse.Namespace] = None
  54. #: The number of errors, warnings, and other messages after running
  55. #: flake8 and taking into account ignored errors and lines.
  56. self.result_count = 0
  57. #: The total number of errors before accounting for ignored errors and
  58. #: lines.
  59. self.total_result_count = 0
  60. #: Whether or not something catastrophic happened and we should exit
  61. #: with a non-zero status code
  62. self.catastrophic_failure = False
  63. #: The parsed diff information
  64. self.parsed_diff: Dict[str, Set[int]] = {}
  65. def parse_preliminary_options(
  66. self, argv: Sequence[str]
  67. ) -> Tuple[argparse.Namespace, List[str]]:
  68. """Get preliminary options from the CLI, pre-plugin-loading.
  69. We need to know the values of a few standard options so that we can
  70. locate configuration files and configure logging.
  71. Since plugins aren't loaded yet, there may be some as-yet-unknown
  72. options; we ignore those for now, they'll be parsed later when we do
  73. real option parsing.
  74. :param argv:
  75. Command-line arguments passed in directly.
  76. :returns:
  77. Populated namespace and list of remaining argument strings.
  78. """
  79. args, rest = self.prelim_arg_parser.parse_known_args(argv)
  80. # XXX (ericvw): Special case "forwarding" the output file option so
  81. # that it can be reparsed again for the BaseFormatter.filename.
  82. if args.output_file:
  83. rest.extend(("--output-file", args.output_file))
  84. return args, rest
  85. def exit_code(self) -> int:
  86. """Return the program exit code."""
  87. if self.catastrophic_failure:
  88. return 1
  89. assert self.options is not None
  90. if self.options.exit_zero:
  91. return 0
  92. else:
  93. return int(self.result_count > 0)
  94. def find_plugins(
  95. self,
  96. cfg: configparser.RawConfigParser,
  97. cfg_dir: str,
  98. *,
  99. enable_extensions: Optional[str],
  100. require_plugins: Optional[str],
  101. ) -> None:
  102. """Find and load the plugins for this application.
  103. Set :attr:`plugins` based on loaded plugins.
  104. """
  105. opts = finder.parse_plugin_options(
  106. cfg,
  107. cfg_dir,
  108. enable_extensions=enable_extensions,
  109. require_plugins=require_plugins,
  110. )
  111. raw = finder.find_plugins(cfg, opts)
  112. self.plugins = finder.load_plugins(raw, opts)
  113. def register_plugin_options(self) -> None:
  114. """Register options provided by plugins to our option manager."""
  115. assert self.plugins is not None
  116. self.option_manager = manager.OptionManager(
  117. version=flake8.__version__,
  118. plugin_versions=self.plugins.versions_str(),
  119. parents=[self.prelim_arg_parser],
  120. )
  121. options.register_default_options(self.option_manager)
  122. self.option_manager.register_plugins(self.plugins)
  123. def parse_configuration_and_cli(
  124. self,
  125. cfg: configparser.RawConfigParser,
  126. cfg_dir: str,
  127. argv: List[str],
  128. ) -> None:
  129. """Parse configuration files and the CLI options."""
  130. assert self.option_manager is not None
  131. assert self.plugins is not None
  132. self.options = aggregator.aggregate_options(
  133. self.option_manager,
  134. cfg,
  135. cfg_dir,
  136. argv,
  137. )
  138. if self.options.bug_report:
  139. info = debug.information(flake8.__version__, self.plugins)
  140. print(json.dumps(info, indent=2, sort_keys=True))
  141. raise SystemExit(0)
  142. if self.options.diff:
  143. LOG.warning(
  144. "the --diff option is deprecated and will be removed in a "
  145. "future version."
  146. )
  147. self.parsed_diff = utils.parse_unified_diff()
  148. for loaded in self.plugins.all_plugins():
  149. parse_options = getattr(loaded.obj, "parse_options", None)
  150. if parse_options is None:
  151. continue
  152. # XXX: ideally we wouldn't have two forms of parse_options
  153. try:
  154. parse_options(
  155. self.option_manager,
  156. self.options,
  157. self.options.filenames,
  158. )
  159. except TypeError:
  160. parse_options(self.options)
  161. def make_formatter(self) -> None:
  162. """Initialize a formatter based on the parsed options."""
  163. assert self.plugins is not None
  164. assert self.options is not None
  165. self.formatter = reporter.make(self.plugins.reporters, self.options)
  166. def make_guide(self) -> None:
  167. """Initialize our StyleGuide."""
  168. assert self.formatter is not None
  169. assert self.options is not None
  170. self.guide = style_guide.StyleGuideManager(
  171. self.options, self.formatter
  172. )
  173. if self.options.diff:
  174. self.guide.add_diff_ranges(self.parsed_diff)
  175. def make_file_checker_manager(self) -> None:
  176. """Initialize our FileChecker Manager."""
  177. assert self.guide is not None
  178. assert self.plugins is not None
  179. self.file_checker_manager = checker.Manager(
  180. style_guide=self.guide,
  181. plugins=self.plugins.checkers,
  182. )
  183. def run_checks(self) -> None:
  184. """Run the actual checks with the FileChecker Manager.
  185. This method encapsulates the logic to make a
  186. :class:`~flake8.checker.Manger` instance run the checks it is
  187. managing.
  188. """
  189. assert self.options is not None
  190. assert self.file_checker_manager is not None
  191. if self.options.diff:
  192. files: Optional[List[str]] = sorted(self.parsed_diff)
  193. if not files:
  194. return
  195. else:
  196. files = None
  197. self.file_checker_manager.start(files)
  198. try:
  199. self.file_checker_manager.run()
  200. except exceptions.PluginExecutionFailed as plugin_failed:
  201. print(str(plugin_failed))
  202. print("Run flake8 with greater verbosity to see more details")
  203. self.catastrophic_failure = True
  204. LOG.info("Finished running")
  205. self.file_checker_manager.stop()
  206. self.end_time = time.time()
  207. def report_benchmarks(self) -> None:
  208. """Aggregate, calculate, and report benchmarks for this run."""
  209. assert self.options is not None
  210. if not self.options.benchmark:
  211. return
  212. assert self.file_checker_manager is not None
  213. assert self.end_time is not None
  214. time_elapsed = self.end_time - self.start_time
  215. statistics = [("seconds elapsed", time_elapsed)]
  216. add_statistic = statistics.append
  217. for statistic in defaults.STATISTIC_NAMES + ("files",):
  218. value = self.file_checker_manager.statistics[statistic]
  219. total_description = f"total {statistic} processed"
  220. add_statistic((total_description, value))
  221. per_second_description = f"{statistic} processed per second"
  222. add_statistic((per_second_description, int(value / time_elapsed)))
  223. assert self.formatter is not None
  224. self.formatter.show_benchmarks(statistics)
  225. def report_errors(self) -> None:
  226. """Report all the errors found by flake8 3.0.
  227. This also updates the :attr:`result_count` attribute with the total
  228. number of errors, warnings, and other messages found.
  229. """
  230. LOG.info("Reporting errors")
  231. assert self.file_checker_manager is not None
  232. results = self.file_checker_manager.report()
  233. self.total_result_count, self.result_count = results
  234. LOG.info(
  235. "Found a total of %d violations and reported %d",
  236. self.total_result_count,
  237. self.result_count,
  238. )
  239. def report_statistics(self) -> None:
  240. """Aggregate and report statistics from this run."""
  241. assert self.options is not None
  242. if not self.options.statistics:
  243. return
  244. assert self.formatter is not None
  245. assert self.guide is not None
  246. self.formatter.show_statistics(self.guide.stats)
  247. def initialize(self, argv: Sequence[str]) -> None:
  248. """Initialize the application to be run.
  249. This finds the plugins, registers their options, and parses the
  250. command-line arguments.
  251. """
  252. # NOTE(sigmavirus24): When updating this, make sure you also update
  253. # our legacy API calls to these same methods.
  254. prelim_opts, remaining_args = self.parse_preliminary_options(argv)
  255. flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file)
  256. cfg, cfg_dir = config.load_config(
  257. config=prelim_opts.config,
  258. extra=prelim_opts.append_config,
  259. isolated=prelim_opts.isolated,
  260. )
  261. self.find_plugins(
  262. cfg,
  263. cfg_dir,
  264. enable_extensions=prelim_opts.enable_extensions,
  265. require_plugins=prelim_opts.require_plugins,
  266. )
  267. self.register_plugin_options()
  268. self.parse_configuration_and_cli(cfg, cfg_dir, remaining_args)
  269. self.make_formatter()
  270. self.make_guide()
  271. self.make_file_checker_manager()
  272. def report(self) -> None:
  273. """Report errors, statistics, and benchmarks."""
  274. assert self.formatter is not None
  275. self.formatter.start()
  276. self.report_errors()
  277. self.report_statistics()
  278. self.report_benchmarks()
  279. self.formatter.stop()
  280. def _run(self, argv: Sequence[str]) -> None:
  281. self.initialize(argv)
  282. self.run_checks()
  283. self.report()
  284. def run(self, argv: Sequence[str]) -> None:
  285. """Run our application.
  286. This method will also handle KeyboardInterrupt exceptions for the
  287. entirety of the flake8 application. If it sees a KeyboardInterrupt it
  288. will forcibly clean up the :class:`~flake8.checker.Manager`.
  289. """
  290. try:
  291. self._run(argv)
  292. except KeyboardInterrupt as exc:
  293. print("... stopped")
  294. LOG.critical("Caught keyboard interrupt from user")
  295. LOG.exception(exc)
  296. self.catastrophic_failure = True
  297. except exceptions.ExecutionError as exc:
  298. print("There was a critical error during execution of Flake8:")
  299. print(exc)
  300. LOG.exception(exc)
  301. self.catastrophic_failure = True
  302. except exceptions.EarlyQuit:
  303. self.catastrophic_failure = True
  304. print("... stopped while processing files")
  305. else:
  306. assert self.options is not None
  307. if self.options.count:
  308. print(self.result_count)