Repository: ckan/ckan

Issue:
Replace pyutilib.component.core with a modern alternative
We use pyutilib.component.core to define the interface classes and methods used by our plugins system, eg things like `SingletonPlugin`, `implements` or `PluginImplementations`. It is not a lot of code but obviously pretty critical as the extensions depend completely on it.

pyutilib.component.core is ancient, and it has since moved back to a single PyUtilib package as discussed in [here](https://github.com/PyUtilib/pyutilib/issues/30).

#### Approach
A quick look at the relevant module on pyutilib shows that the same classes and methods are still available so hopefully it's just a matter of changing the requirements and the imports to point to the new package.

In the future we might want to look at an alternative but to get the Python 2 dependency out of the way this looks like the easiest option.

#### Story points

3


Discussion:

Resolving patch:
diff --git a/ckan/plugins/core.py b/ckan/plugins/core.py
--- a/ckan/plugins/core.py
+++ b/ckan/plugins/core.py
@@ -112,10 +112,10 @@ def plugins_update():
     # the file containing them is imported, for example if two or more
     # extensions are defined in the same file.  Therefore we do a sanity
     # check and disable any that should not be active.
-    for env in PluginGlobals.env_registry.values():
-        for service in env.services.copy():
-            if service.__class__ not in _PLUGINS_CLASS:
-                service.deactivate()
+    for env in PluginGlobals.env.values():
+        for service, id_ in env.singleton_services.items():
+            if service not in _PLUGINS_CLASS:
+                PluginGlobals.plugin_instances[id_].deactivate()
 
     # Reset CKAN to reflect the currently enabled extensions.
     import ckan.config.environment as environment


Test patch:
diff --git a/ckan/tests/legacy/test_plugins.py b/ckan/tests/legacy/test_plugins.py
--- a/ckan/tests/legacy/test_plugins.py
+++ b/ckan/tests/legacy/test_plugins.py
@@ -101,7 +101,7 @@ def test_plugins_load(self):
 
         # synchronous_search automatically gets loaded
         current_plugins = set([plugins.get_plugin(p) for p in ['mapper_plugin', 'routes_plugin', 'synchronous_search'] + find_system_plugins()])
-        assert PluginGlobals.env().services == current_plugins
+        assert set(plugins.core._PLUGINS_SERVICE.values()) == current_plugins
         # cleanup
         config['ckan.plugins'] = config_plugins
         plugins.load_all()
@@ -175,10 +175,15 @@ def test_mapper_plugin_fired_on_delete(self):
 
     def test_routes_plugin_fired(self):
         with plugins.use_plugin('routes_plugin'):
-            routes_plugin = PluginGlobals.env_registry['pca'].plugin_registry['RoutesPlugin'].__instance__
-            assert routes_plugin.calls_made == ['before_map', 'after_map'], \
-                   routes_plugin.calls_made
+            pca = PluginGlobals.env['pca']
+            routes_plugin_idx = pca.singleton_services[
+                pca.plugin_registry['RoutesPlugin']
+            ]
+
+            routes_plugin = PluginGlobals.plugin_instances[routes_plugin_idx]
 
+            assert routes_plugin.calls_made == ['before_map', 'after_map'], \
+                routes_plugin.calls_made
 
     def test_action_plugin_override(self):
         status_show_original = logic.get_action('status_show')(None, {})