# built-in
import sys
from argparse import ArgumentParser, Namespace
from itertools import chain
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
# external
from flake8 import _EXTRA_VERBOSE, LOG as FLAKE8_LOG
from flake8.main.application import Application
from flake8.options.config import ConfigParser, get_local_plugins
from flake8.options.manager import Option
from flake8.plugins.manager import ReportFormatters
from flake8.utils import parse_unified_diff
# app
from .._constants import DEFAULTS, ExitCode
from .._logic import read_config
from .._logic._discover import NoPlugins, get_missing
from ._checkers import FlakeHeavenCheckersManager
from ._plugins import FlakeHeavenCheckers
from ._style_guide import FlakeHeavenStyleGuideManager
LOG = FLAKE8_LOG.getChild(__name__)
# Options that are related to the flake8 codes management logic.
# We use our own codes management via `plugins` and `exceptions`.
UNSUPPORTED = frozenset({
'--extend-exclude', # use only `exclude` in the config
'--per-file-ignores', # use `exceptions`
'--statistics', # use `--format=stat` instead
'--ignore', # use `plugins`
'--extend-ignore', # use `plugins`
'--select', # use `plugins`
'--enable-extensions', # use `plugins`
})
[docs]class FlakeHeavenApplication(Application):
"""
Reloaded flake8 original entrypoint to provide support for some features:
+ pyproject.toml support
+ replace CheckersManager to support for `plugins` option
+ register custom formatters
"""
guide: FlakeHeavenStyleGuideManager
@property
def option_manager(self):
"""We overload this property only to specify setter."""
return self._option_manager
@option_manager.setter
def option_manager(self, manager):
"""Hook to add flakeheaven options into flake8 options parser.
"""
group = manager.parser.add_argument_group('FlakeHeaven')
group.add_argument('--baseline', help='path to baseline')
group.add_argument('--relative', action='store_true',
help='Treat file paths as relative to directory containing baseline file')
group.add_argument('--safe', action='store_true', help='suppress exceptions from plugins')
group.add_argument('--error-on-missing', action='store_true',
help='Error before linting if there are missing plugins')
self._option_manager = manager
[docs] def get_toml_config(
self,
path: Optional[Path] = None,
*,
enforce_keys_from: Dict[str, Option],
) -> Dict[str, Any]:
"""Extract config from TOML.
Args:
path: toml filepath. If not set, searches in cwd parents.
enforce_keys_from: Mapping of configuration option names to
:class:`~flake8.options.manager.Option` instances. It is
used to convert ``dashed-names`` in `toml` to
:class:`~flake8.options.config.ConfigParser` namespace so
it can be updated via its ``__dict__``. Typically, it comes
from either
:attr:`~flake8.options.config.ConfigParser.config_options`,
or directly from
:attr:`~flake8.options.manager.OptionManager.config_options_dict`.
"""
if path is not None:
toml_config = read_config(path)
else:
# lookup for config from current dir up to root
root = Path().resolve()
for dir_path in chain([root], root.parents):
path = dir_path / 'pyproject.toml'
if path.exists():
toml_config = read_config(path)
break
else:
toml_config = {}
for name in list(toml_config.keys()):
try:
option = enforce_keys_from[name]
dst = option.config_name
if dst == name:
continue
if dst is None:
raise ValueError(
f'Unable to parse `{path}`. '
f'Reason: option {option}.config_name not set. '
f'Maybe its not enabled as `parse_from_config`?' # noqa: C812
)
except KeyError:
continue
toml_config[dst] = toml_config.pop(name)
return toml_config
[docs] def parse_preliminary_options(
self,
argv: List[str],
) -> Tuple[Namespace, List[str]]:
# if passed `--config` with path to TOML-config, we should extract it
# before passing into flake8 mechanisms
self._config_path, argv = self.extract_toml_config_path(argv=argv)
return super().parse_preliminary_options(argv)
[docs] def parse_configuration_and_cli(self, config_finder, argv: List[str]) -> None:
parser = self.option_manager.parser
for action in parser._actions.copy():
if not action.option_strings:
continue
name = action.option_strings[-1]
if name not in UNSUPPORTED:
continue
parser._handle_conflict_resolve(None, [(name, action)])
# make default config
config, _ = self.option_manager.parse_args([])
LOG.log(_EXTRA_VERBOSE, 'CONFIG: initial:```%s```', vars(config))
config.__dict__.update(DEFAULTS)
LOG.log(
_EXTRA_VERBOSE, 'CONFIG: after flakeheaven defaults:```%s```', vars(config),
)
# Parse CLI options and legacy flake8 configs.
# Based on `aggregate_options`.
config_parser = ConfigParser(
option_manager=self.option_manager,
config_finder=config_finder,
)
parsed_config = config_parser.parse()
toml_config = self.get_toml_config(
self._config_path,
enforce_keys_from=config_parser.config_options,
)
config.__dict__.update(toml_config)
LOG.log(_EXTRA_VERBOSE, 'CONFIG: after toml update:```%s```', vars(config))
config.extended_default_select = self.option_manager.extended_default_select.copy()
config.extended_default_ignore = self.option_manager.extended_default_ignore.copy()
for config_name, value in parsed_config.items():
dest_name = config_name
# If the config name is somehow different from the destination name,
# fetch the destination name from our Option
if not hasattr(config, config_name):
dest_name = config_parser.config_options[config_name].dest
setattr(config, dest_name, value)
self.options, self.args = self.option_manager.parse_args(
args=argv,
values=config,
)
LOG.log(_EXTRA_VERBOSE, 'OPTIONS: after option manager parsed:```%s```', vars(self.options))
# All this goes from the original `parse_configuration_and_cli`.
# We can't call `super` anymore because all `Application` methods
# redefine everything.
self.running_against_diff = self.options.diff
if self.running_against_diff:
self.parsed_diff = parse_unified_diff()
if not self.parsed_diff:
self.exit()
self.options._running_from_vcs = False
self.check_plugins.provide_options(
optmanager=self.option_manager,
options=self.options,
extra_args=self.args,
)
self.formatting_plugins.provide_options(
optmanager=self.option_manager,
options=self.options,
extra_args=self.args,
)
[docs] def make_file_checker_manager(self) -> None:
self.file_checker_manager = FlakeHeavenCheckersManager(
baseline=self.options.baseline,
style_guide=self.guide,
arguments=self.args,
checker_plugins=self.check_plugins,
relative=self.options.relative,
)
[docs] def find_plugins(self, config_finder) -> None:
local_plugins = get_local_plugins(config_finder)
sys.path.extend(local_plugins.paths)
self.check_plugins = FlakeHeavenCheckers(local_plugins.extension) # this line is changed
self.formatting_plugins = ReportFormatters(local_plugins.report)
self.check_plugins.load_plugins()
self.formatting_plugins.load_plugins()
[docs] def run_checks(self):
try:
missing = get_missing(self, initialize=False)
except NoPlugins as exc:
if self.options.error_on_missing:
raise
else:
print(exc, category=NoPlugins)
else:
if missing:
msg = '\n'.join(missing)
print(f'Missing plugins: \n{msg}')
if self.options.error_on_missing:
sys.exit(ExitCode.PLUGINS_MISSING)
super().run_checks()
[docs] def make_guide(self) -> None:
"""Patched StyleGuide creation just to use FlakeHeavenStyleGuideManager
instead of original one.
"""
if self.guide is None:
self.guide = FlakeHeavenStyleGuideManager(self.options, self.formatter)
if self.running_against_diff:
self.guide.add_diff_ranges(self.parsed_diff)