From 423980164b258b2ec77d8d9bfccb9bc00b220e31 Mon Sep 17 00:00:00 2001 From: Carl Meyer Date: Mon, 23 Oct 2017 16:49:09 -0700 Subject: [PATCH] Add paths option in local-plugins config file. --- docs/source/user/configuration.rst | 21 +++++++++++ src/flake8/main/application.py | 2 + src/flake8/options/config.py | 37 ++++++++++++++++--- .../config_files/local-plugin-path.ini | 5 +++ tests/integration/subdir/aplugin.py | 16 ++++++++ tests/integration/test_plugins.py | 9 +++++ 6 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 tests/fixtures/config_files/local-plugin-path.ini create mode 100644 tests/integration/subdir/aplugin.py diff --git a/docs/source/user/configuration.rst b/docs/source/user/configuration.rst index eacacef..b036eff 100644 --- a/docs/source/user/configuration.rst +++ b/docs/source/user/configuration.rst @@ -267,6 +267,27 @@ example: These configurations will allow you to select your own custom reporter plugin that you've designed or will utilize your new check classes. +If your package is installed in the same virtualenv that |Flake8| will run +from, and your local plugins are part of that package, you're all set; |Flake8| +will be able to import your local plugins. However, if you are working on a +project that isn't set up as an installable package, or |Flake8| doesn't run +from the same virtualenv your code runs in, you may need to tell |Flake8| where +to import your local plugins from. You can do this via the ``paths`` option in +the ``local-plugins`` section of your config: + +.. code-block:: ini + + [flake8:local-plugins] + extension = + MC1 = myflake8plugin:MyChecker1 + paths = + ./path/to + +Relative paths will be interpreted relative to the config file. Multiple paths +can be listed, one per line (or comma separated) as needed. If your local +plugins have any dependencies, it's up to you to ensure they are installed in +whatever Python environment |Flake8| runs in. + .. note:: These plugins otherwise follow the same guidelines as regular plugins. diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 6c68305..233b9af 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -177,6 +177,8 @@ class Application(object): self.prelim_opts.isolated, ) + sys.path.extend(self.local_plugins.paths) + if self.check_plugins is None: self.check_plugins = plugin_manager.Checkers( self.local_plugins.extension) diff --git a/src/flake8/options/config.py b/src/flake8/options/config.py index 71429af..b5b42fb 100644 --- a/src/flake8/options/config.py +++ b/src/flake8/options/config.py @@ -54,6 +54,7 @@ class ConfigFileFinder(object): # caches to avoid double-reading config files self._local_configs = None + self._local_found_files = [] self._user_config = None self._cli_configs = {} @@ -120,14 +121,22 @@ class ConfigFileFinder(object): for filename in self.generate_possible_local_files() ] + [f for f in self.extra_config_files if exists(f)] - def local_configs(self): - """Parse all local config files into one config object.""" + def local_configs_with_files(self): + """Parse all local config files into one config object. + + Return (config, found_config_files) tuple. + """ 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 + self._local_found_files = found_files + return (self._local_configs, self._local_found_files) + + def local_configs(self): + """Parse all local config files into one config object.""" + return self.local_configs_with_files()[0] def user_config_file(self): """Find the user-level config file.""" @@ -314,7 +323,7 @@ def get_local_plugins(config_finder, cli_config=None, isolated=False): :rtype: flake8.options.config.LocalPlugins """ - local_plugins = LocalPlugins(extension=[], report=[]) + local_plugins = LocalPlugins(extension=[], report=[], paths=[]) if isolated: LOG.debug('Refusing to look for local plugins in configuration' 'files due to user-requested isolation') @@ -324,8 +333,11 @@ def get_local_plugins(config_finder, cli_config=None, isolated=False): LOG.debug('Reading local plugins only from "%s" specified via ' '--config by the user', cli_config) config = config_finder.cli_config(cli_config) + config_files = [cli_config] else: - config = config_finder.local_configs() + config, config_files = config_finder.local_configs_with_files() + + base_dirs = {os.path.dirname(cf) for cf in config_files} section = '%s:local-plugins' % config_finder.program_name for plugin_type in ['extension', 'report']: @@ -336,7 +348,20 @@ def get_local_plugins(config_finder, cli_config=None, isolated=False): local_plugins_string, regexp=utils.LOCAL_PLUGIN_LIST_RE, )) + if config.has_option(section, 'paths'): + raw_paths = utils.parse_comma_separated_list( + config.get(section, 'paths').strip(), + regexp=utils.LOCAL_PLUGIN_LIST_RE, + ) + norm_paths = [] + for base_dir in base_dirs: + norm_paths.extend( + path for path in + utils.normalize_paths(raw_paths, parent=base_dir) + if os.path.exists(path) + ) + local_plugins.paths.extend(norm_paths) return local_plugins -LocalPlugins = collections.namedtuple('LocalPlugins', 'extension report') +LocalPlugins = collections.namedtuple('LocalPlugins', 'extension report paths') diff --git a/tests/fixtures/config_files/local-plugin-path.ini b/tests/fixtures/config_files/local-plugin-path.ini new file mode 100644 index 0000000..7368c1e --- /dev/null +++ b/tests/fixtures/config_files/local-plugin-path.ini @@ -0,0 +1,5 @@ +[flake8:local-plugins] +extension = + XE = aplugin:ExtensionTestPlugin2 +paths = + ../../integration/subdir/ diff --git a/tests/integration/subdir/aplugin.py b/tests/integration/subdir/aplugin.py new file mode 100644 index 0000000..98a0464 --- /dev/null +++ b/tests/integration/subdir/aplugin.py @@ -0,0 +1,16 @@ +"""Module that is off sys.path by default, for testing local-plugin-paths.""" + + +class ExtensionTestPlugin2(object): + """Extension test plugin in its own directory.""" + + name = 'ExtensionTestPlugin2' + version = '1.0.0' + + def __init__(self, tree): + """Construct an instance of test plugin.""" + pass + + def run(self): + """Do nothing.""" + pass diff --git a/tests/integration/test_plugins.py b/tests/integration/test_plugins.py index 6d51a4a..e59eb91 100644 --- a/tests/integration/test_plugins.py +++ b/tests/integration/test_plugins.py @@ -3,6 +3,7 @@ from flake8.main import application LOCAL_PLUGIN_CONFIG = 'tests/fixtures/config_files/local-plugin.ini' +LOCAL_PLUGIN_PATH_CONFIG = 'tests/fixtures/config_files/local-plugin-path.ini' class ExtensionTestPlugin(object): @@ -56,3 +57,11 @@ def test_local_plugin_can_add_option(): ['flake8', '--config', LOCAL_PLUGIN_CONFIG, '--anopt', 'foo']) assert app.options.anopt == 'foo' + + +def test_enable_local_plugin_at_non_installed_path(): + """Can add a paths option in local-plugins config section for finding.""" + app = application.Application() + app.initialize(['flake8', '--config', LOCAL_PLUGIN_PATH_CONFIG]) + + assert app.check_plugins['XE'].plugin.name == 'ExtensionTestPlugin2'