Merge pull request #1472 from asottile/config_discovery

refactor and simplify configuration loading
This commit is contained in:
Anthony Sottile 2021-11-22 19:51:21 -05:00 committed by GitHub
commit 841489e312
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 351 additions and 883 deletions

View file

@ -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

View file

@ -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.

View file

@ -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()

View file

@ -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 "

View file

@ -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)',

View file

@ -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

View file

@ -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."""

View file

@ -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.

View file

@ -1,9 +0,0 @@
[flake8]
exclude =
<<<<<<< 642f88cb1b6027e184d9a662b255f7fea4d9eacc
tests/fixtures/,
=======
tests/,
>>>>>>> HEAD
docs/
ignore = D203

View file

@ -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/

View file

@ -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/

View file

@ -1,10 +0,0 @@
[flake8]
ignore =
E123,
W234,
E111
exclude =
foo/,
bar/,
bogus/
quiet = 1

View file

@ -1,5 +0,0 @@
[flake8]
max-line-length = 110
enable_extensions =
H101,
H235

View file

@ -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

View file

@ -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) == [

View file

@ -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

View file

@ -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"]

View file

@ -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"
]

View file

@ -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()

View file

@ -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.',
)
]