diff options
| author | Ian Stapleton Cordasco <graffatcolmingov@gmail.com> | 2017-08-07 11:19:30 +0000 |
|---|---|---|
| committer | Ian Stapleton Cordasco <graffatcolmingov@gmail.com> | 2017-08-07 11:19:30 +0000 |
| commit | 3169b6072b6bee0ef17f94400f68dd2001186e60 (patch) | |
| tree | 5c736b7a706b97ba383e2817cd8dc982f60f24c7 | |
| parent | ff3a7813d6288e377b9ad48fe85026d0e72db8ec (diff) | |
| parent | 18c0b14b5ca84b0cf9f64aa211494dba1e5c3c51 (diff) | |
| download | flake8-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.py | 5 | ||||
| -rw-r--r-- | src/flake8/main/application.py | 112 | ||||
| -rw-r--r-- | src/flake8/options/aggregator.py | 13 | ||||
| -rw-r--r-- | src/flake8/options/config.py | 97 | ||||
| -rw-r--r-- | src/flake8/plugins/manager.py | 62 | ||||
| -rw-r--r-- | tests/fixtures/config_files/README.rst | 10 | ||||
| -rw-r--r-- | tests/fixtures/config_files/local-plugin.ini | 5 | ||||
| -rw-r--r-- | tests/integration/test_aggregator.py | 9 | ||||
| -rw-r--r-- | tests/integration/test_plugins.py | 58 | ||||
| -rw-r--r-- | tests/unit/test_config_file_finder.py | 24 | ||||
| -rw-r--r-- | tests/unit/test_get_local_plugins.py | 38 | ||||
| -rw-r--r-- | tests/unit/test_legacy_api.py | 4 | ||||
| -rw-r--r-- | tests/unit/test_merged_config_parser.py | 81 | ||||
| -rw-r--r-- | tests/unit/test_plugin.py | 11 | ||||
| -rw-r--r-- | tests/unit/test_plugin_manager.py | 12 | ||||
| -rw-r--r-- | tests/unit/test_plugin_type_manager.py | 2 |
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') |
