Merge branch 'local-plugins-updates' into 'master'

Local plugins updates

See merge request !207
This commit is contained in:
Ian Stapleton Cordasco 2017-08-12 13:33:22 +00:00
commit 2e12a2024b
11 changed files with 144 additions and 55 deletions

View file

@ -3,13 +3,23 @@
You can view the `3.5.0 milestone`_ on GitLab for more details.
New Dependency Information
~~~~~~~~~~~~~~~~~~~~~~~~~~
- Allow for PyFlakes 1.6.0 (See also `GitLab#359`_)
- Start using new PyCodestyle checks for bare excepts and ambiguous identifier
(See also `GitLab#361`_)
Features
~~~~~~~~
- Print out information about configuring VCS hooks (See also `GitLab#335`_)
- Allow users to develop plugins "local" to a repository without using
setuptools. See our documentation on local plugins for more information.
(See also `GitLab#357`_)
.. all links
.. _3.5.0 milestone:
@ -18,6 +28,8 @@ You can view the `3.5.0 milestone`_ on GitLab for more details.
.. issue links
.. _GitLab#335:
https://gitlab.com/pycqa/flake8/issues/335
.. _GitLab#357:
https://gitlab.com/pycqa/flake8/issues/357
.. _GitLab#359:
https://gitlab.com/pycqa/flake8/issues/359
.. _GitLab#361:

View file

@ -222,3 +222,51 @@ They use the comments to describe the check but they could also write this as:
Or they could use each comment to describe **why** they've ignored the check.
|Flake8| knows how to parse these lists and will appropriately handle
these situations.
Using Local Plugins
-------------------
.. versionadded:: 3.5.0
|Flake8| allows users to write plugins that live locally in a project. These
plugins do not need to use setuptools or any of the other overhead associated
with plugins distributed on PyPI. To use these plugins, users must specify
them in their configuration file (i.e., ``.flake8``, ``setup.cfg``, or
``tox.ini``). This must be configured in a separate INI section named
``flake8:local-plugins``.
Users may configure plugins that check source code, i.e., ``extension``
plugins, and plugins that report errors, i.e., ``report`` plugins.
An example configuration might look like:
.. code-block:: ini
[flake8:local-plugins]
extension =
MC1 = project.flake8.checkers:MyChecker1
MC2 = project.flake8.checkers:MyChecker2
report =
MR1 = project.flake8.reporters:MyReporter1
MR2 = project.flake8.reporters:MyReporter2
|Flake8| will also, however, allow for commas to separate the plugins for
example:
.. code-block:: ini
[flake8:local-plugins]
extension =
MC1 = project.flake8.checkers:MyChecker1,
MC2 = project.flake8.checkers:MyChecker2
report =
MR1 = project.flake8.reporters:MyReporter1,
MR2 = project.flake8.reporters:MyReporter2
These configurations will allow you to select your own custom reporter plugin
that you've designed or will utilize your new check classes.
.. note::
These plugins otherwise follow the same guidelines as regular plugins.

View file

@ -53,8 +53,14 @@ def information(option_manager):
def plugins_from(option_manager):
"""Generate the list of plugins installed."""
return [{'plugin': plugin, 'version': version}
for (plugin, version) in sorted(option_manager.registered_plugins)]
return [
{
'plugin': plugin.name,
'version': plugin.version,
'is_local': plugin.local,
}
for plugin in sorted(option_manager.registered_plugins)
]
def dependencies():

View file

@ -5,6 +5,8 @@ import logging
import os.path
import sys
from flake8 import utils
LOG = logging.getLogger(__name__)
__all__ = ('ConfigFileFinder', 'MergedConfigParser')
@ -320,11 +322,12 @@ def get_local_plugins(config_finder, cli_config=None, isolated=False):
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()
)
local_plugins_string = config.get(section, plugin_type).strip()
plugin_type_list = getattr(local_plugins, plugin_type)
plugin_type_list.extend(utils.parse_comma_separated_list(
local_plugins_string,
regexp=utils.LOCAL_PLUGIN_LIST_RE,
))
return local_plugins

View file

