refactor and simplify configuration loading

This commit is contained in:
Anthony Sottile 2021-11-22 19:42:50 -05:00
parent dc9b7eb3e4
commit 65c893728e
20 changed files with 351 additions and 883 deletions

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