Add support for local (in-repo, non-setuptools) plugins.

Closes #357
This commit is contained in:
Carl Meyer 2017-08-03 00:25:37 -07:00
parent 6df26ffd57
commit 4e58068657
15 changed files with 391 additions and 133 deletions

View file

@ -1,11 +1,11 @@
About this directory
====================
The files in this directory are test fixtures for unit and integration tests.
Their purpose is described below. Please note the list of file names that can
The files in this directory are test fixtures for unit and integration tests.
Their purpose is described below. Please note the list of file names that can
not be created as they are already used by tests.
New fixtures are preferred over updating existing features unless existing
New fixtures are preferred over updating existing features unless existing
tests will fail.
Files that should not be created
@ -26,6 +26,10 @@ Purposes of existing fixtures
This should be used when providing config files that would have been found
by looking for config files in the current working project directory.
``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``

View file

@ -0,0 +1,5 @@
[flake8:local-plugins]
extension =
XE = test_plugins:ExtensionTestPlugin
report =
XR = test_plugins:ReportTestPlugin

View file

@ -5,6 +5,7 @@ import pytest
from flake8.main import options
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'
@ -25,7 +26,9 @@ def test_aggregate_options_with_config(optmanager):
"""Verify we aggregate options and config values appropriately."""
arguments = ['flake8', '--config', CLI_SPECIFIED_CONFIG, '--select',
'E11,E34,E402,W,F', '--exclude', 'tests/*']
options, args = aggregator.aggregate_options(optmanager, arguments)
config_finder = config.ConfigFileFinder('flake8', arguments, [])
options, args = aggregator.aggregate_options(
optmanager, config_finder, arguments)
assert options.config == CLI_SPECIFIED_CONFIG
assert options.select == ['E11', 'E34', 'E402', 'W', 'F']
@ -37,8 +40,10 @@ def test_aggregate_options_when_isolated(optmanager):
"""Verify we aggregate options and config values appropriately."""
arguments = ['flake8', '--isolated', '--select', 'E11,E34,E402,W,F',
'--exclude', 'tests/*']
config_finder = config.ConfigFileFinder('flake8', arguments, [])
optmanager.extend_default_ignore(['E8'])
options, args = aggregator.aggregate_options(optmanager, arguments)
options, args = aggregator.aggregate_options(
optmanager, config_finder, arguments)
assert options.isolated is True
assert options.select == ['E11', 'E34', 'E402', 'W', 'F']

View file

@ -0,0 +1,58 @@
"""Integration tests for plugin loading."""
from flake8.main import application
LOCAL_PLUGIN_CONFIG = 'tests/fixtures/config_files/local-plugin.ini'
class ExtensionTestPlugin(object):
"""Extension test plugin."""
name = 'ExtensionTestPlugin'
version = '1.0.0'
def __init__(self, tree):
"""Construct an instance of test plugin."""
pass
def run(self):
"""Do nothing."""
pass
@classmethod
def add_options(cls, parser):
"""Register options."""
parser.add_option('--anopt')
class ReportTestPlugin(object):
"""Report test plugin."""
name = 'ReportTestPlugin'
version = '1.0.0'
def __init__(self, tree):
"""Construct an instance of test plugin."""
pass
def run(self):
"""Do nothing."""
pass
def test_enable_local_plugin_from_config():
"""App can load a local plugin from config file."""
app = application.Application()
app.initialize(['flake8', '--config', LOCAL_PLUGIN_CONFIG])
assert app.check_plugins['XE'].plugin is ExtensionTestPlugin
assert app.formatting_plugins['XR'].plugin is ReportTestPlugin
def test_local_plugin_can_add_option():
"""A local plugin can add a CLI option."""
app = application.Application()
app.initialize(
['flake8', '--config', LOCAL_PLUGIN_CONFIG, '--anopt', 'foo'])
assert app.options.anopt == 'foo'

View file

@ -39,6 +39,18 @@ def test_cli_config():
assert parsed_config.has_section('flake8')
def test_cli_config_double_read():
"""Second request for CLI config is cached."""
finder = config.ConfigFileFinder('flake8', None, [])
parsed_config = finder.cli_config(CLI_SPECIFIED_FILEPATH)
boom = Exception("second request for CLI config not cached")
with mock.patch.object(finder, '_read_config', side_effect=boom):
parsed_config_2 = finder.cli_config(CLI_SPECIFIED_FILEPATH)
assert parsed_config is parsed_config_2
@pytest.mark.parametrize('args,expected', [
# No arguments, common prefix of abspath('.')
([],
@ -105,6 +117,18 @@ def test_local_configs():
assert isinstance(finder.local_configs(), configparser.RawConfigParser)
def test_local_configs_double_read():
"""Second request for local configs is cached."""
finder = config.ConfigFileFinder('flake8', None, [])
first_read = finder.local_configs()
boom = Exception("second request for local configs not cached")
with mock.patch.object(finder, '_read_config', side_effect=boom):
second_read = finder.local_configs()
assert first_read is second_read
@pytest.mark.parametrize('files', [
[BROKEN_CONFIG_PATH],
[CLI_SPECIFIED_FILEPATH, BROKEN_CONFIG_PATH],

View file

@ -0,0 +1,38 @@
"""Tests for get_local_plugins."""
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()
local_plugins = config.get_local_plugins(config_finder, isolated=True)
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_finder = mock.MagicMock()
config.get_local_plugins(config_finder, cli_config='foo.ini')
config_finder.cli_config.assert_called_once_with('foo.ini')
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 = test_plugins:ExtensionTestPlugin']
assert local_plugins.report == ['XR = test_plugins:ReportTestPlugin']

View file

@ -9,11 +9,15 @@ from flake8.formatting import base as formatter
def test_get_style_guide():
"""Verify the methods called on our internal Application."""
mockedapp = mock.Mock()
mockedapp.prelim_opts.verbose = 0
mockedapp.prelim_opts.output_file = None
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_and_args.assert_called_once_with([])
mockedapp.make_config_finder.assert_called_once_with()
mockedapp.find_plugins.assert_called_once_with()
mockedapp.register_plugin_options.assert_called_once_with()
mockedapp.parse_configuration_and_cli.assert_called_once_with([])

View file

@ -14,33 +14,13 @@ def optmanager():
return manager.OptionManager(prog='flake8', version='3.0.0a1')
@pytest.mark.parametrize('args,extra_config_files', [
(None, None),
(None, []),
(None, ['foo.ini']),
('flake8/', []),
('flake8/', ['foo.ini']),
])
def test_creates_its_own_config_file_finder(args, extra_config_files,
optmanager):
"""Verify we create a ConfigFileFinder correctly."""
class_path = 'flake8.options.config.ConfigFileFinder'
with mock.patch(class_path) as ConfigFileFinder:
parser = config.MergedConfigParser(
option_manager=optmanager,
extra_config_files=extra_config_files,
args=args,
)
assert parser.program_name == 'flake8'
ConfigFileFinder.assert_called_once_with(
'flake8',
args,
extra_config_files or [],
)
@pytest.fixture
def config_finder():
"""Generate a simple ConfigFileFinder."""
return config.ConfigFileFinder('flake8', [], [])
def test_parse_cli_config(optmanager):
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,
@ -51,7 +31,7 @@ def test_parse_cli_config(optmanager):
action='count')
optmanager.add_option('--quiet', parse_from_config=True,
action='count')
parser = config.MergedConfigParser(optmanager)
parser = config.MergedConfigParser(optmanager, config_finder)
parsed_config = parser.parse_cli_config(
'tests/fixtures/config_files/cli-specified.ini'
@ -72,15 +52,16 @@ def test_parse_cli_config(optmanager):
('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):
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.MergedConfigParser(optmanager)
parser = config.MergedConfigParser(optmanager, config_finder)
assert parser.is_configured_by(parsed_config) is is_configured_by
def test_parse_user_config(optmanager):
def test_parse_user_config(optmanager, config_finder):
"""Verify parsing of user config files."""
optmanager.add_option('--exclude', parse_from_config=True,
comma_separated_list=True,
@ -91,7 +72,7 @@ def test_parse_user_config(optmanager):
action='count')
optmanager.add_option('--quiet', parse_from_config=True,
action='count')
parser = config.MergedConfigParser(optmanager)
parser = config.MergedConfigParser(optmanager, config_finder)
with mock.patch.object(parser.config_finder, 'user_config_file') as usercf:
usercf.return_value = 'tests/fixtures/config_files/cli-specified.ini'
@ -109,7 +90,7 @@ def test_parse_user_config(optmanager):
}
def test_parse_local_config(optmanager):
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,
@ -120,8 +101,7 @@ def test_parse_local_config(optmanager):
action='count')
optmanager.add_option('--quiet', parse_from_config=True,
action='count')
parser = config.MergedConfigParser(optmanager)
config_finder = parser.config_finder
parser = config.MergedConfigParser(optmanager, config_finder)
with mock.patch.object(config_finder, 'local_config_files') as localcfs:
localcfs.return_value = [
@ -141,7 +121,7 @@ def test_parse_local_config(optmanager):
}
def test_merge_user_and_local_config(optmanager):
def test_merge_user_and_local_config(optmanager, config_finder):
"""Verify merging of parsed user and local config files."""
optmanager.add_option('--exclude', parse_from_config=True,
comma_separated_list=True,
@ -150,8 +130,7 @@ def test_merge_user_and_local_config(optmanager):
comma_separated_list=True)
optmanager.add_option('--select', parse_from_config=True,
comma_separated_list=True)
parser = config.MergedConfigParser(optmanager)
config_finder = parser.config_finder
parser = config.MergedConfigParser(optmanager, config_finder)
with mock.patch.object(config_finder, 'local_config_files') as localcfs:
localcfs.return_value = [
@ -172,23 +151,23 @@ def test_merge_user_and_local_config(optmanager):
}
@mock.patch('flake8.options.config.ConfigFileFinder')
def test_parse_isolates_config(ConfigFileManager, optmanager):
def test_parse_isolates_config(optmanager):
"""Verify behaviour of the parse method with isolated=True."""
parser = config.MergedConfigParser(optmanager)
config_finder = mock.MagicMock()
parser = config.MergedConfigParser(optmanager, config_finder)
assert parser.parse(isolated=True) == {}
assert parser.config_finder.local_configs.called is False
assert parser.config_finder.user_config.called is False
assert config_finder.local_configs.called is False
assert config_finder.user_config.called is False
@mock.patch('flake8.options.config.ConfigFileFinder')
def test_parse_uses_cli_config(ConfigFileManager, optmanager):
def test_parse_uses_cli_config(optmanager):
"""Verify behaviour of the parse method with a specified config."""
parser = config.MergedConfigParser(optmanager)
config_finder = mock.MagicMock()
parser = config.MergedConfigParser(optmanager, config_finder)
parser.parse(cli_config='foo.ini')
parser.config_finder.cli_config.assert_called_once_with('foo.ini')
config_finder.cli_config.assert_called_once_with('foo.ini')
@pytest.mark.parametrize('config_fixture_path', [
@ -196,7 +175,8 @@ def test_parse_uses_cli_config(ConfigFileManager, optmanager):
'tests/fixtures/config_files/cli-specified-with-inline-comments.ini',
'tests/fixtures/config_files/cli-specified-without-inline-comments.ini',
])
def test_parsed_configs_are_equivalent(optmanager, config_fixture_path):
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.
@ -206,8 +186,7 @@ def test_parsed_configs_are_equivalent(optmanager, config_fixture_path):
normalize_paths=True)
optmanager.add_option('--ignore', parse_from_config=True,
comma_separated_list=True)
parser = config.MergedConfigParser(optmanager)
config_finder = parser.config_finder
parser = config.MergedConfigParser(optmanager, config_finder)
with mock.patch.object(config_finder, 'local_config_files') as localcfs:
localcfs.return_value = [config_fixture_path]
@ -227,7 +206,8 @@ def test_parsed_configs_are_equivalent(optmanager, config_fixture_path):
@pytest.mark.parametrize('config_file', [
'tests/fixtures/config_files/config-with-hyphenated-options.ini'
])
def test_parsed_hyphenated_and_underscored_names(optmanager, config_file):
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
@ -238,8 +218,7 @@ def test_parsed_hyphenated_and_underscored_names(optmanager, config_file):
type='int')
optmanager.add_option('--enable-extensions', parse_from_config=True,
comma_separated_list=True)
parser = config.MergedConfigParser(optmanager)
config_finder = parser.config_finder
parser = config.MergedConfigParser(optmanager, config_finder)
with mock.patch.object(config_finder, 'local_config_files') as localcfs:
localcfs.return_value = [config_file]

View file

@ -48,3 +48,15 @@ def test_handles_mapping_functions_across_plugins(iter_entry_points):
plugins = [plugin_mgr.plugins[name] for name in plugin_mgr.names]
assert list(plugin_mgr.map(lambda x: x)) == plugins
@mock.patch('pkg_resources.iter_entry_points')
def test_local_plugins(iter_entry_points):
"""Verify PluginManager can load given local plugins."""
iter_entry_points.return_value = []
plugin_mgr = manager.PluginManager(
namespace='testing.pkg_resources',
local_plugins=['X = path.to:Plugin']
)
assert plugin_mgr.plugins['X'].entry_point.module_name == 'path.to'

View file

@ -53,7 +53,7 @@ def test_instantiates_a_manager(PluginManager):
"""Verify we create a PluginManager on instantiation."""
FakeTestType()
PluginManager.assert_called_once_with(TEST_NAMESPACE)
PluginManager.assert_called_once_with(TEST_NAMESPACE, local_plugins=None)
@mock.patch('flake8.plugins.manager.PluginManager')