@ -1,4 +1,5 @@
"""Option handling and Option management logic."""
import collections
import logging
import optparse # pylint: disable=deprecated-module
@ -153,6 +154,10 @@ class Option(object):
return self._opt
PluginVersion = collections.namedtuple("PluginVersion",
["name", "version", "local"])
class OptionManager(object):
"""Manage Options and OptionParser while adding post-processing."""
@ -178,9 +183,9 @@ class OptionManager(object):
self.extended_default_select = set()
@staticmethod
def format_plugin(plugin_tuple):
"""Convert a plugin tuple into a dictionary mapping name to value."""
return dict(zip(["name", "version"], plugin_tuple))
def format_plugin(plugin):
"""Convert a PluginVersion into a dictionary mapping name to value."""
return {attr: getattr(plugin, attr) for attr in ["name", "version"]}
def add_option(self, *args, **kwargs):
"""Create and register a new option.
@ -306,7 +311,7 @@ class OptionManager(object):
self._normalize(options)
return options, xargs
def register_plugin(self, name, version):
def register_plugin(self, name, version, local=False):
"""Register a plugin relying on the OptionManager.
:param str name:
@ -314,5 +319,7 @@ class OptionManager(object):
attribute of the class or function loaded from the entry-point.
:param str version:
The version of the checker that we're using.
:param bool local:
Whether the plugin is local to the project/repository or not.
"""
self.registered_plugins.add((name, version))
self.registered_plugins.add(PluginVersion(name, version, local))

View file

@ -115,8 +115,6 @@ class Plugin(object):
self._version = version_for(self)
else:
self._version = self.plugin.version
if self.local:
self._version += ' [local]'
return self._version

View file

@ -11,14 +11,20 @@ import tokenize
DIFF_HUNK_REGEXP = re.compile(r'^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@.*$')
COMMA_SEPARATED_LIST_RE = re.compile(r'[,\s]')
LOCAL_PLUGIN_LIST_RE = re.compile(r'[,\t\n\r\f\v]')
def parse_comma_separated_list(value):
def parse_comma_separated_list(value, regexp=COMMA_SEPARATED_LIST_RE):
# type: (Union[Sequence[str], str]) -> List[str]
"""Parse a comma-separated list.
:param value:
String or list of strings to be parsed and normalized.
:param regexp:
Compiled regular expression used to split the value when it is a
string.
:type regexp:
_sre.SRE_Pattern
:returns:
List of values with whitespace stripped.
:rtype:
@ -28,7 +34,7 @@ def parse_comma_separated_list(value):
return []
if not isinstance(value, (list, tuple)):
value = COMMA_SEPARATED_LIST_RE.split(value)
value = regexp.split(value)
item_gen = (item.strip() for item in value)
return [item for item in item_gen if item]

View file

