Source code for flakeheaven._patched._app

# 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] @staticmethod def extract_toml_config_path(argv: List[str]) -> Tuple[Optional[Path], List[str]]: if not argv: return None, argv if '--help' in argv: argv = argv.copy() argv.remove('--help') if not argv: return None, ['--help'] parser = ArgumentParser() parser.add_argument('--config') known, unknown = parser.parse_known_args(argv) if known.config and known.config.endswith('.toml'): return Path(known.config).expanduser(), unknown return None, argv
[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_formatter(self, *args, **kwargs) -> None: if self.formatter is None: super().make_formatter(*args, **kwargs)
[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)