From 65c893728ec7834f95afdd06736107b4f3a5ea7a Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 22 Nov 2021 19:42:50 -0500 Subject: [PATCH] refactor and simplify configuration loading --- docs/source/internal/option_handling.rst | 46 +-- src/flake8/api/legacy.py | 17 +- src/flake8/main/application.py | 68 ++-- src/flake8/main/options.py | 1 + src/flake8/options/aggregator.py | 36 +- src/flake8/options/config.py | 372 ++++-------------- src/flake8/options/manager.py | 2 +- tests/fixtures/config_files/README.rst | 10 - tests/fixtures/config_files/broken.ini | 9 - .../cli-specified-with-inline-comments.ini | 16 - .../cli-specified-without-inline-comments.ini | 16 - tests/fixtures/config_files/cli-specified.ini | 10 - .../config-with-hyphenated-options.ini | 5 - .../config_files/no-flake8-section.ini | 20 - tests/integration/test_aggregator.py | 44 ++- tests/unit/test_config_file_finder.py | 143 ------- tests/unit/test_config_parser.py | 188 --------- tests/unit/test_get_local_plugins.py | 49 --- tests/unit/test_legacy_api.py | 16 +- tests/unit/test_options_config.py | 166 ++++++++ 20 files changed, 351 insertions(+), 883 deletions(-) delete mode 100644 tests/fixtures/config_files/broken.ini delete mode 100644 tests/fixtures/config_files/cli-specified-with-inline-comments.ini delete mode 100644 tests/fixtures/config_files/cli-specified-without-inline-comments.ini delete mode 100644 tests/fixtures/config_files/cli-specified.ini delete mode 100644 tests/fixtures/config_files/config-with-hyphenated-options.ini delete mode 100644 tests/fixtures/config_files/no-flake8-section.ini delete mode 100644 tests/unit/test_config_file_finder.py delete mode 100644 tests/unit/test_config_parser.py delete mode 100644 tests/unit/test_get_local_plugins.py create mode 100644 tests/unit/test_options_config.py diff --git a/docs/source/internal/option_handling.rst b/docs/source/internal/option_handling.rst index 00c688f..9e8f4fd 100644 --- a/docs/source/internal/option_handling.rst +++ b/docs/source/internal/option_handling.rst @@ -41,7 +41,7 @@ three new parameters: The last two are not specifically for configuration file handling, but they do improve that dramatically. We found that there were options that, when -specified in a configuration file, often necessitated being spit +specified in a configuration file, often necessitated being split across multiple lines and those options were almost always comma-separated. For example, let's consider a user's list of ignored error codes for a project: @@ -157,42 +157,22 @@ problems with pep8's 1.6 series. As such, |Flake8| has separated out discovery, management, and merging into a module to make reasoning about each of these pieces easier and more explicit (as well as easier to test). -Configuration file discovery is managed by the -:class:`~flake8.options.config.ConfigFileFinder` object. This object needs to -know information about the program's name, any extra arguments passed to it, -and any configuration files that should be appended to the list of discovered -files. It provides methods for finding the files and similar methods for -parsing those fles. For example, it provides -:meth:`~flake8.options.config.ConfigFileFinder.local_config_files` to find -known local config files (and append the extra configuration files) and it -also provides :meth:`~flake8.options.config.ConfigFileFinder.local_configs` -to parse those configuration files. +Configuration file discovery and raw ini reading is managed by +:func:`~flake8.options.config.load_config`. This produces a loaded +:class:`~configparser.RawConfigParser` and a config directory (which will be +used later to normalize paths). -.. note:: ``local_config_files`` also filters out non-existent files. +Next, :func:`~flake8.options.config.parse_config` parses options using the +types in the ``OptionManager``. -Configuration file merging and managemnt is controlled by the -:class:`~flake8.options.config.ConfigParser`. This requires the instance -of :class:`~flake8.options.manager.OptionManager` that the program is using, -the list of appended config files, and the list of extra arguments. This -object is currently the sole user of the -:class:`~flake8.options.config.ConfigFileFinder` object. It appropriately -initializes the object and uses it in each of - -- :meth:`~flake8.options.config.ConfigParser.parse_cli_config` -- :meth:`~flake8.options.config.ConfigParser.parse_local_config` - -Finally, :meth:`~flake8.options.config.ConfigParser.parse` returns the -appropriate configuration dictionary for this execution of |Flake8|. The -main usage of the ``ConfigParser`` is in -:func:`~flake8.options.aggregator.aggregate_options`. +Most of this is done in :func:`~flake8.options.aggregator.aggregate_options`. Aggregating Configuration File and Command Line Arguments --------------------------------------------------------- :func:`~flake8.options.aggregator.aggregate_options` accepts an instance of :class:`~flake8.options.manager.OptionManager` and does the work to parse the -command-line arguments passed by the user necessary for creating an instance -of :class:`~flake8.options.config.ConfigParser`. +command-line arguments. After parsing the configuration file, we determine the default ignore list. We use the defaults from the OptionManager and update those with the parsed @@ -216,10 +196,6 @@ API Documentation :members: :special-members: -.. autoclass:: flake8.options.config.ConfigFileFinder - :members: - :special-members: +.. autofunction:: flake8.options.config.load_config -.. autoclass:: flake8.options.config.ConfigParser - :members: - :special-members: +.. autofunction:: flake8.options.config.parse_config diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py index f80cb3d..ed54770 100644 --- a/src/flake8/api/legacy.py +++ b/src/flake8/api/legacy.py @@ -31,19 +31,16 @@ def get_style_guide(**kwargs): application = app.Application() prelim_opts, remaining_args = application.parse_preliminary_options([]) flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file) - config_finder = config.ConfigFileFinder( - application.program, - prelim_opts.append_config, - config_file=prelim_opts.config, - ignore_config_files=prelim_opts.isolated, + + cfg, cfg_dir = config.load_config( + config=prelim_opts.config, + extra=prelim_opts.append_config, + isolated=prelim_opts.isolated, ) - application.find_plugins(config_finder) + application.find_plugins(cfg, cfg_dir) application.register_plugin_options() - application.parse_configuration_and_cli( - config_finder, - remaining_args, - ) + application.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) # We basically want application.initialize to be called but with these # options set instead before we make our formatter, notifier, internal # style guide and file checker manager. diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 2ed2f68..6825f91 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -1,5 +1,6 @@ """Module containing the application logic for Flake8.""" import argparse +import configparser import logging import sys import time @@ -130,24 +131,35 @@ class Application: else: return int((self.result_count > 0) or self.catastrophic_failure) - def find_plugins(self, config_finder: config.ConfigFileFinder) -> None: + def find_plugins( + self, + cfg: configparser.RawConfigParser, + cfg_dir: str, + ) -> None: """Find and load the plugins for this application. Set the :attr:`check_plugins` and :attr:`formatting_plugins` attributes based on the discovered plugins found. - - :param config.ConfigFileFinder config_finder: - The finder for finding and reading configuration files. """ - local_plugins = config.get_local_plugins(config_finder) - - sys.path.extend(local_plugins.paths) - - self.check_plugins = plugin_manager.Checkers(local_plugins.extension) - - self.formatting_plugins = plugin_manager.ReportFormatters( - local_plugins.report + # TODO: move to src/flake8/plugins/finder.py + extension_local = utils.parse_comma_separated_list( + cfg.get("flake8:local-plugins", "extension", fallback="").strip(), + regexp=utils.LOCAL_PLUGIN_LIST_RE, ) + report_local = utils.parse_comma_separated_list( + cfg.get("flake8:local-plugins", "report", fallback="").strip(), + regexp=utils.LOCAL_PLUGIN_LIST_RE, + ) + + paths_s = cfg.get("flake8:local-plugins", "paths", fallback="").strip() + local_paths = utils.parse_comma_separated_list(paths_s) + local_paths = utils.normalize_paths(local_paths, cfg_dir) + + sys.path.extend(local_paths) + + self.check_plugins = plugin_manager.Checkers(extension_local) + + self.formatting_plugins = plugin_manager.ReportFormatters(report_local) self.check_plugins.load_plugins() self.formatting_plugins.load_plugins() @@ -162,19 +174,15 @@ class Application: def parse_configuration_and_cli( self, - config_finder: config.ConfigFileFinder, + cfg: configparser.RawConfigParser, + cfg_dir: str, argv: List[str], ) -> None: - """Parse configuration files and the CLI options. - - :param config.ConfigFileFinder config_finder: - The finder for finding and reading configuration files. - :param list argv: - Command-line arguments passed in directly. - """ + """Parse configuration files and the CLI options.""" self.options = aggregator.aggregate_options( self.option_manager, - config_finder, + cfg, + cfg_dir, argv, ) @@ -329,18 +337,16 @@ class Application: # 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) - config_finder = config.ConfigFileFinder( - self.program, - prelim_opts.append_config, - config_file=prelim_opts.config, - ignore_config_files=prelim_opts.isolated, + + cfg, cfg_dir = config.load_config( + config=prelim_opts.config, + extra=prelim_opts.append_config, + isolated=prelim_opts.isolated, ) - self.find_plugins(config_finder) + + self.find_plugins(cfg, cfg_dir) self.register_plugin_options() - self.parse_configuration_and_cli( - config_finder, - remaining_args, - ) + self.parse_configuration_and_cli(cfg, cfg_dir, remaining_args) self.make_formatter() self.make_guide() self.make_file_checker_manager() diff --git a/src/flake8/main/options.py b/src/flake8/main/options.py index babd44b..3abc043 100644 --- a/src/flake8/main/options.py +++ b/src/flake8/main/options.py @@ -39,6 +39,7 @@ def register_preliminary_options(parser: argparse.ArgumentParser) -> None: add_argument( "--append-config", action="append", + default=[], help="Provide extra config files to parse in addition to the files " "found by Flake8 by default. These files are the last ones read " "and so they take the highest precedence when multiple files " diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py index 6458d69..0311257 100644 --- a/src/flake8/options/aggregator.py +++ b/src/flake8/options/aggregator.py @@ -4,8 +4,10 @@ This holds the logic that uses the collected and merged config files and applies the user-specified command-line configuration on top of it. """ import argparse +import configparser import logging -from typing import List +from typing import Optional +from typing import Sequence from flake8.options import config from flake8.options.manager import OptionManager @@ -15,34 +17,16 @@ LOG = logging.getLogger(__name__) def aggregate_options( manager: OptionManager, - config_finder: config.ConfigFileFinder, - argv: List[str], + cfg: configparser.RawConfigParser, + cfg_dir: str, + argv: Optional[Sequence[str]], ) -> argparse.Namespace: - """Aggregate and merge CLI and config file options. - - :param flake8.options.manager.OptionManager manager: - The instance of the OptionManager that we're presently using. - :param flake8.options.config.ConfigFileFinder config_finder: - The config file finder to use. - :param list argv: - The list of remaining command-line arguments that were unknown during - preliminary option parsing to pass to ``manager.parse_args``. - :returns: - Tuple of the parsed options and extra arguments returned by - ``manager.parse_args``. - :rtype: - tuple(argparse.Namespace, list) - """ + """Aggregate and merge CLI and config file options.""" # Get defaults from the option parser default_values = manager.parse_args([]) - # Make our new configuration file mergerator - config_parser = config.ConfigParser( - option_manager=manager, config_finder=config_finder - ) - # Get the parsed config - parsed_config = config_parser.parse() + parsed_config = config.parse_config(manager, cfg, cfg_dir) # Extend the default ignore value with the extended default ignore list, # registered by plugins. @@ -70,7 +54,9 @@ def aggregate_options( # If the config name is somehow different from the destination name, # fetch the destination name from our Option if not hasattr(default_values, config_name): - dest_name = config_parser.config_options[config_name].dest + dest_val = manager.config_options_dict[config_name].dest + assert isinstance(dest_val, str) + dest_name = dest_val LOG.debug( 'Overriding default value of (%s) for "%s" with (%s)', diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index fc3b205..d7519df 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -1,318 +1,108 @@ """Config handling logic for Flake8.""" -import collections import configparser import logging import os.path +from typing import Any +from typing import Dict from typing import List from typing import Optional from typing import Tuple -from flake8 import utils +from flake8.options.manager import OptionManager LOG = logging.getLogger(__name__) -__all__ = ("ConfigFileFinder", "ConfigParser") - -class ConfigFileFinder: - """Encapsulate the logic for finding and reading config files.""" - - def __init__( - self, - program_name: str, - extra_config_files: Optional[List[str]] = None, - config_file: Optional[str] = None, - ignore_config_files: bool = False, - ) -> None: - """Initialize object to find config files. - - :param str program_name: - Name of the current program (e.g., flake8). - :param list extra_config_files: - Extra configuration files specified by the user to read. - :param str config_file: - Configuration file override to only read configuration from. - :param bool ignore_config_files: - Determine whether to ignore configuration files or not. - """ - # The values of --append-config from the CLI - if extra_config_files is None: - extra_config_files = [] - self.extra_config_files = utils.normalize_paths(extra_config_files) - - # The value of --config from the CLI. - self.config_file = config_file - - # The value of --isolated from the CLI. - self.ignore_config_files = ignore_config_files - - # User configuration file. - self.program_name = program_name - - # List of filenames to find in the local/project directory - self.project_filenames = ("setup.cfg", "tox.ini", f".{program_name}") - - self.local_directory = os.path.abspath(os.curdir) - - @staticmethod - def _read_config( - *files: str, - ) -> Tuple[configparser.RawConfigParser, List[str]]: - config = configparser.RawConfigParser() - - found_files = [] - for filename in files: +def _find_config_file(path: str) -> Optional[str]: + cfg = configparser.RawConfigParser() + while True: + for candidate in ("setup.cfg", "tox.ini", ".flake8"): + cfg_path = os.path.join(path, candidate) try: - found_files.extend(config.read(filename)) - except UnicodeDecodeError: - LOG.exception( - "There was an error decoding a config file." - "The file with a problem was %s.", - filename, - ) - except configparser.ParsingError: - LOG.exception( - "There was an error trying to parse a config " - "file. The file with a problem was %s.", - filename, - ) - return (config, found_files) + cfg.read(cfg_path) + except (UnicodeDecodeError, configparser.ParsingError) as e: + LOG.warning("ignoring unparseable config %s: %s", cfg_path, e) + else: + # only consider it a config if it contains flake8 sections + if "flake8" in cfg or "flake8:local-plugins" in cfg: + return cfg_path - def cli_config(self, files: str) -> configparser.RawConfigParser: - """Read and parse the config file specified on the command-line.""" - config, found_files = self._read_config(files) - if found_files: - LOG.debug("Found cli configuration files: %s", found_files) - return config + new_path = os.path.dirname(path) + if new_path == path: + break + else: + path = new_path - def generate_possible_local_files(self): - """Find and generate all local config files.""" - parent = tail = os.getcwd() - found_config_files = False - while tail and not found_config_files: - for project_filename in self.project_filenames: - filename = os.path.abspath( - os.path.join(parent, project_filename) - ) - if os.path.exists(filename): - yield filename - found_config_files = True - self.local_directory = parent - (parent, tail) = os.path.split(parent) - - def local_config_files(self): - """Find all local config files which actually exist. - - Filter results from - :meth:`~ConfigFileFinder.generate_possible_local_files` based - on whether the filename exists or not. - - :returns: - List of files that exist that are local project config files with - extra config files appended to that list (which also exist). - :rtype: - [str] - """ - exists = os.path.exists - return [ - filename for filename in self.generate_possible_local_files() - ] + [f for f in self.extra_config_files if exists(f)] - - def local_configs_with_files(self): - """Parse all local config files into one config object. - - Return (config, found_config_files) tuple. - """ - config, found_files = self._read_config(*self.local_config_files()) - if found_files: - LOG.debug("Found local configuration files: %s", found_files) - return (config, found_files) - - def local_configs(self): - """Parse all local config files into one config object.""" - return self.local_configs_with_files()[0] + # did not find any configuration file + return None -class ConfigParser: - """Encapsulate merging different types of configuration files. +def load_config( + config: Optional[str], + extra: List[str], + *, + isolated: bool = False, +) -> Tuple[configparser.RawConfigParser, str]: + """Load the configuration given the user options. - This parses out the options registered that were specified in the - configuration files, handles extra configuration files, and returns - dictionaries with the parsed values. + - in ``isolated`` mode, return an empty configuration + - if a config file is given in ``config`` use that, otherwise attempt to + discover a configuration using ``tox.ini`` / ``setup.cfg`` / ``.flake8`` + - finally, load any ``extra`` configuration files """ + pwd = os.path.abspath(".") - #: Set of actions that should use the - #: :meth:`~configparser.RawConfigParser.getbool` method. - GETBOOL_ACTIONS = {"store_true", "store_false"} + if isolated: + return configparser.RawConfigParser(), pwd - def __init__(self, option_manager, config_finder): - """Initialize the ConfigParser instance. + if config is None: + config = _find_config_file(pwd) - :param flake8.options.manager.OptionManager option_manager: - Initialized OptionManager. - :param flake8.options.config.ConfigFileFinder config_finder: - Initialized ConfigFileFinder. - """ - #: Our instance of flake8.options.manager.OptionManager - self.option_manager = option_manager - #: The prog value for the cli parser - self.program_name = option_manager.program_name - #: Mapping of configuration option names to - #: :class:`~flake8.options.manager.Option` instances - self.config_options = option_manager.config_options_dict - #: Our instance of our :class:`~ConfigFileFinder` - self.config_finder = config_finder - - def _normalize_value(self, option, value, parent=None): - if parent is None: - parent = self.config_finder.local_directory - - final_value = option.normalize(value, parent) - LOG.debug( - '%r has been normalized to %r for option "%s"', - value, - final_value, - option.config_name, - ) - return final_value - - def _parse_config(self, config_parser, parent=None): - config_dict = {} - for option_name in config_parser.options(self.program_name): - if option_name not in self.config_options: - LOG.debug( - 'Option "%s" is not registered. Ignoring.', option_name - ) - continue - option = self.config_options[option_name] - - # Use the appropriate method to parse the config value - method = config_parser.get - if option.type is int or option.action == "count": - method = config_parser.getint - elif option.action in self.GETBOOL_ACTIONS: - method = config_parser.getboolean - - value = method(self.program_name, option_name) - LOG.debug('Option "%s" returned value: %r', option_name, value) - - final_value = self._normalize_value(option, value, parent) - config_dict[option.config_name] = final_value - - return config_dict - - def is_configured_by(self, config): - """Check if the specified config parser has an appropriate section.""" - return config.has_section(self.program_name) - - def parse_local_config(self): - """Parse and return the local configuration files.""" - config = self.config_finder.local_configs() - if not self.is_configured_by(config): - LOG.debug( - "Local configuration files have no %s section", - self.program_name, - ) - return {} - - LOG.debug("Parsing local configuration files.") - return self._parse_config(config) - - def parse_cli_config(self, config_path): - """Parse and return the file specified by --config.""" - config = self.config_finder.cli_config(config_path) - if not self.is_configured_by(config): - LOG.debug( - "CLI configuration files have no %s section", - self.program_name, - ) - return {} - - LOG.debug("Parsing CLI configuration files.") - return self._parse_config(config, os.path.dirname(config_path)) - - def parse(self): - """Parse and return the local config files. - - :returns: - Dictionary of parsed configuration options - :rtype: - dict - """ - if self.config_finder.ignore_config_files: - LOG.debug( - "Refusing to parse configuration files due to user-" - "requested isolation" - ) - return {} - - if self.config_finder.config_file: - LOG.debug( - "Ignoring user and locally found configuration files. " - 'Reading only configuration from "%s" specified via ' - "--config by the user", - self.config_finder.config_file, - ) - return self.parse_cli_config(self.config_finder.config_file) - - return self.parse_local_config() - - -def get_local_plugins(config_finder): - """Get local plugins lists from config files. - - :param flake8.options.config.ConfigFileFinder config_finder: - The config file finder to use. - :returns: - LocalPlugins namedtuple containing two lists of plugin strings, - one for extension (checker) plugins and one for report plugins. - :rtype: - flake8.options.config.LocalPlugins - """ - local_plugins = LocalPlugins(extension=[], report=[], paths=[]) - if config_finder.ignore_config_files: - LOG.debug( - "Refusing to look for local plugins in configuration" - "files due to user-requested isolation" - ) - return local_plugins - - if config_finder.config_file: - LOG.debug( - 'Reading local plugins only from "%s" specified via ' - "--config by the user", - config_finder.config_file, - ) - config = config_finder.cli_config(config_finder.config_file) - config_files = [config_finder.config_file] + cfg = configparser.RawConfigParser() + if config is not None: + cfg.read(config) + cfg_dir = os.path.dirname(config) else: - config, config_files = config_finder.local_configs_with_files() + cfg_dir = pwd - base_dirs = {os.path.dirname(cf) for cf in config_files} + # TODO: remove this and replace it with configuration modifying plugins + # read the additional configs afterwards + for filename in extra: + cfg.read(filename) - section = f"{config_finder.program_name}:local-plugins" - for plugin_type in ["extension", "report"]: - if config.has_option(section, plugin_type): - local_plugins_string = config.get(section, plugin_type).strip() - plugin_type_list = getattr(local_plugins, plugin_type) - plugin_type_list.extend( - utils.parse_comma_separated_list( - local_plugins_string, regexp=utils.LOCAL_PLUGIN_LIST_RE - ) - ) - if config.has_option(section, "paths"): - raw_paths = utils.parse_comma_separated_list( - config.get(section, "paths").strip() - ) - norm_paths: List[str] = [] - for base_dir in base_dirs: - norm_paths.extend( - path - for path in utils.normalize_paths(raw_paths, parent=base_dir) - if os.path.exists(path) - ) - local_plugins.paths.extend(norm_paths) - return local_plugins + return cfg, cfg_dir -LocalPlugins = collections.namedtuple("LocalPlugins", "extension report paths") +def parse_config( + option_manager: OptionManager, + cfg: configparser.RawConfigParser, + cfg_dir: str, +) -> Dict[str, Any]: + """Parse and normalize the typed configuration options.""" + if "flake8" not in cfg: + return {} + + config_dict = {} + + for option_name in cfg["flake8"]: + option = option_manager.config_options_dict.get(option_name) + if option is None: + LOG.debug('Option "%s" is not registered. Ignoring.', option_name) + continue + + # Use the appropriate method to parse the config value + value: Any + if option.type is int or option.action == "count": + value = cfg.getint("flake8", option_name) + elif option.action in {"store_true", "store_false"}: + value = cfg.getboolean("flake8", option_name) + else: + value = cfg.get("flake8", option_name) + + LOG.debug('Option "%s" returned value: %r', option_name, value) + + final_value = option.normalize(value, cfg_dir) + assert option.config_name is not None + config_dict[option.config_name] = final_value + + return config_dict diff --git a/src/flake8/options/manager.py b/src/flake8/options/manager.py index f9765e1..a125372 100644 --- a/src/flake8/options/manager.py +++ b/src/flake8/options/manager.py @@ -462,7 +462,7 @@ class OptionManager: def parse_args( self, - args: Optional[List[str]] = None, + args: Optional[Sequence[str]] = None, values: Optional[argparse.Namespace] = None, ) -> argparse.Namespace: """Proxy to calling the OptionParser's parse_args method.""" diff --git a/tests/fixtures/config_files/README.rst b/tests/fixtures/config_files/README.rst index 4570989..8796051 100644 --- a/tests/fixtures/config_files/README.rst +++ b/tests/fixtures/config_files/README.rst @@ -16,17 +16,7 @@ Files that should not be created Purposes of existing fixtures ----------------------------- -``tests/fixtures/config_files/cli-specified.ini`` - - This should only be used when providing config file(s) specified by the - user on the command-line. - ``tests/fixtures/config_files/local-plugin.ini`` This is for testing configuring a plugin via flake8 config file instead of setuptools entry-point. - -``tests/fixtures/config_files/no-flake8-section.ini`` - - This should be used when parsing an ini file without a ``[flake8]`` - section. diff --git a/tests/fixtures/config_files/broken.ini b/tests/fixtures/config_files/broken.ini deleted file mode 100644 index 33986ae..0000000 --- a/tests/fixtures/config_files/broken.ini +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -exclude = -<<<<<<< 642f88cb1b6027e184d9a662b255f7fea4d9eacc - tests/fixtures/, -======= - tests/, ->>>>>>> HEAD - docs/ -ignore = D203 diff --git a/tests/fixtures/config_files/cli-specified-with-inline-comments.ini b/tests/fixtures/config_files/cli-specified-with-inline-comments.ini deleted file mode 100644 index 4d57e85..0000000 --- a/tests/fixtures/config_files/cli-specified-with-inline-comments.ini +++ /dev/null @@ -1,16 +0,0 @@ -[flake8] -# This is a flake8 config, there are many like it, but this is mine -ignore = - # Disable E123 - E123, - # Disable W234 - W234, - # Also disable E111 - E111 -exclude = - # Exclude foo/ - foo/, - # Exclude bar/ while we're at it - bar/, - # Exclude bogus/ - bogus/ diff --git a/tests/fixtures/config_files/cli-specified-without-inline-comments.ini b/tests/fixtures/config_files/cli-specified-without-inline-comments.ini deleted file mode 100644 index f50ba75..0000000 --- a/tests/fixtures/config_files/cli-specified-without-inline-comments.ini +++ /dev/null @@ -1,16 +0,0 @@ -[flake8] -# This is a flake8 config, there are many like it, but this is mine -# Disable E123 -# Disable W234 -# Also disable E111 -ignore = - E123, - W234, - E111 -# Exclude foo/ -# Exclude bar/ while we're at it -# Exclude bogus/ -exclude = - foo/, - bar/, - bogus/ diff --git a/tests/fixtures/config_files/cli-specified.ini b/tests/fixtures/config_files/cli-specified.ini deleted file mode 100644 index 75c5f23..0000000 --- a/tests/fixtures/config_files/cli-specified.ini +++ /dev/null @@ -1,10 +0,0 @@ -[flake8] -ignore = - E123, - W234, - E111 -exclude = - foo/, - bar/, - bogus/ -quiet = 1 diff --git a/tests/fixtures/config_files/config-with-hyphenated-options.ini b/tests/fixtures/config_files/config-with-hyphenated-options.ini deleted file mode 100644 index cc0f90e..0000000 --- a/tests/fixtures/config_files/config-with-hyphenated-options.ini +++ /dev/null @@ -1,5 +0,0 @@ -[flake8] -max-line-length = 110 -enable_extensions = - H101, - H235 diff --git a/tests/fixtures/config_files/no-flake8-section.ini b/tests/fixtures/config_files/no-flake8-section.ini deleted file mode 100644 index a85b709..0000000 --- a/tests/fixtures/config_files/no-flake8-section.ini +++ /dev/null @@ -1,20 +0,0 @@ -[tox] -minversion=2.3.1 -envlist = py26,py27,py32,py33,py34,py35,flake8 - -[testenv] -deps = - mock - pytest -commands = - py.test {posargs} - -[testenv:flake8] -skipsdist = true -skip_install = true -use_develop = false -deps = - flake8 - flake8-docstrings -commands = - flake8 diff --git a/tests/integration/test_aggregator.py b/tests/integration/test_aggregator.py index 70331a4..cdc7281 100644 --- a/tests/integration/test_aggregator.py +++ b/tests/integration/test_aggregator.py @@ -1,5 +1,4 @@ """Test aggregation of config files and command-line options.""" -import argparse import os import pytest @@ -9,24 +8,38 @@ from flake8.options import aggregator from flake8.options import config from flake8.options import manager -CLI_SPECIFIED_CONFIG = "tests/fixtures/config_files/cli-specified.ini" - @pytest.fixture def optmanager(): """Create a new OptionManager.""" - prelim_parser = argparse.ArgumentParser(add_help=False) - options.register_preliminary_options(prelim_parser) option_manager = manager.OptionManager( prog="flake8", version="3.0.0", - parents=[prelim_parser], ) options.register_default_options(option_manager) return option_manager -def test_aggregate_options_with_config(optmanager): +@pytest.fixture +def flake8_config(tmp_path): + cfg_s = """\ +[flake8] +ignore = + E123, + W234, + E111 +exclude = + foo/, + bar/, + bogus/ +quiet = 1 +""" + cfg = tmp_path.joinpath("tox.ini") + cfg.write_text(cfg_s) + return str(cfg) + + +def test_aggregate_options_with_config(optmanager, flake8_config): """Verify we aggregate options and config values appropriately.""" arguments = [ "flake8", @@ -35,11 +48,12 @@ def test_aggregate_options_with_config(optmanager): "--exclude", "tests/*", ] - config_finder = config.ConfigFileFinder( - "flake8", config_file=CLI_SPECIFIED_CONFIG - ) + cfg, cfg_dir = config.load_config(flake8_config, []) options = aggregator.aggregate_options( - optmanager, config_finder, arguments + optmanager, + cfg, + cfg_dir, + arguments, ) assert options.select == ["E11", "E34", "E402", "W", "F"] @@ -47,7 +61,7 @@ def test_aggregate_options_with_config(optmanager): assert options.exclude == [os.path.abspath("tests/*")] -def test_aggregate_options_when_isolated(optmanager): +def test_aggregate_options_when_isolated(optmanager, flake8_config): """Verify we aggregate options and config values appropriately.""" arguments = [ "flake8", @@ -56,11 +70,9 @@ def test_aggregate_options_when_isolated(optmanager): "--exclude", "tests/*", ] - config_finder = config.ConfigFileFinder("flake8", ignore_config_files=True) + cfg, cfg_dir = config.load_config(flake8_config, [], isolated=True) optmanager.extend_default_ignore(["E8"]) - options = aggregator.aggregate_options( - optmanager, config_finder, arguments - ) + options = aggregator.aggregate_options(optmanager, cfg, cfg_dir, arguments) assert options.select == ["E11", "E34", "E402", "W", "F"] assert sorted(options.ignore) == [ diff --git a/tests/unit/test_config_file_finder.py b/tests/unit/test_config_file_finder.py deleted file mode 100644 index 5116796..0000000 --- a/tests/unit/test_config_file_finder.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Tests for the ConfigFileFinder.""" -import configparser -import os -from unittest import mock - -import pytest - -from flake8.options import config - -CLI_SPECIFIED_FILEPATH = "tests/fixtures/config_files/cli-specified.ini" -BROKEN_CONFIG_PATH = "tests/fixtures/config_files/broken.ini" - - -def test_cli_config(): - """Verify opening and reading the file specified via the cli.""" - cli_filepath = CLI_SPECIFIED_FILEPATH - finder = config.ConfigFileFinder("flake8") - - parsed_config = finder.cli_config(cli_filepath) - assert parsed_config.has_section("flake8") - - -@pytest.mark.parametrize( - "cwd,expected", - [ - # Root directory of project - ( - os.path.abspath("."), - [os.path.abspath("setup.cfg"), os.path.abspath("tox.ini")], - ), - # Subdirectory of project directory - ( - os.path.abspath("src"), - [os.path.abspath("setup.cfg"), os.path.abspath("tox.ini")], - ), - # Outside of project directory - (os.path.abspath("/"), []), - ], -) -def test_generate_possible_local_files(cwd, expected): - """Verify generation of all possible config paths.""" - finder = config.ConfigFileFinder("flake8") - - with mock.patch.object(os, "getcwd", return_value=cwd): - config_files = list(finder.generate_possible_local_files()) - - assert config_files == expected - - -@pytest.mark.parametrize( - "extra_config_files,expected", - [ - # Extra config files specified - ( - [CLI_SPECIFIED_FILEPATH], - [ - os.path.abspath("setup.cfg"), - os.path.abspath("tox.ini"), - os.path.abspath(CLI_SPECIFIED_FILEPATH), - ], - ), - # Missing extra config files specified - ( - [ - CLI_SPECIFIED_FILEPATH, - "tests/fixtures/config_files/missing.ini", - ], - [ - os.path.abspath("setup.cfg"), - os.path.abspath("tox.ini"), - os.path.abspath(CLI_SPECIFIED_FILEPATH), - ], - ), - ], -) -def test_local_config_files(extra_config_files, expected): - """Verify discovery of local config files.""" - finder = config.ConfigFileFinder("flake8", extra_config_files) - - assert list(finder.local_config_files()) == expected - - -def test_local_configs(): - """Verify we return a ConfigParser.""" - finder = config.ConfigFileFinder("flake8") - - assert isinstance(finder.local_configs(), configparser.RawConfigParser) - - -@pytest.mark.parametrize( - "files", - [ - [BROKEN_CONFIG_PATH], - [CLI_SPECIFIED_FILEPATH, BROKEN_CONFIG_PATH], - ], -) -def test_read_config_catches_broken_config_files(files): - """Verify that we do not allow the exception to bubble up.""" - _, parsed = config.ConfigFileFinder._read_config(*files) - assert BROKEN_CONFIG_PATH not in parsed - - -def test_read_config_catches_decoding_errors(tmpdir): - """Verify that we do not allow the exception to bubble up.""" - setup_cfg = tmpdir.join("setup.cfg") - # pick bytes that are unlikely to decode - setup_cfg.write_binary(b"[x]\ny = \x81\x8d\x90\x9d") - _, parsed = config.ConfigFileFinder._read_config(setup_cfg.strpath) - assert parsed == [] - - -def test_config_file_default_value(): - """Verify the default 'config_file' attribute value.""" - finder = config.ConfigFileFinder("flake8") - assert finder.config_file is None - - -def test_setting_config_file_value(): - """Verify the 'config_file' attribute matches constructed value.""" - config_file_value = "flake8.ini" - finder = config.ConfigFileFinder("flake8", config_file=config_file_value) - assert finder.config_file == config_file_value - - -def test_ignore_config_files_default_value(): - """Verify the default 'ignore_config_files' attribute value.""" - finder = config.ConfigFileFinder("flake8") - assert finder.ignore_config_files is False - - -@pytest.mark.parametrize( - "ignore_config_files_arg", - [ - False, - True, - ], -) -def test_setting_ignore_config_files_value(ignore_config_files_arg): - """Verify the 'ignore_config_files' attribute matches constructed value.""" - finder = config.ConfigFileFinder( - "flake8", ignore_config_files=ignore_config_files_arg - ) - assert finder.ignore_config_files is ignore_config_files_arg diff --git a/tests/unit/test_config_parser.py b/tests/unit/test_config_parser.py deleted file mode 100644 index 0baa108..0000000 --- a/tests/unit/test_config_parser.py +++ /dev/null @@ -1,188 +0,0 @@ -"""Unit tests for flake8.options.config.ConfigParser.""" -import os -from unittest import mock - -import pytest - -from flake8.options import config -from flake8.options import manager - - -@pytest.fixture -def optmanager(): - """Generate an OptionManager with simple values.""" - return manager.OptionManager(prog="flake8", version="3.0.0a1") - - -@pytest.fixture -def config_finder(): - """Generate a simple ConfigFileFinder.""" - return config.ConfigFileFinder("flake8") - - -def test_parse_cli_config(optmanager, config_finder): - """Parse the specified config file as a cli config file.""" - optmanager.add_option( - "--exclude", - parse_from_config=True, - comma_separated_list=True, - normalize_paths=True, - ) - optmanager.add_option( - "--ignore", parse_from_config=True, comma_separated_list=True - ) - optmanager.add_option("--quiet", parse_from_config=True, action="count") - parser = config.ConfigParser(optmanager, config_finder) - - config_file = "tests/fixtures/config_files/cli-specified.ini" - parsed_config = parser.parse_cli_config(config_file) - - config_dir = os.path.dirname(config_file) - assert parsed_config == { - "ignore": ["E123", "W234", "E111"], - "exclude": [ - os.path.abspath(os.path.join(config_dir, "foo/")), - os.path.abspath(os.path.join(config_dir, "bar/")), - os.path.abspath(os.path.join(config_dir, "bogus/")), - ], - "quiet": 1, - } - - -@pytest.mark.parametrize( - "filename,is_configured_by", - [ - ("tests/fixtures/config_files/cli-specified.ini", True), - ("tests/fixtures/config_files/no-flake8-section.ini", False), - ], -) -def test_is_configured_by( - filename, is_configured_by, optmanager, config_finder -): - """Verify the behaviour of the is_configured_by method.""" - parsed_config, _ = config.ConfigFileFinder._read_config(filename) - parser = config.ConfigParser(optmanager, config_finder) - - assert parser.is_configured_by(parsed_config) is is_configured_by - - -def test_parse_local_config(optmanager, config_finder): - """Verify parsing of local config files.""" - optmanager.add_option( - "--exclude", - parse_from_config=True, - comma_separated_list=True, - normalize_paths=True, - ) - optmanager.add_option( - "--ignore", parse_from_config=True, comma_separated_list=True - ) - optmanager.add_option("--quiet", parse_from_config=True, action="count") - parser = config.ConfigParser(optmanager, config_finder) - - with mock.patch.object(config_finder, "local_config_files") as localcfs: - localcfs.return_value = [ - "tests/fixtures/config_files/cli-specified.ini" - ] - parsed_config = parser.parse_local_config() - - assert parsed_config == { - "ignore": ["E123", "W234", "E111"], - "exclude": [ - os.path.abspath("foo/"), - os.path.abspath("bar/"), - os.path.abspath("bogus/"), - ], - "quiet": 1, - } - - -def test_parse_isolates_config(optmanager): - """Verify behaviour of the parse method with isolated=True.""" - config_finder = mock.MagicMock() - config_finder.ignore_config_files = True - parser = config.ConfigParser(optmanager, config_finder) - - assert parser.parse() == {} - assert config_finder.local_configs.called is False - - -def test_parse_uses_cli_config(optmanager): - """Verify behaviour of the parse method with a specified config.""" - config_file_value = "foo.ini" - config_finder = mock.MagicMock() - config_finder.config_file = config_file_value - config_finder.ignore_config_files = False - parser = config.ConfigParser(optmanager, config_finder) - - parser.parse() - config_finder.cli_config.assert_called_once_with(config_file_value) - - -@pytest.mark.parametrize( - "config_fixture_path", - [ - "tests/fixtures/config_files/cli-specified.ini", - "tests/fixtures/config_files/cli-specified-with-inline-comments.ini", - "tests/fixtures/config_files/cli-specified-without-inline-comments.ini", # noqa: E501 - ], -) -def test_parsed_configs_are_equivalent( - optmanager, config_finder, config_fixture_path -): - """Verify the each file matches the expected parsed output. - - This is used to ensure our documented behaviour does not regress. - """ - optmanager.add_option( - "--exclude", - parse_from_config=True, - comma_separated_list=True, - normalize_paths=True, - ) - optmanager.add_option( - "--ignore", parse_from_config=True, comma_separated_list=True - ) - parser = config.ConfigParser(optmanager, config_finder) - - with mock.patch.object(config_finder, "local_config_files") as localcfs: - localcfs.return_value = [config_fixture_path] - parsed_config = parser.parse() - - assert parsed_config["ignore"] == ["E123", "W234", "E111"] - assert parsed_config["exclude"] == [ - os.path.abspath("foo/"), - os.path.abspath("bar/"), - os.path.abspath("bogus/"), - ] - - -@pytest.mark.parametrize( - "config_file", - ["tests/fixtures/config_files/config-with-hyphenated-options.ini"], -) -def test_parsed_hyphenated_and_underscored_names( - optmanager, config_finder, config_file -): - """Verify we find hyphenated option names as well as underscored. - - This tests for options like --max-line-length and --enable-extensions - which are able to be specified either as max-line-length or - max_line_length in our config files. - """ - optmanager.add_option( - "--max-line-length", parse_from_config=True, type=int - ) - optmanager.add_option( - "--enable-extensions", - parse_from_config=True, - comma_separated_list=True, - ) - parser = config.ConfigParser(optmanager, config_finder) - - with mock.patch.object(config_finder, "local_config_files") as localcfs: - localcfs.return_value = [config_file] - parsed_config = parser.parse() - - assert parsed_config["max_line_length"] == 110 - assert parsed_config["enable_extensions"] == ["H101", "H235"] diff --git a/tests/unit/test_get_local_plugins.py b/tests/unit/test_get_local_plugins.py deleted file mode 100644 index 44d8725..0000000 --- a/tests/unit/test_get_local_plugins.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Tests for get_local_plugins.""" -from unittest import mock - -from flake8.options import config - - -def test_get_local_plugins_respects_isolated(): - """Verify behaviour of get_local_plugins with isolated=True.""" - config_finder = mock.MagicMock() - config_finder.ignore_config_files = True - - local_plugins = config.get_local_plugins(config_finder) - - assert local_plugins.extension == [] - assert local_plugins.report == [] - assert config_finder.local_configs.called is False - assert config_finder.user_config.called is False - - -def test_get_local_plugins_uses_cli_config(): - """Verify behaviour of get_local_plugins with a specified config.""" - config_obj = mock.Mock() - config_finder = mock.MagicMock() - config_finder.cli_config.return_value = config_obj - config_finder.ignore_config_files = False - config_obj.get.return_value = "" - config_file_value = "foo.ini" - config_finder.config_file = config_file_value - - config.get_local_plugins(config_finder) - - config_finder.cli_config.assert_called_once_with(config_file_value) - - -def test_get_local_plugins(): - """Verify get_local_plugins returns expected plugins.""" - config_fixture_path = "tests/fixtures/config_files/local-plugin.ini" - config_finder = config.ConfigFileFinder("flake8") - - with mock.patch.object(config_finder, "local_config_files") as localcfs: - localcfs.return_value = [config_fixture_path] - local_plugins = config.get_local_plugins(config_finder) - - assert local_plugins.extension == [ - "XE = tests.integration.test_plugins:ExtensionTestPlugin" - ] - assert local_plugins.report == [ - "XR = tests.integration.test_plugins:ReportTestPlugin" - ] diff --git a/tests/unit/test_legacy_api.py b/tests/unit/test_legacy_api.py index 1dcdeb6..671b21a 100644 --- a/tests/unit/test_legacy_api.py +++ b/tests/unit/test_legacy_api.py @@ -1,5 +1,6 @@ """Tests for Flake8's legacy API.""" import argparse +import configparser import os.path from unittest import mock @@ -7,7 +8,7 @@ import pytest from flake8.api import legacy as api from flake8.formatting import base as formatter -from flake8.options.config import ConfigFileFinder +from flake8.options import config def test_get_style_guide(): @@ -22,22 +23,21 @@ def test_get_style_guide(): mockedapp = mock.Mock() mockedapp.parse_preliminary_options.return_value = (prelim_opts, []) mockedapp.program = "flake8" - with mock.patch( - "flake8.api.legacy.config.ConfigFileFinder" - ) as mock_config_finder: # noqa: E501 - config_finder = ConfigFileFinder(mockedapp.program) - mock_config_finder.return_value = config_finder + 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(config_finder) + mockedapp.find_plugins.assert_called_once_with(cfg, cfg_dir) mockedapp.register_plugin_options.assert_called_once_with() mockedapp.parse_configuration_and_cli.assert_called_once_with( - config_finder, [] + cfg, cfg_dir, [] ) mockedapp.make_formatter.assert_called_once_with() mockedapp.make_guide.assert_called_once_with() diff --git a/tests/unit/test_options_config.py b/tests/unit/test_options_config.py new file mode 100644 index 0000000..75bfac5 --- /dev/null +++ b/tests/unit/test_options_config.py @@ -0,0 +1,166 @@ +import configparser + +import pytest + +from flake8.main.options import register_default_options +from flake8.options import config +from flake8.options.manager import OptionManager + + +def test_config_not_found_returns_none(tmp_path): + assert config._find_config_file(str(tmp_path)) is None + + +def test_config_file_without_section_is_not_considered(tmp_path): + tmp_path.joinpath("setup.cfg").touch() + + assert config._find_config_file(str(tmp_path)) is None + + +def test_config_file_with_parse_error_is_not_considered(tmp_path, caplog): + tmp_path.joinpath("setup.cfg").write_text("[error") + + assert config._find_config_file(str(tmp_path)) is None + + assert len(caplog.record_tuples) == 1 + ((mod, level, msg),) = caplog.record_tuples + assert (mod, level) == ("flake8.options.config", 30) + assert msg.startswith("ignoring unparseable config ") + + +def test_config_file_with_encoding_error_is_not_considered(tmp_path, caplog): + tmp_path.joinpath("setup.cfg").write_bytes(b"\xa0\xef\xfe\x12") + + assert config._find_config_file(str(tmp_path)) is None + + assert len(caplog.record_tuples) == 1 + ((mod, level, msg),) = caplog.record_tuples + assert (mod, level) == ("flake8.options.config", 30) + assert msg.startswith("ignoring unparseable config ") + + +@pytest.mark.parametrize("cfg_name", ("setup.cfg", "tox.ini", ".flake8")) +def test_find_config_file_exists_at_path(tmp_path, cfg_name): + expected = tmp_path.joinpath(cfg_name) + expected.write_text("[flake8]") + + assert config._find_config_file(str(tmp_path)) == str(expected) + + +@pytest.mark.parametrize("section", ("flake8", "flake8:local-plugins")) +def test_find_config_either_section(tmp_path, section): + expected = tmp_path.joinpath("setup.cfg") + expected.write_text(f"[{section}]") + + assert config._find_config_file(str(tmp_path)) == str(expected) + + +def test_find_config_searches_upwards(tmp_path): + subdir = tmp_path.joinpath("d") + subdir.mkdir() + + expected = tmp_path.joinpath("setup.cfg") + expected.write_text("[flake8]") + + assert config._find_config_file(str(subdir)) == str(expected) + + +def test_load_config_config_specified_skips_discovery(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + custom_cfg = tmpdir.join("custom.cfg") + custom_cfg.write("[flake8]\nindent-size=8\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(str(custom_cfg), [], isolated=False) + + assert cfg.get("flake8", "indent-size") == "8" + assert cfg_dir == str(tmpdir) + + +def test_load_config_no_config_file_does_discovery(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=False) + + assert cfg.get("flake8", "indent-size") == "2" + assert cfg_dir == str(tmpdir) + + +def test_load_config_no_config_found_sets_cfg_dir_to_pwd(tmpdir): + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=False) + + assert cfg.sections() == [] + assert cfg_dir == str(tmpdir) + + +def test_load_config_isolated_ignores_configuration(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [], isolated=True) + + assert cfg.sections() == [] + assert cfg_dir == str(tmpdir) + + +def test_load_config_append_config(tmpdir): + tmpdir.join("setup.cfg").write("[flake8]\nindent-size=2\n") + other = tmpdir.join("other.cfg") + other.write("[flake8]\nindent-size=8\n") + + with tmpdir.as_cwd(): + cfg, cfg_dir = config.load_config(None, [str(other)], isolated=False) + + assert cfg.get("flake8", "indent-size") == "8" + assert cfg_dir == str(tmpdir) + + +@pytest.fixture +def opt_manager(): + ret = OptionManager(prog="flake8", version="123") + register_default_options(ret) + return ret + + +def test_parse_config_no_values(tmp_path, opt_manager): + cfg = configparser.RawConfigParser() + ret = config.parse_config(opt_manager, cfg, tmp_path) + assert ret == {} + + +def test_parse_config_typed_values(tmp_path, opt_manager): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8") + cfg.set("flake8", "indent_size", "2") + cfg.set("flake8", "hang_closing", "true") + # test normalizing dashed-options + cfg.set("flake8", "extend-exclude", "d/1,d/2") + + ret = config.parse_config(opt_manager, cfg, str(tmp_path)) + assert ret == { + "indent_size": 2, + "hang_closing": True, + "extend_exclude": [ + str(tmp_path.joinpath("d/1")), + str(tmp_path.joinpath("d/2")), + ], + } + + +def test_parse_config_ignores_unknowns(tmp_path, opt_manager, caplog): + cfg = configparser.RawConfigParser() + cfg.add_section("flake8") + cfg.set("flake8", "wat", "wat") + + ret = config.parse_config(opt_manager, cfg, str(tmp_path)) + assert ret == {} + + assert caplog.record_tuples == [ + ( + "flake8.options.config", + 10, + 'Option "wat" is not registered. Ignoring.', + ) + ]