summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIan Stapleton Cordasco <graffatcolmingov@gmail.com>2017-08-07 11:19:30 +0000
committerIan Stapleton Cordasco <graffatcolmingov@gmail.com>2017-08-07 11:19:30 +0000
commit3169b6072b6bee0ef17f94400f68dd2001186e60 (patch)
tree5c736b7a706b97ba383e2817cd8dc982f60f24c7
parentff3a7813d6288e377b9ad48fe85026d0e72db8ec (diff)
parent18c0b14b5ca84b0cf9f64aa211494dba1e5c3c51 (diff)
downloadflake8-3169b6072b6bee0ef17f94400f68dd2001186e60.tar.gz
Merge branch 'local-plugins' into 'master'
Add support for local (in-repo, non-setuptools) plugins. Closes #357 See merge request !197
-rw-r--r--src/flake8/api/legacy.py5
-rw-r--r--src/flake8/main/application.py112
-rw-r--r--src/flake8/options/aggregator.py13
-rw-r--r--src/flake8/options/config.py97
-rw-r--r--src/flake8/plugins/manager.py62
-rw-r--r--tests/fixtures/config_files/README.rst10
-rw-r--r--tests/fixtures/config_files/local-plugin.ini5
-rw-r--r--tests/integration/test_aggregator.py9
-rw-r--r--tests/integration/test_plugins.py58
-rw-r--r--tests/unit/test_config_file_finder.py24
-rw-r--r--tests/unit/test_get_local_plugins.py38
-rw-r--r--tests/unit/test_legacy_api.py4
-rw-r--r--tests/unit/test_merged_config_parser.py81
-rw-r--r--tests/unit/test_plugin.py11
-rw-r--r--tests/unit/test_plugin_manager.py12
-rw-r--r--tests/unit/test_plugin_type_manager.py2
16 files changed, 409 insertions, 134 deletions
diff --git a/src/flake8/api/legacy.py b/src/flake8/api/legacy.py
index 2b983c8..b332860 100644
--- a/src/flake8/api/legacy.py
+++ b/src/flake8/api/legacy.py
@@ -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([])
diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py
index 293ac92..dc65f90 100644
--- a/src/flake8/main/application.py
+++ b/src/flake8/main/application.py
@@ -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)
-
- # 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 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
+
+ #: 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)
diff --git a/src/flake8/options/aggregator.py b/src/flake8/options/aggregator.py
index 4075dc9..5b8ab9c 100644
--- a/src/flake8/options/aggregator.py
+++ b/src/flake8/options/aggregator.py
@@ -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
diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py
index a6ac63f..49910ba 100644
--- a/src/flake8/options/config.py
+++ b/src/flake8/options/config.py
@@ -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')
diff --git a/src/flake8/plugins/manager.py b/src/flake8/plugins/manager.py
index 80a9ef2..699b1aa 100644
--- a/src/flake8/plugins/manager.py
+++ b/src/flake8/plugins/manager.py
@@ -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'
diff --git a/tests/fixtures/config_files/README.rst b/tests/fixtures/config_files/README.rst
index b00adad..f6994c9 100644
--- a/tests/fixtures/config_files/README.rst
+++ b/tests/fixtures/config_files/README.rst
@@ -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``
diff --git a/tests/fixtures/config_files/local-plugin.ini b/tests/fixtures/config_files/local-plugin.ini
new file mode 100644
index 0000000..d0aa3be
--- /dev/null
+++ b/tests/fixtures/config_files/local-plugin.ini
@@ -0,0 +1,5 @@
+[flake8:local-plugins]
+extension =
+ XE = test_plugins:ExtensionTestPlugin
+report =
+ XR = test_plugins:ReportTestPlugin
diff --git a/tests/integration/test_aggregator.py b/tests/integration/test_aggregator.py
index 929bdbf..c789624 100644
--- a/tests/integration/test_aggregator.py
+++ b/tests/integration/test_aggregator.py
@@ -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']
diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py
new file mode 100644
index 0000000..6d51a4a
--- /dev/null
+++ b/tests/integration/test_plugins.py
@@ -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'
diff --git a/tests/unit/test_config_file_finder.py b/tests/unit/test_config_file_finder.py
index 3f321da..2a9f903 100644
--- a/tests/unit/test_config_file_finder.py
+++ b/tests/unit/test_config_file_finder.py
@@ -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],
diff --git a/tests/unit/test_get_local_plugins.py b/tests/unit/test_get_local_plugins.py
new file mode 100644
index 0000000..942599a
--- /dev/null
+++ b/tests/unit/test_get_local_plugins.py
@@ -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']
diff --git a/tests/unit/test_legacy_api.py b/tests/unit/test_legacy_api.py
index 0f1d1f3..f456bae 100644
--- a/tests/unit/test_legacy_api.py
+++ b/tests/unit/test_legacy_api.py
@@ -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([])
diff --git a/tests/unit/test_merged_config_parser.py b/tests/unit/test_merged_config_parser.py
index 1bc2bbe..37d5e6f 100644
--- a/tests/unit/test_merged_config_parser.py
+++ b/tests/unit/test_merged_config_parser.py
@@ -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]
diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py
index 84f676a..d6bc27f 100644
--- a/tests/unit/test_plugin.py
+++ b/tests/unit/test_plugin.py
@@ -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
diff --git a/tests/unit/test_plugin_manager.py b/tests/unit/test_plugin_manager.py
index 8991b96..9019ead 100644
--- a/tests/unit/test_plugin_manager.py
+++ b/tests/unit/test_plugin_manager.py
@@ -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'
diff --git a/tests/unit/test_plugin_type_manager.py b/tests/unit/test_plugin_type_manager.py
index 4e69cee..b81a1ae 100644
--- a/tests/unit/test_plugin_type_manager.py
+++ b/tests/unit/test_plugin_type_manager.py
@@ -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')