-
Notifications
You must be signed in to change notification settings - Fork 17.3k
Replace pkg_resources with importlib.metadata to avoid VersionConflict errors #12694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,30 +78,6 @@ def test_app_blueprints(self): | |
| self.assertTrue('test_plugin' in self.app.blueprints) | ||
| self.assertEqual(self.app.blueprints['test_plugin'].name, bp.name) | ||
|
|
||
| @mock.patch('airflow.plugins_manager.pkg_resources.iter_entry_points') | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test was in the "TestPluginsRBAC" class -- I moved to the "right" place. |
||
| def test_entrypoint_plugin_errors_dont_raise_exceptions(self, mock_ep_plugins): | ||
| """ | ||
| Test that Airflow does not raise an Error if there is any Exception because of the | ||
| Plugin. | ||
| """ | ||
| from airflow.plugins_manager import import_errors, load_entrypoint_plugins | ||
|
|
||
| mock_entrypoint = mock.Mock() | ||
| mock_entrypoint.name = 'test-entrypoint' | ||
| mock_entrypoint.module_name = 'test.plugins.test_plugins_manager' | ||
| mock_entrypoint.load.side_effect = Exception('Version Conflict') | ||
| mock_ep_plugins.return_value = [mock_entrypoint] | ||
|
|
||
| with self.assertLogs("airflow.plugins_manager", level="ERROR") as log_output: | ||
| load_entrypoint_plugins() | ||
|
|
||
| received_logs = log_output.output[0] | ||
| # Assert Traceback is shown too | ||
| assert "Traceback (most recent call last):" in received_logs | ||
| assert "Version Conflict" in received_logs | ||
| assert "Failed to import plugin test-entrypoint" in received_logs | ||
| assert ("test.plugins.test_plugins_manager", "Version Conflict") in import_errors.items() | ||
|
|
||
|
|
||
| class TestPluginsManager: | ||
| def test_no_log_when_no_plugins(self, caplog): | ||
|
|
@@ -210,6 +186,33 @@ class AirflowAdminMenuLinksPlugin(AirflowPlugin): | |
|
|
||
| assert caplog.record_tuples == [] | ||
|
|
||
| def test_entrypoint_plugin_errors_dont_raise_exceptions(self, caplog): | ||
| """ | ||
| Test that Airflow does not raise an error if there is any Exception because of a plugin. | ||
| """ | ||
| from airflow.plugins_manager import import_errors, load_entrypoint_plugins | ||
|
|
||
| mock_dist = mock.Mock() | ||
|
|
||
| mock_entrypoint = mock.Mock() | ||
| mock_entrypoint.name = 'test-entrypoint' | ||
| mock_entrypoint.group = 'airflow.plugins' | ||
| mock_entrypoint.module_name = 'test.plugins.test_plugins_manager' | ||
| mock_entrypoint.load.side_effect = ImportError('my_fake_module not found') | ||
| mock_dist.entry_points = [mock_entrypoint] | ||
|
|
||
| with mock.patch('importlib_metadata.distributions', return_value=[mock_dist]), caplog.at_level( | ||
| logging.ERROR, logger='airflow.plugins_manager' | ||
| ): | ||
| load_entrypoint_plugins() | ||
|
|
||
| received_logs = caplog.text | ||
| # Assert Traceback is shown too | ||
| assert "Traceback (most recent call last):" in received_logs | ||
| assert "my_fake_module not found" in received_logs | ||
| assert "Failed to import plugin test-entrypoint" in received_logs | ||
| assert ("test.plugins.test_plugins_manager", "my_fake_module not found") in import_errors.items() | ||
|
|
||
|
|
||
| class TestPluginsDirectorySource(unittest.TestCase): | ||
| def test_should_return_correct_path_name(self): | ||
|
|
@@ -221,20 +224,23 @@ def test_should_return_correct_path_name(self): | |
| self.assertEqual("<em>$PLUGINS_FOLDER/</em>test_plugins_manager.py", source.__html__()) | ||
|
|
||
|
|
||
| class TestEntryPointSource(unittest.TestCase): | ||
| @mock.patch('airflow.plugins_manager.pkg_resources.iter_entry_points') | ||
| def test_should_return_correct_source_details(self, mock_ep_plugins): | ||
| class TestEntryPointSource: | ||
| def test_should_return_correct_source_details(self): | ||
| from airflow import plugins_manager | ||
|
|
||
| mock_entrypoint = mock.Mock() | ||
| mock_entrypoint.name = 'test-entrypoint-plugin' | ||
| mock_entrypoint.module_name = 'module_name_plugin' | ||
| mock_entrypoint.dist = 'test-entrypoint-plugin==1.0.0' | ||
| mock_ep_plugins.return_value = [mock_entrypoint] | ||
|
|
||
| plugins_manager.load_entrypoint_plugins() | ||
| mock_dist = mock.Mock() | ||
| mock_dist.metadata = {'name': 'test-entrypoint-plugin'} | ||
| mock_dist.version = '1.0.0' | ||
| mock_dist.entry_points = [mock_entrypoint] | ||
|
|
||
| with mock.patch('importlib_metadata.distributions', return_value=[mock_dist]): | ||
| plugins_manager.load_entrypoint_plugins() | ||
|
|
||
| source = plugins_manager.EntryPointSource(mock_entrypoint) | ||
| self.assertEqual(str(mock_entrypoint), source.entrypoint) | ||
| self.assertEqual("test-entrypoint-plugin==1.0.0: " + str(mock_entrypoint), str(source)) | ||
| self.assertEqual("<em>test-entrypoint-plugin==1.0.0:</em> " + str(mock_entrypoint), source.__html__()) | ||
| source = plugins_manager.EntryPointSource(mock_entrypoint, mock_dist) | ||
| assert str(mock_entrypoint) == source.entrypoint | ||
| assert "test-entrypoint-plugin==1.0.0: " + str(mock_entrypoint) == str(source) | ||
| assert "<em>test-entrypoint-plugin==1.0.0:</em> " + str(mock_entrypoint) == source.__html__() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -52,7 +52,7 @@ | |
| from airflow.models.serialized_dag import SerializedDagModel | ||
| from airflow.operators.bash import BashOperator | ||
| from airflow.operators.dummy_operator import DummyOperator | ||
| from airflow.plugins_manager import AirflowPlugin, EntryPointSource, PluginsDirectorySource | ||
| from airflow.plugins_manager import AirflowPlugin, EntryPointSource | ||
| from airflow.security import permissions | ||
| from airflow.ti_deps.dependencies_states import QUEUEABLE_STATES, RUNNABLE_STATES | ||
| from airflow.utils import dates, timezone | ||
|
|
@@ -67,6 +67,7 @@ | |
| from tests.test_utils.asserts import assert_queries_count | ||
| from tests.test_utils.config import conf_vars | ||
| from tests.test_utils.db import clear_db_runs | ||
| from tests.test_utils.mock_plugins import mock_plugin_manager | ||
|
|
||
|
|
||
| class TemplateWithContext(NamedTuple): | ||
|
|
@@ -337,10 +338,6 @@ class PluginOperator(BaseOperator): | |
| pass | ||
|
|
||
|
|
||
| class EntrypointPlugin(AirflowPlugin): | ||
| name = 'test-entrypoint-testpluginview' | ||
|
|
||
|
|
||
| class TestPluginView(TestBase): | ||
| def test_should_list_plugins_on_page_with_details(self): | ||
| resp = self.client.get('/plugin') | ||
|
|
@@ -349,45 +346,22 @@ def test_should_list_plugins_on_page_with_details(self): | |
| self.check_content_in_response("source", resp) | ||
| self.check_content_in_response("<em>$PLUGINS_FOLDER/</em>test_plugin.py", resp) | ||
|
|
||
| @mock.patch('airflow.plugins_manager.pkg_resources.iter_entry_points') | ||
| def test_should_list_entrypoint_plugins_on_page_with_details(self, mock_ep_plugins): | ||
| from airflow.plugins_manager import load_entrypoint_plugins | ||
|
|
||
| mock_entrypoint = mock.Mock() | ||
| mock_entrypoint.name = 'test-entrypoint-testpluginview' | ||
| mock_entrypoint.module_name = 'module_name_testpluginview' | ||
| mock_entrypoint.dist = 'test-entrypoint-testpluginview==1.0.0' | ||
| mock_entrypoint.load.return_value = EntrypointPlugin | ||
| mock_ep_plugins.return_value = [mock_entrypoint] | ||
| def test_should_list_entrypoint_plugins_on_page_with_details(self): | ||
|
|
||
| load_entrypoint_plugins() | ||
| resp = self.client.get('/plugin') | ||
| mock_plugin = AirflowPlugin() | ||
| mock_plugin.name = "test_plugin" | ||
| mock_plugin.source = EntryPointSource( | ||
| mock.Mock(), mock.Mock(version='1.0.0', metadata={'name': 'test-entrypoint-testpluginview'}) | ||
| ) | ||
| with mock_plugin_manager(plugins=[mock_plugin]): | ||
| resp = self.client.get('/plugin') | ||
|
|
||
| self.check_content_in_response("test_plugin", resp) | ||
| self.check_content_in_response("Airflow Plugins", resp) | ||
| self.check_content_in_response("source", resp) | ||
| self.check_content_in_response("<em>test-entrypoint-testpluginview==1.0.0:</em> <Mock id=", resp) | ||
|
|
||
|
|
||
| class TestPluginsDirectorySource(unittest.TestCase): | ||
| def test_should_provide_correct_attribute_values(self): | ||
| source = PluginsDirectorySource("./test_views.py") | ||
| self.assertEqual("$PLUGINS_FOLDER/../../test_views.py", str(source)) | ||
| self.assertEqual("<em>$PLUGINS_FOLDER/</em>../../test_views.py", source.__html__()) | ||
| self.assertEqual("../../test_views.py", source.path) | ||
|
|
||
|
|
||
| class TestEntryPointSource(unittest.TestCase): | ||
| def test_should_provide_correct_attribute_values(self): | ||
| mock_entrypoint = mock.Mock() | ||
| mock_entrypoint.dist = 'test-entrypoint-dist==1.0.0' | ||
| source = EntryPointSource(mock_entrypoint) | ||
| self.assertEqual("test-entrypoint-dist==1.0.0", source.dist) | ||
| self.assertEqual(str(mock_entrypoint), source.entrypoint) | ||
| self.assertEqual("test-entrypoint-dist==1.0.0: " + str(mock_entrypoint), str(source)) | ||
| self.assertEqual("<em>test-entrypoint-dist==1.0.0:</em> " + str(mock_entrypoint), source.__html__()) | ||
|
|
||
|
|
||
|
Comment on lines
-372
to
-390
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These were duplicating tests in test_plugins_manager.py -- they don't belong here. |
||
| class TestPoolModelView(TestBase): | ||
| def setUp(self): | ||
| super().setUp() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you meant
argcompleteperhaps? I note thatargcompletehas been updated to supportimportlib_metadata3.x (and otherwise only has bugfixes, looks like a desireable upgrade).And if 3.1 works, please widen the pin to
>=1.7,<4; theargcomplete~=1.10pin above already lets you useimportlib_metadata3.1 if you have the newerargcompleteinstalled.Also, Python 3.8 doesn't need
importlib_metadataanymore, andargcompletecorrectly won't depend on it in 3.8 and up. Please do so here too:and make the imports conditional:
If you update the
argcompletedependency too, then you'd not even have to set the>=1.7lower bound,~=3.1would do.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whops, yes I did mean argcomplete. Though I also found virtualenv (dep of pylint) was also pinning this, so it may not help us yet.
This line in the readme https://github.com/python/importlib_metadata/ :
Makes me wonder if/when we should continue to use it anyway on Py 3.8?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah though virtualenv has been updated too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your not using the 3.9 / importlib_metadata 1.6 feature:
Added module and attr attributes to EntryPoint; everything else is bug fixing or perf improvements, which all have been backported to 3.8.Even if you were using those extra attributes, using
python_version<="3.8"(and inverting the import guard) would be a good idea to keep the dependencies flexible and avoid limiting what packages people can use with Airflow.I don't see where
virtualenvdepends on importlib_metadata? Nor does pylint depend on virtualenv. Might be in older versions.pre-commitdepends on virtualenv though. And jsonlint depends onimportlib_metadatabut sets no pin.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
God in getting my dist names all in a mix today. I did mean pre-commit -> virtualenv. I'll have to check what version of virtualenv pre-commit specifies.
I'll open a new PR to add the guard as you suggest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've already opened a PR :-) See #12703