diff --git a/flake8/exceptions.py b/flake8/exceptions.py new file mode 100644 index 0000000..ac78769 --- /dev/null +++ b/flake8/exceptions.py @@ -0,0 +1,25 @@ +"""Exception classes for all of Flake8.""" + + +class Flake8Exception(Exception): + """Plain Flake8 exception.""" + + pass + + +class FailedToLoadPlugin(Flake8Exception): + """Exception raised when a plugin fails to load.""" + + FORMAT = 'Flake8 failed to load plugin "%(name)s" due to %(exc)s.' + + def __init__(self, *args, **kwargs): + """Initialize our FailedToLoadPlugin exception.""" + self.plugin = kwargs.pop('plugin') + self.ep_name = self.plugin.name + self.original_exception = kwargs.pop('exception') + super(FailedToLoadPlugin, self).__init__(*args, **kwargs) + + def __str__(self): + """Return a nice string for our exception.""" + return self.FORMAT % {'name': self.ep_name, + 'exc': self.original_exception} diff --git a/flake8/plugins/manager.py b/flake8/plugins/manager.py index ce74e08..a80020e 100644 --- a/flake8/plugins/manager.py +++ b/flake8/plugins/manager.py @@ -5,6 +5,7 @@ import logging import pkg_resources from flake8 import _trie +from flake8 import exceptions LOG = logging.getLogger(__name__) @@ -58,6 +59,21 @@ class Plugin(object): r"""Call the plugin with \*args and \*\*kwargs.""" return self.plugin(*args, **kwargs) + def _load(self, verify_requirements): + # Avoid relying on hasattr() here. + resolve = getattr(self.entry_point, 'resolve', None) + require = getattr(self.entry_point, 'require', None) + if resolve and require: + if verify_requirements: + LOG.debug('Verifying plugin "%s"\'s requirements.', + self.name) + require() + self._plugin = resolve() + else: + self._plugin = self.entry_point.load( + require=verify_requirements + ) + def load_plugin(self, verify_requirements=False): """Retrieve the plugin for this entry-point. @@ -73,19 +89,16 @@ class Plugin(object): """ if self._plugin is None: LOG.debug('Loading plugin "%s" from entry-point.', self.name) - # Avoid relying on hasattr() here. - resolve = getattr(self.entry_point, 'resolve', None) - require = getattr(self.entry_point, 'require', None) - if resolve and require: - if verify_requirements: - LOG.debug('Verifying plugin "%s"\'s requirements.', - self.name) - require() - self._plugin = resolve() - else: - self._plugin = self.entry_point.load( - require=verify_requirements + try: + self._load(verify_requirements) + except Exception as load_exception: + LOG.exception(load_exception, exc_info=True) + failed_to_load = exceptions.FailedToLoadPlugin( + plugin=self, + exception=load_exception, ) + LOG.critical(str(failed_to_load)) + raise failed_to_load def provide_options(self, optmanager, options, extra_args): """Pass the parsed options and extra arguments to the plugin.""" diff --git a/tests/unit/test_plugin.py b/tests/unit/test_plugin.py index 28b95c9..01454a1 100644 --- a/tests/unit/test_plugin.py +++ b/tests/unit/test_plugin.py @@ -1,6 +1,8 @@ """Tests for flake8.plugins.manager.Plugin.""" import mock +import pytest +from flake8 import exceptions from flake8.plugins import manager @@ -48,6 +50,16 @@ def test_load_plugin_only_calls_require_when_verifying_requirements(): entry_point.resolve.assert_called_once_with() +def test_load_plugin_catches_and_reraises_exceptions(): + """Verify we raise our own FailedToLoadPlugin.""" + entry_point = mock.Mock(spec=['require', 'resolve']) + entry_point.resolve.side_effect = ValueError('Test failure') + plugin = manager.Plugin('T000', entry_point) + + with pytest.raises(exceptions.FailedToLoadPlugin): + plugin.load_plugin() + + def test_plugin_property_loads_plugin_on_first_use(): """Verify that we load our plugin when we first try to use it.""" entry_point = mock.Mock(spec=['require', 'resolve', 'load'])