@ -4,6 +4,7 @@ import pytest
import setuptools
from flake8.main import debug
from flake8.options import manager
def test_dependencies():
@ -15,11 +16,18 @@ def test_dependencies():
@pytest.mark.parametrize('plugins, expected', [
([], []),
([('pycodestyle', '2.0.0')], [{'plugin': 'pycodestyle',
'version': '2.0.0'}]),
([('pycodestyle', '2.0.0'), ('mccabe', '0.5.9')],
[{'plugin': 'mccabe', 'version': '0.5.9'},
{'plugin': 'pycodestyle', 'version': '2.0.0'}]),
([manager.PluginVersion('pycodestyle', '2.0.0', False)],
[{'plugin': 'pycodestyle', 'version': '2.0.0', 'is_local': False}]),
([manager.PluginVersion('pycodestyle', '2.0.0', False),
manager.PluginVersion('mccabe', '0.5.9', False)],
[{'plugin': 'mccabe', 'version': '0.5.9', 'is_local': False},
{'plugin': 'pycodestyle', 'version': '2.0.0', 'is_local': False}]),
([manager.PluginVersion('pycodestyle', '2.0.0', False),
manager.PluginVersion('my-local', '0.0.1', True),
manager.PluginVersion('mccabe', '0.5.9', False)],
[{'plugin': 'mccabe', 'version': '0.5.9', 'is_local': False},
{'plugin': 'my-local', 'version': '0.0.1', 'is_local': True},
{'plugin': 'pycodestyle', 'version': '2.0.0', 'is_local': False}]),
])
def test_plugins_from(plugins, expected):
"""Test that we format plugins appropriately."""
@ -34,8 +42,10 @@ def test_information(system, pyversion, pyimpl):
"""Verify that we return all the information we care about."""
expected = {
'version': '3.1.0',
'plugins': [{'plugin': 'mccabe', 'version': '0.5.9'},
{'plugin': 'pycodestyle', 'version': '2.0.0'}],
'plugins': [{'plugin': 'mccabe', 'version': '0.5.9',
'is_local': False},
{'plugin': 'pycodestyle', 'version': '2.0.0',
'is_local': False}],
'dependencies': [{'dependency': 'setuptools',
'version': setuptools.__version__}],
'platform': {
@ -45,8 +55,10 @@ def test_information(system, pyversion, pyimpl):
},
}
option_manager = mock.Mock(
registered_plugins={('pycodestyle', '2.0.0'),
('mccabe', '0.5.9')},
registered_plugins={
manager.PluginVersion('pycodestyle', '2.0.0', False),
manager.PluginVersion('mccabe', '0.5.9', False),
},
version='3.1.0',
)
assert expected == debug.information(option_manager)
@ -75,7 +87,10 @@ def test_print_information_no_plugins(dumps, information, print_mock):
@mock.patch('json.dumps', return_value='{}')
def test_print_information(dumps, information, print_mock):
"""Verify we print and exit only when we have plugins."""
plugins = [('pycodestyle', '2.0.0'), ('mccabe', '0.5.9')]
plugins = [
manager.PluginVersion('pycodestyle', '2.0.0', False),
manager.PluginVersion('mccabe', '0.5.9', False),
]
option_manager = mock.Mock(registered_plugins=set(plugins))
with pytest.raises(SystemExit):
debug.print_information(

View file

@ -18,7 +18,10 @@ def test_get_local_plugins_respects_isolated():
def test_get_local_plugins_uses_cli_config():
"""Verify behaviour of get_local_plugins with a specified config."""
config_obj = mock.Mock()
config_finder = mock.MagicMock()
config_finder.cli_config.return_value = config_obj
config_obj.get.return_value = ''
config.get_local_plugins(config_finder, cli_config='foo.ini')

View file

@ -121,7 +121,9 @@ def test_parse_args_normalize_paths(optmanager):
def test_format_plugin():
"""Verify that format_plugin turns a tuple into a dictionary."""
plugin = manager.OptionManager.format_plugin(('Testing', '0.0.0'))
plugin = manager.OptionManager.format_plugin(
manager.PluginVersion('Testing', '0.0.0', False)
)
assert plugin['name'] == 'Testing'
assert plugin['version'] == '0.0.0'
@ -129,9 +131,9 @@ def test_format_plugin():
def test_generate_versions(optmanager):
"""Verify a comma-separated string is generated of registered plugins."""
optmanager.registered_plugins = [
('Testing 100', '0.0.0'),
('Testing 101', '0.0.0'),
('Testing 300', '0.0.0'),
manager.PluginVersion('Testing 100', '0.0.0', False),
manager.PluginVersion('Testing 101', '0.0.0', False),
manager.PluginVersion('Testing 300', '0.0.0', True),
]
assert (optmanager.generate_versions() ==
'Testing 100: 0.0.0, Testing 101: 0.0.0, Testing 300: 0.0.0')
@ -140,11 +142,11 @@ def test_generate_versions(optmanager):
def test_plugins_are_sorted_in_generate_versions(optmanager):
"""Verify we sort before joining strings in generate_versions."""
optmanager.registered_plugins = [
('pyflakes', '1.5.0'),
('mccabe', '0.7.0'),
('pycodestyle', '2.2.0'),
('flake8-docstrings', '0.6.1'),
('flake8-bugbear', '2016.12.1'),
manager.PluginVersion('pyflakes', '1.5.0', False),
manager.PluginVersion('mccabe', '0.7.0', False),
manager.PluginVersion('pycodestyle', '2.2.0', False),
manager.PluginVersion('flake8-docstrings', '0.6.1', False),
manager.PluginVersion('flake8-bugbear', '2016.12.1', False),
]
assert (optmanager.generate_versions() ==
'flake8-bugbear: 2016.12.1, '
@ -157,9 +159,9 @@ def test_plugins_are_sorted_in_generate_versions(optmanager):
def test_generate_versions_with_format_string(optmanager):
"""Verify a comma-separated string is generated of registered plugins."""
optmanager.registered_plugins.update([
('Testing', '0.0.0'),
('Testing', '0.0.0'),
('Testing', '0.0.0'),
manager.PluginVersion('Testing', '0.0.0', False),
manager.PluginVersion('Testing', '0.0.0', False),
manager.PluginVersion('Testing', '0.0.0', False),
])
assert (
optmanager.generate_versions() == 'Testing: 0.0.0'
@ -172,9 +174,9 @@ def test_update_version_string(optmanager):
assert optmanager.parser.version == TEST_VERSION
optmanager.registered_plugins = [
('Testing 100', '0.0.0'),
('Testing 101', '0.0.0'),
('Testing 300', '0.0.0'),
manager.PluginVersion('Testing 100', '0.0.0', False),
manager.PluginVersion('Testing 101', '0.0.0', False),
manager.PluginVersion('Testing 300', '0.0.0', False),
]
optmanager.update_version_string()
@ -190,9 +192,9 @@ def test_generate_epilog(optmanager):
assert optmanager.parser.epilog is None
optmanager.registered_plugins = [
('Testing 100', '0.0.0'),
('Testing 101', '0.0.0'),
('Testing 300', '0.0.0'),
manager.PluginVersion('Testing 100', '0.0.0', False),
manager.PluginVersion('Testing 101', '0.0.0', False),
manager.PluginVersion('Testing 300', '0.0.0', False),
]
expected_value = (

View file

@ -111,17 +111,6 @@ 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