From d3c95f00d0ac14adcceb423173ff7106aa42d116 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 28 Nov 2019 17:30:52 -0800 Subject: [PATCH] Switch from entrypoints to importlib_metadata --- setup.cfg | 2 +- src/flake8/_compat.py | 14 +++++++ src/flake8/exceptions.py | 15 +++---- src/flake8/main/application.py | 4 +- src/flake8/main/debug.py | 7 ++-- src/flake8/plugins/manager.py | 14 ++++--- src/flake8/style_guide.py | 8 +--- tests/integration/test_checker.py | 7 +++- tests/unit/test_debug.py | 8 +--- tests/unit/test_exceptions.py | 8 +--- tests/unit/test_plugin_manager.py | 58 ++++++++++++-------------- tests/unit/test_plugin_type_manager.py | 2 +- 12 files changed, 71 insertions(+), 76 deletions(-) create mode 100644 src/flake8/_compat.py diff --git a/setup.cfg b/setup.cfg index a83dce8..f482ec0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -41,7 +41,6 @@ install_requires= # http://flake8.pycqa.org/en/latest/faq.html#why-does-flake8-use-ranges-for-its-dependencies # And in which releases we will update those ranges here: # http://flake8.pycqa.org/en/latest/internal/releases.html#releasing-flake8 - entrypoints >= 0.3.0, < 0.4.0 pyflakes >= 2.1.0, < 2.2.0 pycodestyle >= 2.5.0, < 2.6.0 mccabe >= 0.6.0, < 0.7.0 @@ -49,6 +48,7 @@ install_requires= typing; python_version<"3.5" configparser; python_version<"3.2" functools32; python_version<"3.2" + importlib-metadata; python_version<"3.8" python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* diff --git a/src/flake8/_compat.py b/src/flake8/_compat.py new file mode 100644 index 0000000..85af0a3 --- /dev/null +++ b/src/flake8/_compat.py @@ -0,0 +1,14 @@ +"""Expose backports in a single place.""" +import sys + +if sys.version_info >= (3,): # pragma: no cover (PY3+) + from functools import lru_cache +else: # pragma: no cover (= (3, 8): # pragma: no cover (PY38+) + import importlib.metadata as importlib_metadata +else: # pragma: no cover ( None + def __init__(self, plugin_name, exception): + # type: (str, Exception) -> None """Initialize our FailedToLoadPlugin exception.""" - self.plugin = plugin - self.ep_name = self.plugin.name + self.plugin_name = plugin_name self.original_exception = exception - super(FailedToLoadPlugin, self).__init__(plugin, exception) + super(FailedToLoadPlugin, self).__init__(plugin_name, exception) def __str__(self): # type: () -> str """Format our exception message.""" return self.FORMAT % { - "name": self.ep_name, + "name": self.plugin_name, "exc": self.original_exception, } diff --git a/src/flake8/main/application.py b/src/flake8/main/application.py index 42e365f..a20c1d4 100644 --- a/src/flake8/main/application.py +++ b/src/flake8/main/application.py @@ -168,9 +168,7 @@ class Application(object): sys.path.extend(local_plugins.paths) - self.check_plugins = plugin_manager.Checkers( - local_plugins.extension - ) + self.check_plugins = plugin_manager.Checkers(local_plugins.extension) self.formatting_plugins = plugin_manager.ReportFormatters( local_plugins.report diff --git a/src/flake8/main/debug.py b/src/flake8/main/debug.py index 1d56189..83a5a11 100644 --- a/src/flake8/main/debug.py +++ b/src/flake8/main/debug.py @@ -4,8 +4,7 @@ from __future__ import print_function import argparse import json import platform - -import entrypoints +from typing import Dict, List class DebugAction(argparse.Action): @@ -61,6 +60,6 @@ def plugins_from(option_manager): ] -def dependencies(): +def dependencies(): # type: () -> List[Dict[str, str]] """Generate the list of dependencies we care about.""" - return [{"dependency": "entrypoints", "version": entrypoints.__version__}] + return [] diff --git a/src/flake8/plugins/manager.py b/src/flake8/plugins/manager.py index 75cc1ab..903a130 100644 --- a/src/flake8/plugins/manager.py +++ b/src/flake8/plugins/manager.py @@ -2,10 +2,9 @@ import logging from typing import Any, Dict, List, Optional, Set -import entrypoints - from flake8 import exceptions from flake8 import utils +from flake8._compat import importlib_metadata LOG = logging.getLogger(__name__) @@ -159,7 +158,7 @@ class Plugin(object): except Exception as load_exception: LOG.exception(load_exception) failed_to_load = exceptions.FailedToLoadPlugin( - plugin=self, exception=load_exception + plugin_name=self.name, exception=load_exception ) LOG.critical(str(failed_to_load)) raise failed_to_load @@ -224,6 +223,7 @@ class PluginManager(object): # pylint: disable=too-few-public-methods """Find and manage plugins consistently.""" def __init__(self, namespace, local_plugins=None): + # type: (str, Optional[List[str]]) -> None """Initialize the manager. :param str namespace: @@ -246,12 +246,16 @@ class PluginManager(object): # pylint: disable=too-few-public-methods for plugin_str in local_plugins: name, _, entry_str = plugin_str.partition("=") name, entry_str = name.strip(), entry_str.strip() - entry_point = entrypoints.EntryPoint.from_string(entry_str, name) + entry_point = importlib_metadata.EntryPoint(name, entry_str, None) 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 entrypoints.get_group_all(self.namespace): + eps = importlib_metadata.entry_points().get(self.namespace, ()) + # python2.7 occasionally gives duplicate results due to redundant + # `local/lib` -> `../lib` symlink on linux in virtualenvs so we + # eliminate duplicates here + for entry_point in sorted(frozenset(eps)): if entry_point.name == "per-file-ignores": LOG.warning( "flake8-per-file-ignores plugin is incompatible with " diff --git a/src/flake8/style_guide.py b/src/flake8/style_guide.py index ef9c6e9..9be0fe8 100644 --- a/src/flake8/style_guide.py +++ b/src/flake8/style_guide.py @@ -7,13 +7,13 @@ import enum import itertools import linecache import logging -import sys from typing import Dict, Generator, List, Match, Optional, Sequence, Set from typing import Tuple, Union from flake8 import defaults from flake8 import statistics from flake8 import utils +from flake8._compat import lru_cache from flake8.formatting import base as base_formatter __all__ = ("StyleGuide",) @@ -21,12 +21,6 @@ __all__ = ("StyleGuide",) LOG = logging.getLogger(__name__) -if sys.version_info < (3, 2): - from functools32 import lru_cache -else: - from functools import lru_cache - - class Selected(enum.Enum): """Enum representing an explicitly or implicitly selected code.""" diff --git a/tests/integration/test_checker.py b/tests/integration/test_checker.py index ca3ed4f..0c14474 100644 --- a/tests/integration/test_checker.py +++ b/tests/integration/test_checker.py @@ -3,6 +3,7 @@ import mock import pytest from flake8 import checker +from flake8._compat import importlib_metadata from flake8.plugins import manager PHYSICAL_LINE = "# Physical line content" @@ -100,7 +101,11 @@ def mock_file_checker_with_plugin(plugin_target): entry_point.load.return_value = plugin_target # Load the checker plugins using the entry point mock - with mock.patch('entrypoints.get_group_all', return_value=[entry_point]): + with mock.patch.object( + importlib_metadata, + 'entry_points', + return_value={'flake8.extension': [entry_point]}, + ): checks = manager.Checkers() # Prevent it from reading lines from stdin or somewhere else diff --git a/tests/unit/test_debug.py b/tests/unit/test_debug.py index c973abb..6398cf9 100644 --- a/tests/unit/test_debug.py +++ b/tests/unit/test_debug.py @@ -1,5 +1,4 @@ """Tests for our debugging module.""" -import entrypoints import mock import pytest @@ -9,9 +8,7 @@ from flake8.options import manager def test_dependencies(): """Verify that we format our dependencies appropriately.""" - expected = [{'dependency': 'entrypoints', - 'version': entrypoints.__version__}] - assert expected == debug.dependencies() + assert [] == debug.dependencies() @pytest.mark.parametrize('plugins, expected', [ @@ -46,8 +43,7 @@ def test_information(system, pyversion, pyimpl): 'is_local': False}, {'plugin': 'pycodestyle', 'version': '2.0.0', 'is_local': False}], - 'dependencies': [{'dependency': 'entrypoints', - 'version': entrypoints.__version__}], + 'dependencies': [], 'platform': { 'python_implementation': 'CPython', 'python_version': '3.5.3', diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py index 4766b78..0254cb2 100644 --- a/tests/unit/test_exceptions.py +++ b/tests/unit/test_exceptions.py @@ -1,10 +1,7 @@ """Tests for the flake8.exceptions module.""" import pickle -import entrypoints - from flake8 import exceptions -from flake8.plugins import manager as plugins_manager class _ExceptionTest: @@ -22,10 +19,7 @@ class TestFailedToLoadPlugin(_ExceptionTest): """Tests for the FailedToLoadPlugin exception.""" err = exceptions.FailedToLoadPlugin( - plugin=plugins_manager.Plugin( - 'plugin_name', - entrypoints.EntryPoint('plugin_name', 'os.path', None), - ), + plugin_name='plugin_name', exception=ValueError('boom!'), ) diff --git a/tests/unit/test_plugin_manager.py b/tests/unit/test_plugin_manager.py index 72959b0..9ad6aba 100644 --- a/tests/unit/test_plugin_manager.py +++ b/tests/unit/test_plugin_manager.py @@ -1,62 +1,58 @@ """Tests for flake8.plugins.manager.PluginManager.""" import mock +from flake8._compat import importlib_metadata from flake8.plugins import manager -def create_entry_point_mock(name): - """Create a mocked EntryPoint.""" - ep = mock.Mock(spec=['name']) - ep.name = name - return ep - - -@mock.patch('entrypoints.get_group_all') -def test_calls_entrypoints_on_instantiation(get_group_all): - """Verify that we call get_group_all when we create a manager.""" - get_group_all.return_value = [] +@mock.patch.object(importlib_metadata, 'entry_points') +def test_calls_entrypoints_on_instantiation(entry_points_mck): + """Verify that we call entry_points() when we create a manager.""" + entry_points_mck.return_value = {} manager.PluginManager(namespace='testing.entrypoints') - - get_group_all.assert_called_once_with('testing.entrypoints') + entry_points_mck.assert_called_once_with() -@mock.patch('entrypoints.get_group_all') -def test_calls_entrypoints_creates_plugins_automaticaly(get_group_all): +@mock.patch.object(importlib_metadata, 'entry_points') +def test_calls_entrypoints_creates_plugins_automaticaly(entry_points_mck): """Verify that we create Plugins on instantiation.""" - get_group_all.return_value = [ - create_entry_point_mock('T100'), - create_entry_point_mock('T200'), - ] + entry_points_mck.return_value = { + 'testing.entrypoints': [ + importlib_metadata.EntryPoint('T100', '', None), + importlib_metadata.EntryPoint('T200', '', None), + ], + } plugin_mgr = manager.PluginManager(namespace='testing.entrypoints') - get_group_all.assert_called_once_with('testing.entrypoints') + entry_points_mck.assert_called_once_with() assert 'T100' in plugin_mgr.plugins assert 'T200' in plugin_mgr.plugins assert isinstance(plugin_mgr.plugins['T100'], manager.Plugin) assert isinstance(plugin_mgr.plugins['T200'], manager.Plugin) -@mock.patch('entrypoints.get_group_all') -def test_handles_mapping_functions_across_plugins(get_group_all): +@mock.patch.object(importlib_metadata, 'entry_points') +def test_handles_mapping_functions_across_plugins(entry_points_mck): """Verify we can use the PluginManager call functions on all plugins.""" - entry_point_mocks = [ - create_entry_point_mock('T100'), - create_entry_point_mock('T200'), - ] - get_group_all.return_value = entry_point_mocks + entry_points_mck.return_value = { + 'testing.entrypoints': [ + importlib_metadata.EntryPoint('T100', '', None), + importlib_metadata.EntryPoint('T200', '', None), + ], + } plugin_mgr = manager.PluginManager(namespace='testing.entrypoints') plugins = [plugin_mgr.plugins[name] for name in plugin_mgr.names] assert list(plugin_mgr.map(lambda x: x)) == plugins -@mock.patch('entrypoints.get_group_all') -def test_local_plugins(get_group_all): +@mock.patch.object(importlib_metadata, 'entry_points') +def test_local_plugins(entry_points_mck): """Verify PluginManager can load given local plugins.""" - get_group_all.return_value = [] + entry_points_mck.return_value = {} plugin_mgr = manager.PluginManager( namespace='testing.entrypoints', local_plugins=['X = path.to:Plugin'] ) - assert plugin_mgr.plugins['X'].entry_point.module_name == 'path.to' + assert plugin_mgr.plugins['X'].entry_point.value == 'path.to:Plugin' diff --git a/tests/unit/test_plugin_type_manager.py b/tests/unit/test_plugin_type_manager.py index 4174c58..939aa8c 100644 --- a/tests/unit/test_plugin_type_manager.py +++ b/tests/unit/test_plugin_type_manager.py @@ -13,7 +13,7 @@ def create_plugin_mock(raise_exception=False): plugin = mock.create_autospec(manager.Plugin, instance=True) if raise_exception: plugin.load_plugin.side_effect = exceptions.FailedToLoadPlugin( - plugin=mock.Mock(name='T101'), + plugin_name='T101', exception=ValueError('Test failure'), ) return plugin