mirror of
https://github.com/PyCQA/flake8.git
synced 2026-04-07 21:44:18 +00:00
Merge pull request #1723 from PyCQA/mp-other-plats
enable multiprocessing on other platforms
This commit is contained in:
commit
b89d81a919
7 changed files with 175 additions and 317 deletions
|
|
@ -10,11 +10,10 @@ import logging
|
||||||
import os.path
|
import os.path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import flake8
|
|
||||||
from flake8.discover_files import expand_paths
|
from flake8.discover_files import expand_paths
|
||||||
from flake8.formatting import base as formatter
|
from flake8.formatting import base as formatter
|
||||||
from flake8.main import application as app
|
from flake8.main import application as app
|
||||||
from flake8.options import config
|
from flake8.options.parse_args import parse_args
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -163,7 +162,7 @@ class StyleGuide:
|
||||||
# Stop cringing... I know it's gross.
|
# Stop cringing... I know it's gross.
|
||||||
self._application.make_guide()
|
self._application.make_guide()
|
||||||
self._application.file_checker_manager = None
|
self._application.file_checker_manager = None
|
||||||
self._application.make_file_checker_manager()
|
self._application.make_file_checker_manager([])
|
||||||
|
|
||||||
def input_file(
|
def input_file(
|
||||||
self,
|
self,
|
||||||
|
|
@ -200,23 +199,7 @@ def get_style_guide(**kwargs: Any) -> StyleGuide:
|
||||||
An initialized StyleGuide
|
An initialized StyleGuide
|
||||||
"""
|
"""
|
||||||
application = app.Application()
|
application = app.Application()
|
||||||
prelim_opts, remaining_args = application.parse_preliminary_options([])
|
application.plugins, application.options = parse_args([])
|
||||||
flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file)
|
|
||||||
|
|
||||||
cfg, cfg_dir = config.load_config(
|
|
||||||
config=prelim_opts.config,
|
|
||||||
extra=prelim_opts.append_config,
|
|
||||||
isolated=prelim_opts.isolated,
|
|
||||||
)
|
|
||||||
|
|
||||||
application.find_plugins(
|
|
||||||
cfg,
|
|
||||||
cfg_dir,
|
|
||||||
enable_extensions=prelim_opts.enable_extensions,
|
|
||||||
require_plugins=prelim_opts.require_plugins,
|
|
||||||
)
|
|
||||||
application.register_plugin_options()
|
|
||||||
application.parse_configuration_and_cli(cfg, cfg_dir, remaining_args)
|
|
||||||
# We basically want application.initialize to be called but with these
|
# We basically want application.initialize to be called but with these
|
||||||
# options set instead before we make our formatter, notifier, internal
|
# options set instead before we make our formatter, notifier, internal
|
||||||
# style guide and file checker manager.
|
# style guide and file checker manager.
|
||||||
|
|
@ -229,5 +212,5 @@ def get_style_guide(**kwargs: Any) -> StyleGuide:
|
||||||
LOG.error('Could not update option "%s"', key)
|
LOG.error('Could not update option "%s"', key)
|
||||||
application.make_formatter()
|
application.make_formatter()
|
||||||
application.make_guide()
|
application.make_guide()
|
||||||
application.make_file_checker_manager()
|
application.make_file_checker_manager([])
|
||||||
return StyleGuide(application)
|
return StyleGuide(application)
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,17 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import collections
|
import contextlib
|
||||||
import errno
|
import errno
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing.pool
|
import multiprocessing.pool
|
||||||
import signal
|
import signal
|
||||||
import tokenize
|
import tokenize
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from typing import Sequence
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from flake8 import defaults
|
from flake8 import defaults
|
||||||
|
|
@ -18,6 +20,7 @@ from flake8 import exceptions
|
||||||
from flake8 import processor
|
from flake8 import processor
|
||||||
from flake8 import utils
|
from flake8 import utils
|
||||||
from flake8.discover_files import expand_paths
|
from flake8.discover_files import expand_paths
|
||||||
|
from flake8.options.parse_args import parse_args
|
||||||
from flake8.plugins.finder import Checkers
|
from flake8.plugins.finder import Checkers
|
||||||
from flake8.plugins.finder import LoadedPlugin
|
from flake8.plugins.finder import LoadedPlugin
|
||||||
from flake8.style_guide import StyleGuideManager
|
from flake8.style_guide import StyleGuideManager
|
||||||
|
|
@ -41,6 +44,41 @@ SERIAL_RETRY_ERRNOS = {
|
||||||
# noise in diffs.
|
# noise in diffs.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_mp_plugins: Checkers
|
||||||
|
_mp_options: argparse.Namespace
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _mp_prefork(
|
||||||
|
plugins: Checkers, options: argparse.Namespace
|
||||||
|
) -> Generator[None, None, None]:
|
||||||
|
# we can save significant startup work w/ `fork` multiprocessing
|
||||||
|
global _mp_plugins, _mp_options
|
||||||
|
_mp_plugins, _mp_options = plugins, options
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
del _mp_plugins, _mp_options
|
||||||
|
|
||||||
|
|
||||||
|
def _mp_init(argv: Sequence[str]) -> None:
|
||||||
|
global _mp_plugins, _mp_options
|
||||||
|
|
||||||
|
# Ensure correct signaling of ^C using multiprocessing.Pool.
|
||||||
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
|
|
||||||
|
try:
|
||||||
|
_mp_plugins, _mp_options # for `fork` this'll already be set
|
||||||
|
except NameError:
|
||||||
|
plugins, options = parse_args(argv)
|
||||||
|
_mp_plugins, _mp_options = plugins.checkers, options
|
||||||
|
|
||||||
|
|
||||||
|
def _mp_run(filename: str) -> tuple[str, Results, dict[str, int]]:
|
||||||
|
return FileChecker(
|
||||||
|
filename=filename, plugins=_mp_plugins, options=_mp_options
|
||||||
|
).run_checks()
|
||||||
|
|
||||||
|
|
||||||
class Manager:
|
class Manager:
|
||||||
"""Manage the parallelism and checker instances for each plugin and file.
|
"""Manage the parallelism and checker instances for each plugin and file.
|
||||||
|
|
@ -65,14 +103,13 @@ class Manager:
|
||||||
self,
|
self,
|
||||||
style_guide: StyleGuideManager,
|
style_guide: StyleGuideManager,
|
||||||
plugins: Checkers,
|
plugins: Checkers,
|
||||||
|
argv: Sequence[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize our Manager instance."""
|
"""Initialize our Manager instance."""
|
||||||
self.style_guide = style_guide
|
self.style_guide = style_guide
|
||||||
self.options = style_guide.options
|
self.options = style_guide.options
|
||||||
self.plugins = plugins
|
self.plugins = plugins
|
||||||
self.jobs = self._job_count()
|
self.jobs = self._job_count()
|
||||||
self._all_checkers: list[FileChecker] = []
|
|
||||||
self.checkers: list[FileChecker] = []
|
|
||||||
self.statistics = {
|
self.statistics = {
|
||||||
"files": 0,
|
"files": 0,
|
||||||
"logical lines": 0,
|
"logical lines": 0,
|
||||||
|
|
@ -80,30 +117,22 @@ class Manager:
|
||||||
"tokens": 0,
|
"tokens": 0,
|
||||||
}
|
}
|
||||||
self.exclude = (*self.options.exclude, *self.options.extend_exclude)
|
self.exclude = (*self.options.exclude, *self.options.extend_exclude)
|
||||||
|
self.argv = argv
|
||||||
|
self.results: list[tuple[str, Results, dict[str, int]]] = []
|
||||||
|
|
||||||
def _process_statistics(self) -> None:
|
def _process_statistics(self) -> None:
|
||||||
for checker in self.checkers:
|
for _, _, statistics in self.results:
|
||||||
for statistic in defaults.STATISTIC_NAMES:
|
for statistic in defaults.STATISTIC_NAMES:
|
||||||
self.statistics[statistic] += checker.statistics[statistic]
|
self.statistics[statistic] += statistics[statistic]
|
||||||
self.statistics["files"] += len(self.checkers)
|
self.statistics["files"] += len(self.filenames)
|
||||||
|
|
||||||
def _job_count(self) -> int:
|
def _job_count(self) -> int:
|
||||||
# First we walk through all of our error cases:
|
# First we walk through all of our error cases:
|
||||||
# - multiprocessing library is not present
|
# - multiprocessing library is not present
|
||||||
# - we're running on windows in which case we know we have significant
|
|
||||||
# implementation issues
|
|
||||||
# - the user provided stdin and that's not something we can handle
|
# - the user provided stdin and that's not something we can handle
|
||||||
# well
|
# well
|
||||||
# - the user provided some awful input
|
# - the user provided some awful input
|
||||||
|
|
||||||
# class state is only preserved when using the `fork` strategy.
|
|
||||||
if multiprocessing.get_start_method() != "fork":
|
|
||||||
LOG.warning(
|
|
||||||
"The multiprocessing module is not available. "
|
|
||||||
"Ignoring --jobs arguments."
|
|
||||||
)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if utils.is_using_stdin(self.options.filenames):
|
if utils.is_using_stdin(self.options.filenames):
|
||||||
LOG.warning(
|
LOG.warning(
|
||||||
"The --jobs option is not compatible with supplying "
|
"The --jobs option is not compatible with supplying "
|
||||||
|
|
@ -141,27 +170,6 @@ class Manager:
|
||||||
)
|
)
|
||||||
return reported_results_count
|
return reported_results_count
|
||||||
|
|
||||||
def make_checkers(self, paths: list[str] | None = None) -> None:
|
|
||||||
"""Create checkers for each file."""
|
|
||||||
if paths is None:
|
|
||||||
paths = self.options.filenames
|
|
||||||
|
|
||||||
self._all_checkers = [
|
|
||||||
FileChecker(
|
|
||||||
filename=filename,
|
|
||||||
plugins=self.plugins,
|
|
||||||
options=self.options,
|
|
||||||
)
|
|
||||||
for filename in expand_paths(
|
|
||||||
paths=paths,
|
|
||||||
stdin_display_name=self.options.stdin_display_name,
|
|
||||||
filename_patterns=self.options.filename,
|
|
||||||
exclude=self.exclude,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
self.checkers = [c for c in self._all_checkers if c.should_process]
|
|
||||||
LOG.info("Checking %d files", len(self.checkers))
|
|
||||||
|
|
||||||
def report(self) -> tuple[int, int]:
|
def report(self) -> tuple[int, int]:
|
||||||
"""Report all of the errors found in the managed file checkers.
|
"""Report all of the errors found in the managed file checkers.
|
||||||
|
|
||||||
|
|
@ -172,9 +180,8 @@ class Manager:
|
||||||
A tuple of the total results found and the results reported.
|
A tuple of the total results found and the results reported.
|
||||||
"""
|
"""
|
||||||
results_reported = results_found = 0
|
results_reported = results_found = 0
|
||||||
for checker in self._all_checkers:
|
for filename, results, _ in self.results:
|
||||||
results = sorted(checker.results, key=lambda tup: (tup[1], tup[2]))
|
results.sort(key=lambda tup: (tup[1], tup[2]))
|
||||||
filename = checker.display_name
|
|
||||||
with self.style_guide.processing_file(filename):
|
with self.style_guide.processing_file(filename):
|
||||||
results_reported += self._handle_results(filename, results)
|
results_reported += self._handle_results(filename, results)
|
||||||
results_found += len(results)
|
results_found += len(results)
|
||||||
|
|
@ -182,12 +189,8 @@ class Manager:
|
||||||
|
|
||||||
def run_parallel(self) -> None:
|
def run_parallel(self) -> None:
|
||||||
"""Run the checkers in parallel."""
|
"""Run the checkers in parallel."""
|
||||||
# fmt: off
|
with _mp_prefork(self.plugins, self.options):
|
||||||
final_results: dict[str, list[tuple[str, int, int, str, str | None]]] = collections.defaultdict(list) # noqa: E501
|
pool = _try_initialize_processpool(self.jobs, self.argv)
|
||||||
final_statistics: dict[str, dict[str, int]] = collections.defaultdict(dict) # noqa: E501
|
|
||||||
# fmt: on
|
|
||||||
|
|
||||||
pool = _try_initialize_processpool(self.jobs)
|
|
||||||
|
|
||||||
if pool is None:
|
if pool is None:
|
||||||
self.run_serial()
|
self.run_serial()
|
||||||
|
|
@ -195,17 +198,7 @@ class Manager:
|
||||||
|
|
||||||
pool_closed = False
|
pool_closed = False
|
||||||
try:
|
try:
|
||||||
pool_map = pool.imap_unordered(
|
self.results = list(pool.imap_unordered(_mp_run, self.filenames))
|
||||||
_run_checks,
|
|
||||||
self.checkers,
|
|
||||||
chunksize=calculate_pool_chunksize(
|
|
||||||
len(self.checkers), self.jobs
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for ret in pool_map:
|
|
||||||
filename, results, statistics = ret
|
|
||||||
final_results[filename] = results
|
|
||||||
final_statistics[filename] = statistics
|
|
||||||
pool.close()
|
pool.close()
|
||||||
pool.join()
|
pool.join()
|
||||||
pool_closed = True
|
pool_closed = True
|
||||||
|
|
@ -214,15 +207,16 @@ class Manager:
|
||||||
pool.terminate()
|
pool.terminate()
|
||||||
pool.join()
|
pool.join()
|
||||||
|
|
||||||
for checker in self.checkers:
|
|
||||||
filename = checker.display_name
|
|
||||||
checker.results = final_results[filename]
|
|
||||||
checker.statistics = final_statistics[filename]
|
|
||||||
|
|
||||||
def run_serial(self) -> None:
|
def run_serial(self) -> None:
|
||||||
"""Run the checkers in serial."""
|
"""Run the checkers in serial."""
|
||||||
for checker in self.checkers:
|
self.results = [
|
||||||
checker.run_checks()
|
FileChecker(
|
||||||
|
filename=filename,
|
||||||
|
plugins=self.plugins,
|
||||||
|
options=self.options,
|
||||||
|
).run_checks()
|
||||||
|
for filename in self.filenames
|
||||||
|
]
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
"""Run all the checkers.
|
"""Run all the checkers.
|
||||||
|
|
@ -234,7 +228,7 @@ class Manager:
|
||||||
:issue:`117`) this also implements fallback to serial processing.
|
:issue:`117`) this also implements fallback to serial processing.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if self.jobs > 1 and len(self.checkers) > 1:
|
if self.jobs > 1 and len(self.filenames) > 1:
|
||||||
self.run_parallel()
|
self.run_parallel()
|
||||||
else:
|
else:
|
||||||
self.run_serial()
|
self.run_serial()
|
||||||
|
|
@ -242,7 +236,7 @@ class Manager:
|
||||||
LOG.warning("Flake8 was interrupted by the user")
|
LOG.warning("Flake8 was interrupted by the user")
|
||||||
raise exceptions.EarlyQuit("Early quit while running checks")
|
raise exceptions.EarlyQuit("Early quit while running checks")
|
||||||
|
|
||||||
def start(self, paths: list[str] | None = None) -> None:
|
def start(self) -> None:
|
||||||
"""Start checking files.
|
"""Start checking files.
|
||||||
|
|
||||||
:param paths:
|
:param paths:
|
||||||
|
|
@ -250,7 +244,14 @@ class Manager:
|
||||||
:meth:`~Manager.make_checkers`.
|
:meth:`~Manager.make_checkers`.
|
||||||
"""
|
"""
|
||||||
LOG.info("Making checkers")
|
LOG.info("Making checkers")
|
||||||
self.make_checkers(paths)
|
self.filenames = tuple(
|
||||||
|
expand_paths(
|
||||||
|
paths=self.options.filenames,
|
||||||
|
stdin_display_name=self.options.stdin_display_name,
|
||||||
|
filename_patterns=self.options.filename,
|
||||||
|
exclude=self.exclude,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
"""Stop checking files."""
|
"""Stop checking files."""
|
||||||
|
|
@ -325,7 +326,7 @@ class FileChecker:
|
||||||
|
|
||||||
def run_check(self, plugin: LoadedPlugin, **arguments: Any) -> Any:
|
def run_check(self, plugin: LoadedPlugin, **arguments: Any) -> Any:
|
||||||
"""Run the check in a single plugin."""
|
"""Run the check in a single plugin."""
|
||||||
assert self.processor is not None
|
assert self.processor is not None, self.filename
|
||||||
try:
|
try:
|
||||||
params = self.processor.keyword_arguments_for(
|
params = self.processor.keyword_arguments_for(
|
||||||
plugin.parameters, arguments
|
plugin.parameters, arguments
|
||||||
|
|
@ -409,7 +410,7 @@ class FileChecker:
|
||||||
|
|
||||||
def run_ast_checks(self) -> None:
|
def run_ast_checks(self) -> None:
|
||||||
"""Run all checks expecting an abstract syntax tree."""
|
"""Run all checks expecting an abstract syntax tree."""
|
||||||
assert self.processor is not None
|
assert self.processor is not None, self.filename
|
||||||
ast = self.processor.build_ast()
|
ast = self.processor.build_ast()
|
||||||
|
|
||||||
for plugin in self.plugins.tree:
|
for plugin in self.plugins.tree:
|
||||||
|
|
@ -514,7 +515,9 @@ class FileChecker:
|
||||||
|
|
||||||
def run_checks(self) -> tuple[str, Results, dict[str, int]]:
|
def run_checks(self) -> tuple[str, Results, dict[str, int]]:
|
||||||
"""Run checks against the file."""
|
"""Run checks against the file."""
|
||||||
assert self.processor is not None
|
if self.processor is None or not self.should_process:
|
||||||
|
return self.display_name, self.results, self.statistics
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.run_ast_checks()
|
self.run_ast_checks()
|
||||||
self.process_tokens()
|
self.process_tokens()
|
||||||
|
|
@ -522,11 +525,11 @@ class FileChecker:
|
||||||
code = "E902" if isinstance(e, tokenize.TokenError) else "E999"
|
code = "E902" if isinstance(e, tokenize.TokenError) else "E999"
|
||||||
row, column = self._extract_syntax_information(e)
|
row, column = self._extract_syntax_information(e)
|
||||||
self.report(code, row, column, f"{type(e).__name__}: {e.args[0]}")
|
self.report(code, row, column, f"{type(e).__name__}: {e.args[0]}")
|
||||||
return self.filename, self.results, self.statistics
|
return self.display_name, self.results, self.statistics
|
||||||
|
|
||||||
logical_lines = self.processor.statistics["logical lines"]
|
logical_lines = self.processor.statistics["logical lines"]
|
||||||
self.statistics["logical lines"] = logical_lines
|
self.statistics["logical lines"] = logical_lines
|
||||||
return self.filename, self.results, self.statistics
|
return self.display_name, self.results, self.statistics
|
||||||
|
|
||||||
def handle_newline(self, token_type: int) -> None:
|
def handle_newline(self, token_type: int) -> None:
|
||||||
"""Handle the logic when encountering a newline token."""
|
"""Handle the logic when encountering a newline token."""
|
||||||
|
|
@ -573,17 +576,13 @@ class FileChecker:
|
||||||
self.run_physical_checks(line)
|
self.run_physical_checks(line)
|
||||||
|
|
||||||
|
|
||||||
def _pool_init() -> None:
|
|
||||||
"""Ensure correct signaling of ^C using multiprocessing.Pool."""
|
|
||||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
||||||
|
|
||||||
|
|
||||||
def _try_initialize_processpool(
|
def _try_initialize_processpool(
|
||||||
job_count: int,
|
job_count: int,
|
||||||
|
argv: Sequence[str],
|
||||||
) -> multiprocessing.pool.Pool | None:
|
) -> multiprocessing.pool.Pool | None:
|
||||||
"""Return a new process pool instance if we are able to create one."""
|
"""Return a new process pool instance if we are able to create one."""
|
||||||
try:
|
try:
|
||||||
return multiprocessing.Pool(job_count, _pool_init)
|
return multiprocessing.Pool(job_count, _mp_init, initargs=(argv,))
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
if err.errno not in SERIAL_RETRY_ERRNOS:
|
if err.errno not in SERIAL_RETRY_ERRNOS:
|
||||||
raise
|
raise
|
||||||
|
|
@ -593,22 +592,6 @@ def _try_initialize_processpool(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def calculate_pool_chunksize(num_checkers: int, num_jobs: int) -> int:
|
|
||||||
"""Determine the chunksize for the multiprocessing Pool.
|
|
||||||
|
|
||||||
- For chunksize, see: https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.imap # noqa
|
|
||||||
- This formula, while not perfect, aims to give each worker two batches of
|
|
||||||
work.
|
|
||||||
- See: https://github.com/pycqa/flake8/issues/829#note_18878876
|
|
||||||
- See: https://github.com/pycqa/flake8/issues/197
|
|
||||||
"""
|
|
||||||
return max(num_checkers // (num_jobs * 2), 1)
|
|
||||||
|
|
||||||
|
|
||||||
def _run_checks(checker: FileChecker) -> tuple[str, Results, dict[str, int]]:
|
|
||||||
return checker.run_checks()
|
|
||||||
|
|
||||||
|
|
||||||
def find_offset(
|
def find_offset(
|
||||||
offset: int, mapping: processor._LogicalMapping
|
offset: int, mapping: processor._LogicalMapping
|
||||||
) -> tuple[int, int]:
|
) -> tuple[int, int]:
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import configparser
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
@ -15,10 +14,7 @@ from flake8 import exceptions
|
||||||
from flake8 import style_guide
|
from flake8 import style_guide
|
||||||
from flake8.formatting.base import BaseFormatter
|
from flake8.formatting.base import BaseFormatter
|
||||||
from flake8.main import debug
|
from flake8.main import debug
|
||||||
from flake8.main import options
|
from flake8.options.parse_args import parse_args
|
||||||
from flake8.options import aggregator
|
|
||||||
from flake8.options import config
|
|
||||||
from flake8.options import manager
|
|
||||||
from flake8.plugins import finder
|
from flake8.plugins import finder
|
||||||
from flake8.plugins import reporter
|
from flake8.plugins import reporter
|
||||||
|
|
||||||
|
|
@ -35,12 +31,6 @@ class Application:
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
#: The timestamp when the Application finished reported errors.
|
#: The timestamp when the Application finished reported errors.
|
||||||
self.end_time: float | None = None
|
self.end_time: float | None = None
|
||||||
#: The prelimary argument parser for handling options required for
|
|
||||||
#: obtaining and parsing the configuration file.
|
|
||||||
self.prelim_arg_parser = options.stage1_arg_parser()
|
|
||||||
#: The instance of :class:`flake8.options.manager.OptionManager` used
|
|
||||||
#: to parse and handle the options and arguments passed by the user
|
|
||||||
self.option_manager: manager.OptionManager | None = None
|
|
||||||
|
|
||||||
self.plugins: finder.Plugins | None = None
|
self.plugins: finder.Plugins | None = None
|
||||||
#: The user-selected formatter from :attr:`formatting_plugins`
|
#: The user-selected formatter from :attr:`formatting_plugins`
|
||||||
|
|
@ -65,30 +55,6 @@ class Application:
|
||||||
#: with a non-zero status code
|
#: with a non-zero status code
|
||||||
self.catastrophic_failure = False
|
self.catastrophic_failure = False
|
||||||
|
|
||||||
def parse_preliminary_options(
|
|
||||||
self, argv: Sequence[str]
|
|
||||||
) -> tuple[argparse.Namespace, list[str]]:
|
|
||||||
"""Get preliminary options from the CLI, pre-plugin-loading.
|
|
||||||
|
|
||||||
We need to know the values of a few standard options so that we can
|
|
||||||
locate configuration files and configure logging.
|
|
||||||
|
|
||||||
Since plugins aren't loaded yet, there may be some as-yet-unknown
|
|
||||||
options; we ignore those for now, they'll be parsed later when we do
|
|
||||||
real option parsing.
|
|
||||||
|
|
||||||
:param argv:
|
|
||||||
Command-line arguments passed in directly.
|
|
||||||
:returns:
|
|
||||||
Populated namespace and list of remaining argument strings.
|
|
||||||
"""
|
|
||||||
args, rest = self.prelim_arg_parser.parse_known_args(argv)
|
|
||||||
# XXX (ericvw): Special case "forwarding" the output file option so
|
|
||||||
# that it can be reparsed again for the BaseFormatter.filename.
|
|
||||||
if args.output_file:
|
|
||||||
rest.extend(("--output-file", args.output_file))
|
|
||||||
return args, rest
|
|
||||||
|
|
||||||
def exit_code(self) -> int:
|
def exit_code(self) -> int:
|
||||||
"""Return the program exit code."""
|
"""Return the program exit code."""
|
||||||
if self.catastrophic_failure:
|
if self.catastrophic_failure:
|
||||||
|
|
@ -99,76 +65,6 @@ class Application:
|
||||||
else:
|
else:
|
||||||
return int(self.result_count > 0)
|
return int(self.result_count > 0)
|
||||||
|
|
||||||
def find_plugins(
|
|
||||||
self,
|
|
||||||
cfg: configparser.RawConfigParser,
|
|
||||||
cfg_dir: str,
|
|
||||||
*,
|
|
||||||
enable_extensions: str | None,
|
|
||||||
require_plugins: str | None,
|
|
||||||
) -> None:
|
|
||||||
"""Find and load the plugins for this application.
|
|
||||||
|
|
||||||
Set :attr:`plugins` based on loaded plugins.
|
|
||||||
"""
|
|
||||||
opts = finder.parse_plugin_options(
|
|
||||||
cfg,
|
|
||||||
cfg_dir,
|
|
||||||
enable_extensions=enable_extensions,
|
|
||||||
require_plugins=require_plugins,
|
|
||||||
)
|
|
||||||
raw = finder.find_plugins(cfg, opts)
|
|
||||||
self.plugins = finder.load_plugins(raw, opts)
|
|
||||||
|
|
||||||
def register_plugin_options(self) -> None:
|
|
||||||
"""Register options provided by plugins to our option manager."""
|
|
||||||
assert self.plugins is not None
|
|
||||||
|
|
||||||
self.option_manager = manager.OptionManager(
|
|
||||||
version=flake8.__version__,
|
|
||||||
plugin_versions=self.plugins.versions_str(),
|
|
||||||
parents=[self.prelim_arg_parser],
|
|
||||||
formatter_names=list(self.plugins.reporters),
|
|
||||||
)
|
|
||||||
options.register_default_options(self.option_manager)
|
|
||||||
self.option_manager.register_plugins(self.plugins)
|
|
||||||
|
|
||||||
def parse_configuration_and_cli(
|
|
||||||
self,
|
|
||||||
cfg: configparser.RawConfigParser,
|
|
||||||
cfg_dir: str,
|
|
||||||
argv: list[str],
|
|
||||||
) -> None:
|
|
||||||
"""Parse configuration files and the CLI options."""
|
|
||||||
assert self.option_manager is not None
|
|
||||||
assert self.plugins is not None
|
|
||||||
self.options = aggregator.aggregate_options(
|
|
||||||
self.option_manager,
|
|
||||||
cfg,
|
|
||||||
cfg_dir,
|
|
||||||
argv,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.options.bug_report:
|
|
||||||
info = debug.information(flake8.__version__, self.plugins)
|
|
||||||
print(json.dumps(info, indent=2, sort_keys=True))
|
|
||||||
raise SystemExit(0)
|
|
||||||
|
|
||||||
for loaded in self.plugins.all_plugins():
|
|
||||||
parse_options = getattr(loaded.obj, "parse_options", None)
|
|
||||||
if parse_options is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# XXX: ideally we wouldn't have two forms of parse_options
|
|
||||||
try:
|
|
||||||
parse_options(
|
|
||||||
self.option_manager,
|
|
||||||
self.options,
|
|
||||||
self.options.filenames,
|
|
||||||
)
|
|
||||||
except TypeError:
|
|
||||||
parse_options(self.options)
|
|
||||||
|
|
||||||
def make_formatter(self) -> None:
|
def make_formatter(self) -> None:
|
||||||
"""Initialize a formatter based on the parsed options."""
|
"""Initialize a formatter based on the parsed options."""
|
||||||
assert self.plugins is not None
|
assert self.plugins is not None
|
||||||
|
|
@ -183,13 +79,14 @@ class Application:
|
||||||
self.options, self.formatter
|
self.options, self.formatter
|
||||||
)
|
)
|
||||||
|
|
||||||
def make_file_checker_manager(self) -> None:
|
def make_file_checker_manager(self, argv: Sequence[str]) -> None:
|
||||||
"""Initialize our FileChecker Manager."""
|
"""Initialize our FileChecker Manager."""
|
||||||
assert self.guide is not None
|
assert self.guide is not None
|
||||||
assert self.plugins is not None
|
assert self.plugins is not None
|
||||||
self.file_checker_manager = checker.Manager(
|
self.file_checker_manager = checker.Manager(
|
||||||
style_guide=self.guide,
|
style_guide=self.guide,
|
||||||
plugins=self.plugins.checkers,
|
plugins=self.plugins.checkers,
|
||||||
|
argv=argv,
|
||||||
)
|
)
|
||||||
|
|
||||||
def run_checks(self) -> None:
|
def run_checks(self) -> None:
|
||||||
|
|
@ -265,28 +162,16 @@ class Application:
|
||||||
This finds the plugins, registers their options, and parses the
|
This finds the plugins, registers their options, and parses the
|
||||||
command-line arguments.
|
command-line arguments.
|
||||||
"""
|
"""
|
||||||
# NOTE(sigmavirus24): When updating this, make sure you also update
|
self.plugins, self.options = parse_args(argv)
|
||||||
# our legacy API calls to these same methods.
|
|
||||||
prelim_opts, remaining_args = self.parse_preliminary_options(argv)
|
|
||||||
flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file)
|
|
||||||
|
|
||||||
cfg, cfg_dir = config.load_config(
|
if self.options.bug_report:
|
||||||
config=prelim_opts.config,
|
info = debug.information(flake8.__version__, self.plugins)
|
||||||
extra=prelim_opts.append_config,
|
print(json.dumps(info, indent=2, sort_keys=True))
|
||||||
isolated=prelim_opts.isolated,
|
raise SystemExit(0)
|
||||||
)
|
|
||||||
|
|
||||||
self.find_plugins(
|
|
||||||
cfg,
|
|
||||||
cfg_dir,
|
|
||||||
enable_extensions=prelim_opts.enable_extensions,
|
|
||||||
require_plugins=prelim_opts.require_plugins,
|
|
||||||
)
|
|
||||||
self.register_plugin_options()
|
|
||||||
self.parse_configuration_and_cli(cfg, cfg_dir, remaining_args)
|
|
||||||
self.make_formatter()
|
self.make_formatter()
|
||||||
self.make_guide()
|
self.make_guide()
|
||||||
self.make_file_checker_manager()
|
self.make_file_checker_manager(argv)
|
||||||
|
|
||||||
def report(self) -> None:
|
def report(self) -> None:
|
||||||
"""Report errors, statistics, and benchmarks."""
|
"""Report errors, statistics, and benchmarks."""
|
||||||
|
|
|
||||||
70
src/flake8/options/parse_args.py
Normal file
70
src/flake8/options/parse_args.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
"""Procedure for parsing args, config, loading plugins."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
import flake8
|
||||||
|
from flake8.main import options
|
||||||
|
from flake8.options import aggregator
|
||||||
|
from flake8.options import config
|
||||||
|
from flake8.options import manager
|
||||||
|
from flake8.plugins import finder
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(
|
||||||
|
argv: Sequence[str],
|
||||||
|
) -> tuple[finder.Plugins, argparse.Namespace]:
|
||||||
|
"""Procedure for parsing args, config, loading plugins."""
|
||||||
|
prelim_parser = options.stage1_arg_parser()
|
||||||
|
|
||||||
|
args0, rest = prelim_parser.parse_known_args(argv)
|
||||||
|
# XXX (ericvw): Special case "forwarding" the output file option so
|
||||||
|
# that it can be reparsed again for the BaseFormatter.filename.
|
||||||
|
if args0.output_file:
|
||||||
|
rest.extend(("--output-file", args0.output_file))
|
||||||
|
|
||||||
|
flake8.configure_logging(args0.verbose, args0.output_file)
|
||||||
|
|
||||||
|
cfg, cfg_dir = config.load_config(
|
||||||
|
config=args0.config,
|
||||||
|
extra=args0.append_config,
|
||||||
|
isolated=args0.isolated,
|
||||||
|
)
|
||||||
|
|
||||||
|
plugin_opts = finder.parse_plugin_options(
|
||||||
|
cfg,
|
||||||
|
cfg_dir,
|
||||||
|
enable_extensions=args0.enable_extensions,
|
||||||
|
require_plugins=args0.require_plugins,
|
||||||
|
)
|
||||||
|
raw_plugins = finder.find_plugins(cfg, plugin_opts)
|
||||||
|
plugins = finder.load_plugins(raw_plugins, plugin_opts)
|
||||||
|
|
||||||
|
option_manager = manager.OptionManager(
|
||||||
|
version=flake8.__version__,
|
||||||
|
plugin_versions=plugins.versions_str(),
|
||||||
|
parents=[prelim_parser],
|
||||||
|
formatter_names=list(plugins.reporters),
|
||||||
|
)
|
||||||
|
options.register_default_options(option_manager)
|
||||||
|
option_manager.register_plugins(plugins)
|
||||||
|
|
||||||
|
opts = aggregator.aggregate_options(option_manager, cfg, cfg_dir, rest)
|
||||||
|
|
||||||
|
for loaded in plugins.all_plugins():
|
||||||
|
parse_options = getattr(loaded.obj, "parse_options", None)
|
||||||
|
if parse_options is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# XXX: ideally we wouldn't have two forms of parse_options
|
||||||
|
try:
|
||||||
|
parse_options(
|
||||||
|
option_manager,
|
||||||
|
opts,
|
||||||
|
opts.filenames,
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
parse_options(opts)
|
||||||
|
|
||||||
|
return plugins, opts
|
||||||
|
|
@ -266,17 +266,12 @@ def test_report_order(results, expected_order):
|
||||||
# tuples to create the expected result lists from the indexes
|
# tuples to create the expected result lists from the indexes
|
||||||
expected_results = [results[index] for index in expected_order]
|
expected_results = [results[index] for index in expected_order]
|
||||||
|
|
||||||
file_checker = mock.Mock(spec=["results", "display_name"])
|
|
||||||
file_checker.results = results
|
|
||||||
file_checker.display_name = "placeholder"
|
|
||||||
|
|
||||||
style_guide = mock.MagicMock(spec=["options", "processing_file"])
|
style_guide = mock.MagicMock(spec=["options", "processing_file"])
|
||||||
|
|
||||||
# Create a placeholder manager without arguments or plugins
|
# Create a placeholder manager without arguments or plugins
|
||||||
# Just add one custom file checker which just provides the results
|
# Just add one custom file checker which just provides the results
|
||||||
manager = checker.Manager(style_guide, finder.Checkers([], [], []))
|
manager = checker.Manager(style_guide, finder.Checkers([], [], []), [])
|
||||||
manager.checkers = manager._all_checkers = [file_checker]
|
manager.results = [("placeholder", results, {})]
|
||||||
|
|
||||||
# _handle_results is the first place which gets the sorted result
|
# _handle_results is the first place which gets the sorted result
|
||||||
# Should something non-private be mocked instead?
|
# Should something non-private be mocked instead?
|
||||||
handler = mock.Mock(side_effect=count_side_effect)
|
handler = mock.Mock(side_effect=count_side_effect)
|
||||||
|
|
@ -295,9 +290,9 @@ def test_acquire_when_multiprocessing_pool_can_initialize():
|
||||||
This simulates the behaviour on most common platforms.
|
This simulates the behaviour on most common platforms.
|
||||||
"""
|
"""
|
||||||
with mock.patch("multiprocessing.Pool") as pool:
|
with mock.patch("multiprocessing.Pool") as pool:
|
||||||
result = checker._try_initialize_processpool(2)
|
result = checker._try_initialize_processpool(2, [])
|
||||||
|
|
||||||
pool.assert_called_once_with(2, checker._pool_init)
|
pool.assert_called_once_with(2, checker._mp_init, initargs=([],))
|
||||||
assert result is pool.return_value
|
assert result is pool.return_value
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -314,9 +309,9 @@ def test_acquire_when_multiprocessing_pool_can_not_initialize():
|
||||||
https://github.com/python/cpython/blob/4e02981de0952f54bf87967f8e10d169d6946b40/Lib/multiprocessing/synchronize.py#L30-L33
|
https://github.com/python/cpython/blob/4e02981de0952f54bf87967f8e10d169d6946b40/Lib/multiprocessing/synchronize.py#L30-L33
|
||||||
"""
|
"""
|
||||||
with mock.patch("multiprocessing.Pool", side_effect=ImportError) as pool:
|
with mock.patch("multiprocessing.Pool", side_effect=ImportError) as pool:
|
||||||
result = checker._try_initialize_processpool(2)
|
result = checker._try_initialize_processpool(2, [])
|
||||||
|
|
||||||
pool.assert_called_once_with(2, checker._pool_init)
|
pool.assert_called_once_with(2, checker._mp_init, initargs=([],))
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,9 @@ def style_guide_mock():
|
||||||
def _parallel_checker_manager():
|
def _parallel_checker_manager():
|
||||||
"""Call Manager.run() and return the number of calls to `run_serial`."""
|
"""Call Manager.run() and return the number of calls to `run_serial`."""
|
||||||
style_guide = style_guide_mock()
|
style_guide = style_guide_mock()
|
||||||
manager = checker.Manager(style_guide, finder.Checkers([], [], []))
|
manager = checker.Manager(style_guide, finder.Checkers([], [], []), [])
|
||||||
# multiple checkers is needed for parallel mode
|
# multiple files is needed for parallel mode
|
||||||
manager.checkers = [mock.Mock(), mock.Mock()]
|
manager.filenames = ("file1", "file2")
|
||||||
return manager
|
return manager
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -36,8 +36,7 @@ def test_oserrors_cause_serial_fall_back():
|
||||||
assert serial.call_count == 1
|
assert serial.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(multiprocessing, "get_start_method", return_value="fork")
|
def test_oserrors_are_reraised():
|
||||||
def test_oserrors_are_reraised(_):
|
|
||||||
"""Verify that unexpected OSErrors will cause the Manager to reraise."""
|
"""Verify that unexpected OSErrors will cause the Manager to reraise."""
|
||||||
err = OSError(errno.EAGAIN, "Ominous message")
|
err = OSError(errno.EAGAIN, "Ominous message")
|
||||||
with mock.patch("_multiprocessing.SemLock", side_effect=err):
|
with mock.patch("_multiprocessing.SemLock", side_effect=err):
|
||||||
|
|
@ -48,14 +47,6 @@ def test_oserrors_are_reraised(_):
|
||||||
assert serial.call_count == 0
|
assert serial.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(multiprocessing, "get_start_method", return_value="spawn")
|
|
||||||
def test_multiprocessing_is_disabled(_):
|
|
||||||
"""Verify not being able to import multiprocessing forces jobs to 0."""
|
|
||||||
style_guide = style_guide_mock()
|
|
||||||
manager = checker.Manager(style_guide, finder.Checkers([], [], []))
|
|
||||||
assert manager.jobs == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_multiprocessing_cpu_count_not_implemented():
|
def test_multiprocessing_cpu_count_not_implemented():
|
||||||
"""Verify that jobs is 0 if cpu_count is unavailable."""
|
"""Verify that jobs is 0 if cpu_count is unavailable."""
|
||||||
style_guide = style_guide_mock()
|
style_guide = style_guide_mock()
|
||||||
|
|
@ -66,22 +57,18 @@ def test_multiprocessing_cpu_count_not_implemented():
|
||||||
"cpu_count",
|
"cpu_count",
|
||||||
side_effect=NotImplementedError,
|
side_effect=NotImplementedError,
|
||||||
):
|
):
|
||||||
manager = checker.Manager(style_guide, finder.Checkers([], [], []))
|
manager = checker.Manager(style_guide, finder.Checkers([], [], []), [])
|
||||||
assert manager.jobs == 0
|
assert manager.jobs == 0
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(multiprocessing, "get_start_method", return_value="spawn")
|
def test_make_checkers():
|
||||||
def test_make_checkers(_):
|
|
||||||
"""Verify that we create a list of FileChecker instances."""
|
"""Verify that we create a list of FileChecker instances."""
|
||||||
style_guide = style_guide_mock()
|
style_guide = style_guide_mock()
|
||||||
style_guide.options.filenames = ["file1", "file2"]
|
style_guide.options.filenames = ["file1", "file2"]
|
||||||
manager = checker.Manager(style_guide, finder.Checkers([], [], []))
|
manager = checker.Manager(style_guide, finder.Checkers([], [], []), [])
|
||||||
|
|
||||||
with mock.patch("flake8.utils.fnmatch", return_value=True):
|
with mock.patch("flake8.utils.fnmatch", return_value=True):
|
||||||
with mock.patch("flake8.processor.FileProcessor"):
|
with mock.patch("flake8.processor.FileProcessor"):
|
||||||
manager.make_checkers(["file1", "file2"])
|
manager.start()
|
||||||
|
|
||||||
assert manager._all_checkers
|
assert manager.filenames == ("file1", "file2")
|
||||||
for file_checker in manager._all_checkers:
|
|
||||||
assert file_checker.filename in style_guide.options.filenames
|
|
||||||
assert not manager.checkers # the files don't exist
|
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,12 @@
|
||||||
"""Tests for Flake8's legacy API."""
|
"""Tests for Flake8's legacy API."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
|
||||||
import configparser
|
|
||||||
import os.path
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from flake8.api import legacy as api
|
from flake8.api import legacy as api
|
||||||
from flake8.formatting import base as formatter
|
from flake8.formatting import base as formatter
|
||||||
from flake8.options import config
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_style_guide():
|
|
||||||
"""Verify the methods called on our internal Application."""
|
|
||||||
prelim_opts = argparse.Namespace(
|
|
||||||
append_config=[],
|
|
||||||
config=None,
|
|
||||||
isolated=False,
|
|
||||||
output_file=None,
|
|
||||||
verbose=0,
|
|
||||||
enable_extensions=None,
|
|
||||||
require_plugins=None,
|
|
||||||
)
|
|
||||||
mockedapp = mock.Mock()
|
|
||||||
mockedapp.parse_preliminary_options.return_value = (prelim_opts, [])
|
|
||||||
mockedapp.program = "flake8"
|
|
||||||
|
|
||||||
cfg = configparser.RawConfigParser()
|
|
||||||
cfg_dir = os.getcwd()
|
|
||||||
|
|
||||||
with mock.patch.object(config, "load_config", return_value=(cfg, cfg_dir)):
|
|
||||||
with mock.patch("flake8.main.application.Application") as application:
|
|
||||||
application.return_value = mockedapp
|
|
||||||
style_guide = api.get_style_guide()
|
|
||||||
|
|
||||||
application.assert_called_once_with()
|
|
||||||
mockedapp.parse_preliminary_options.assert_called_once_with([])
|
|
||||||
mockedapp.find_plugins.assert_called_once_with(
|
|
||||||
cfg,
|
|
||||||
cfg_dir,
|
|
||||||
enable_extensions=None,
|
|
||||||
require_plugins=None,
|
|
||||||
)
|
|
||||||
mockedapp.register_plugin_options.assert_called_once_with()
|
|
||||||
mockedapp.parse_configuration_and_cli.assert_called_once_with(
|
|
||||||
cfg, cfg_dir, []
|
|
||||||
)
|
|
||||||
mockedapp.make_formatter.assert_called_once_with()
|
|
||||||
mockedapp.make_guide.assert_called_once_with()
|
|
||||||
mockedapp.make_file_checker_manager.assert_called_once_with()
|
|
||||||
assert isinstance(style_guide, api.StyleGuide)
|
|
||||||
|
|
||||||
|
|
||||||
def test_styleguide_options():
|
def test_styleguide_options():
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue