Merge branch 'local-plugins' into 'master'

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

Closes #357

See merge request !197
This commit is contained in:
Ian Stapleton Cordasco 2017-08-07 11:19:30 +00:00
commit 3169b6072b
16 changed files with 408 additions and 133 deletions

View file

@ -6,6 +6,7 @@ In 3.0 we no longer have an "engine" module but we maintain the API from it.
import logging
import os.path
import flake8
from flake8.formatting import base as formatter
from flake8.main import application as app
@ -26,6 +27,10 @@ def get_style_guide(**kwargs):
:class:`StyleGuide`
"""
application = app.Application()
application.parse_preliminary_options_and_args([])
flake8.configure_logging(
application.prelim_opts.verbose, application.prelim_opts.output_file)
application.make_config_finder()
application.find_plugins()
application.register_plugin_options()
application.parse_configuration_and_cli([])

View file

@ -12,7 +12,7 @@ from flake8 import exceptions
from flake8 import style_guide
from flake8 import utils
from flake8.main import options
from flake8.options import aggregator
from flake8.options import aggregator, config
from flake8.options import manager
from flake8.plugins import manager as plugin_manager
@ -45,34 +45,16 @@ class Application(object):
prog='flake8', version=flake8.__version__
)
options.register_default_options(self.option_manager)
#: The preliminary options parsed from CLI before plugins are loaded,
#: into a :class:`optparse.Values` instance
self.prelim_opts = None
#: The preliminary arguments parsed from CLI before plugins are loaded
self.prelim_args = None
#: The instance of :class:`flake8.options.config.ConfigFileFinder`
self.config_finder = None
# We haven't found or registered our plugins yet, so let's defer
# printing the version until we aggregate options from config files
# and the command-line. First, let's clone our arguments on the CLI,
# then we'll attempt to remove ``--version`` so that we can avoid
# triggering the "version" action in optparse. If it's not there, we
# do not need to worry and we can continue. If it is, we successfully
# defer printing the version until just a little bit later.
# Similarly we have to defer printing the help text until later.
args = sys.argv[:]
try:
args.remove('--version')
except ValueError:
pass
try:
args.remove('--help')
except ValueError:
pass
try:
args.remove('-h')
except ValueError:
pass
preliminary_opts, _ = self.option_manager.parse_known_args(args)
# Set the verbosity of the program
flake8.configure_logging(preliminary_opts.verbose,
preliminary_opts.output_file)
#: The :class:`flake8.options.config.LocalPlugins` found in config
self.local_plugins = None
#: The instance of :class:`flake8.plugins.manager.Checkers`
self.check_plugins = None
#: The instance of :class:`flake8.plugins.manager.Listeners`
@ -111,6 +93,48 @@ class Application(object):
#: The parsed diff information
self.parsed_diff = {}
def parse_preliminary_options_and_args(self, argv=None):
"""Get preliminary options and args from CLI, pre-plugin-loading.
We need to know the values of a few standard options and args now, so
that we can find config files and configure logging.
Since plugins aren't loaded yet, there may be some as-yet-unknown
options; we ignore those for now, they'll be parsed later when we do
real option parsing.
Sets self.prelim_opts and self.prelim_args.
:param list argv:
Command-line arguments passed in directly.
"""
# We haven't found or registered our plugins yet, so let's defer
# printing the version until we aggregate options from config files
# and the command-line. First, let's clone our arguments on the CLI,
# then we'll attempt to remove ``--version`` so that we can avoid
# triggering the "version" action in optparse. If it's not there, we
# do not need to worry and we can continue. If it is, we successfully
# defer printing the version until just a little bit later.
# Similarly we have to defer printing the help text until later.
args = (argv or sys.argv)[:]
try:
args.remove('--version')
except ValueError:
pass
try:
args.remove('--help')
except ValueError:
pass
try:
args.remove('-h')
except ValueError:
pass
opts, args = self.option_manager.parse_known_args(args)
# parse_known_args includes unknown options as args; get rid of them
args = [a for a in args if not a.startswith('-')]
self.prelim_opts, self.prelim_args = opts, args
def exit(self):
# type: () -> NoneType
"""Handle finalization and exiting the program.
@ -125,6 +149,17 @@ class Application(object):
raise SystemExit((self.result_count > 0) or
self.catastrophic_failure)
def make_config_finder(self):
"""Make our ConfigFileFinder based on preliminary opts and args."""
if self.config_finder is None:
extra_config_files = utils.normalize_paths(
self.prelim_opts.append_config)
self.config_finder = config.ConfigFileFinder(
self.option_manager.program_name,
self.prelim_args,
extra_config_files,
)
def find_plugins(self):
# type: () -> NoneType
"""Find and load the plugins for this application.
@ -135,14 +170,23 @@ class Application(object):
of finding plugins (via :mod:`pkg_resources`) we want this to be
idempotent and so only update those attributes if they are ``None``.
"""
if self.local_plugins is None:
self.local_plugins = config.get_local_plugins(
self.config_finder,
self.prelim_opts.config,
self.prelim_opts.isolated,
)
if self.check_plugins is None:
self.check_plugins = plugin_manager.Checkers()
self.check_plugins = plugin_manager.Checkers(
self.local_plugins.extension)
if self.listening_plugins is None:
self.listening_plugins = plugin_manager.Listeners()
if self.formatting_plugins is None:
self.formatting_plugins = plugin_manager.ReportFormatters()
self.formatting_plugins = plugin_manager.ReportFormatters(
self.local_plugins.report)
self.check_plugins.load_plugins()
self.listening_plugins.load_plugins()
@ -165,7 +209,7 @@ class Application(object):
"""
if self.options is None and self.args is None:
self.options, self.args = aggregator.aggregate_options(
self.option_manager, argv
self.option_manager, self.config_finder, argv
)
self.running_against_diff = self.options.diff
@ -314,6 +358,10 @@ class Application(object):
"""
# NOTE(sigmavirus24): When updating this, make sure you also update
# our legacy API calls to these same methods.
self.parse_preliminary_options_and_args(argv)
flake8.configure_logging(
self.prelim_opts.verbose, self.prelim_opts.output_file)
self.make_config_finder()
self.find_plugins()
self.register_plugin_options()
self.parse_configuration_and_cli(argv)

View file

@ -5,17 +5,18 @@ applies the user-specified command-line configuration on top of it.
"""
import logging
from flake8 import utils
from flake8.options import config
LOG = logging.getLogger(__name__)
def aggregate_options(manager, arglist=None, values=None):
def aggregate_options(manager, config_finder, arglist=None, values=None):
"""Aggregate and merge CLI and config file options.
:param flake8.option.manager.OptionManager manager:
: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 arglist:
The list of arguments to pass to ``manager.parse_args``. In most cases
this will be None so ``parse_args`` uses ``sys.argv``. This is mostly
@ -32,14 +33,12 @@ def aggregate_options(manager, arglist=None, values=None):
default_values, _ = manager.parse_args([], values=values)
# Get original CLI values so we can find additional config file paths and
# see if --config was specified.
original_values, original_args = manager.parse_args(arglist)
extra_config_files = utils.normalize_paths(original_values.append_config)
original_values, _ = manager.parse_args(arglist)
# Make our new configuration file mergerator
config_parser = config.MergedConfigParser(
option_manager=manager,
extra_config_files=extra_config_files,
args=original_args,
config_finder=config_finder,
)
# Get the parsed config

View file

@ -1,4 +1,5 @@
"""Config handling logic for Flake8."""
import collections
import configparser
import logging
import os.path
@ -49,6 +50,11 @@ class ConfigFileFinder(object):
args = ['.']
self.parent = self.tail = os.path.abspath(os.path.commonprefix(args))
# caches to avoid double-reading config files
self._local_configs = None
self._user_config = None
self._cli_configs = {}
@staticmethod
def _read_config(files):
config = configparser.RawConfigParser()
@ -63,10 +69,12 @@ class ConfigFileFinder(object):
def cli_config(self, files):
"""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
if files not in self._cli_configs:
config, found_files = self._read_config(files)
if found_files:
LOG.debug('Found cli configuration files: %s', found_files)
self._cli_configs[files] = config
return self._cli_configs[files]
def generate_possible_local_files(self):
"""Find and generate all local config files."""
@ -104,10 +112,12 @@ class ConfigFileFinder(object):
def local_configs(self):
"""Parse all local config files into one config object."""
config, found_files = self._read_config(self.local_config_files())
if found_files:
LOG.debug('Found local configuration files: %s', found_files)
return config
if self._local_configs is None:
config, found_files = self._read_config(self.local_config_files())
if found_files:
LOG.debug('Found local configuration files: %s', found_files)
self._local_configs = config
return self._local_configs
def user_config_file(self):
"""Find the user-level config file."""
@ -117,10 +127,12 @@ class ConfigFileFinder(object):
def user_config(self):
"""Parse the user config file into a config object."""
config, found_files = self._read_config(self.user_config_file())
if found_files:
LOG.debug('Found user configuration files: %s', found_files)
return config
if self._user_config is None:
config, found_files = self._read_config(self.user_config_file())
if found_files:
LOG.debug('Found user configuration files: %s', found_files)
self._user_config = config
return self._user_config
class MergedConfigParser(object):
@ -138,30 +150,23 @@ class MergedConfigParser(object):
#: :meth:`~configparser.RawConfigParser.getbool` method.
GETBOOL_ACTIONS = {'store_true', 'store_false'}
def __init__(self, option_manager, extra_config_files=None, args=None):
def __init__(self, option_manager, config_finder):
"""Initialize the MergedConfigParser instance.
:param flake8.option.manager.OptionManager option_manager:
:param flake8.options.manager.OptionManager option_manager:
Initialized OptionManager.
:param list extra_config_files:
List of extra config files to parse.
:params list args:
The extra parsed arguments from the command-line.
: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
#: Parsed extra arguments
self.args = args
#: Mapping of configuration option names to
#: :class:`~flake8.options.manager.Option` instances
self.config_options = option_manager.config_options_dict
#: List of extra config files
self.extra_config_files = extra_config_files or []
#: Our instance of our :class:`~ConfigFileFinder`
self.config_finder = ConfigFileFinder(self.program_name, self.args,
self.extra_config_files)
self.config_finder = config_finder
def _normalize_value(self, option, value):
final_value = option.normalize(
@ -280,3 +285,47 @@ class MergedConfigParser(object):
return self.parse_cli_config(cli_config)
return self.merge_user_and_local_config()
def get_local_plugins(config_finder, cli_config=None, isolated=False):
"""Get local plugins lists from config files.
:param flake8.options.config.ConfigFileFinder config_finder:
The config file finder to use.
:param str cli_config:
Value of --config when specified at the command-line. Overrides
all other config files.
:param bool isolated:
Determines if we should parse configuration files at all or not.
If running in isolated mode, we ignore all configuration files
: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=[])
if isolated:
LOG.debug('Refusing to look for local plugins in configuration'
'files due to user-requested isolation')
return local_plugins
if cli_config:
LOG.debug('Reading local plugins only from "%s" specified via '
'--config by the user', cli_config)
config = config_finder.cli_config(cli_config)
else:
config = config_finder.local_configs()
section = '%s:local-plugins' % config_finder.program_name
for plugin_type in ['extension', 'report']:
if config.has_option(section, plugin_type):
getattr(local_plugins, plugin_type).extend(
c.strip() for c in config.get(
section, plugin_type
).strip().splitlines()
)
return local_plugins
LocalPlugins = collections.namedtuple('LocalPlugins', 'extension report')

View file

@ -24,7 +24,7 @@ NO_GROUP_FOUND = object()
class Plugin(object):
"""Wrap an EntryPoint from setuptools and other logic."""
def __init__(self, name, entry_point):
def __init__(self, name, entry_point, local=False):
"""Initialize our Plugin.
:param str name:
@ -33,9 +33,12 @@ class Plugin(object):
EntryPoint returned by setuptools.
:type entry_point:
setuptools.EntryPoint
:param bool local:
Is this a repo-local plugin?
"""
self.name = name
self.entry_point = entry_point
self.local = local
self._plugin = None
self._parameters = None
self._parameter_names = None
@ -112,6 +115,8 @@ class Plugin(object):
self._version = version_for(self)
else:
self._version = self.plugin.version
if self.local:
self._version += ' [local]'
return self._version
@ -236,11 +241,14 @@ class Plugin(object):
class PluginManager(object): # pylint: disable=too-few-public-methods
"""Find and manage plugins consistently."""
def __init__(self, namespace, verify_requirements=False):
def __init__(self, namespace,
verify_requirements=False, local_plugins=None):
"""Initialize the manager.
:param str namespace:
Namespace of the plugins to manage, e.g., 'flake8.extension'.
:param list local_plugins:
Plugins from config (as "X = path.to:Plugin" strings).
:param bool verify_requirements:
Whether or not to make setuptools verify that the requirements for
the plugin are satisfied.
@ -249,15 +257,36 @@ class PluginManager(object): # pylint: disable=too-few-public-methods
self.verify_requirements = verify_requirements
self.plugins = {}
self.names = []
self._load_all_plugins()
self._load_local_plugins(local_plugins or [])
self._load_entrypoint_plugins()
def _load_all_plugins(self):
def _load_local_plugins(self, local_plugins):
"""Load local plugins from config.
:param list local_plugins:
Plugins from config (as "X = path.to:Plugin" strings).
"""
for plugin_str in local_plugins:
entry_point = pkg_resources.EntryPoint.parse(plugin_str)
self._load_plugin_from_entrypoint(entry_point, local=True)
def _load_entrypoint_plugins(self):
LOG.info('Loading entry-points for "%s".', self.namespace)
for entry_point in pkg_resources.iter_entry_points(self.namespace):
name = entry_point.name
self.plugins[name] = Plugin(name, entry_point)
self.names.append(name)
LOG.debug('Loaded %r for plugin "%s".', self.plugins[name], name)
self._load_plugin_from_entrypoint(entry_point)
def _load_plugin_from_entrypoint(self, entry_point, local=False):
"""Load a plugin from a setuptools EntryPoint.
:param EntryPoint entry_point:
EntryPoint to load plugin from.
:param bool local:
Is this a repo-local plugin?
"""
name = entry_point.name
self.plugins[name] = Plugin(name, entry_point, local=local)
self.names.append(name)
LOG.debug('Loaded %r for plugin "%s".', self.plugins[name], name)
def map(self, func, *args, **kwargs):
r"""Call ``func`` with the plugin and \*args and \**kwargs after.
@ -329,9 +358,14 @@ class PluginTypeManager(object):
namespace = None
def __init__(self):
"""Initialize the plugin type's manager."""
self.manager = PluginManager(self.namespace)
def __init__(self, local_plugins=None):
"""Initialize the plugin type's manager.
:param list local_plugins:
Plugins from config file instead of entry-points
"""
self.manager = PluginManager(
self.namespace, local_plugins=local_plugins)
self.plugins_loaded = False
def __contains__(self, name):
@ -436,7 +470,7 @@ class NotifierBuilderMixin(object): # pylint: disable=too-few-public-methods
class Checkers(PluginTypeManager):
"""All of the checkers registered through entry-ponits."""
"""All of the checkers registered through entry-points or config."""
namespace = 'flake8.extension'
@ -515,12 +549,12 @@ class Checkers(PluginTypeManager):
class Listeners(PluginTypeManager, NotifierBuilderMixin):
"""All of the listeners registered through entry-points."""
"""All of the listeners registered through entry-points or config."""
namespace = 'flake8.listen'
class ReportFormatters(PluginTypeManager):
"""All of the report formatters registered through entry-points."""
"""All of the report formatters registered through entry-points/config."""
namespace = 'flake8.report'

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

@ -111,6 +111,17 @@ def test_version_proxies_to_the_plugin():
assert plugin.version == 'a.b.c'
def test_local_plugin_version():
"""Verify that local plugins have [local] appended to version."""
entry_point = mock.Mock(spec=['require', 'resolve', 'load'])
plugin_obj = mock.Mock(spec_set=['version'])
plugin_obj.version = 'a.b.c'
plugin = manager.Plugin('T000', entry_point, local=True)
plugin._plugin = plugin_obj
assert plugin.version == 'a.b.c [local]'
def test_register_options():
"""Verify we call add_options on the plugin only if it exists."""
# Set up our mocks and Plugin object

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