Repository: ckan/ckan

Issue:
CKAN 2.9 changes order in which plugins are returned by PluginImplementations
## Summary
I'm porting a big project from CKAN 2.8 to 2.9. My plugin overrides a template from ckanext-scheming to customize the form. After upgrade, my changes weren't reflected because my custom template wasn't loaded.
Only after changing the ordering of the plugins in `ckan.plugins` it was picked up.

So on CKAN <= 2.8, in order for plugin `abc` to override `scheming_datasets` you needed:

```
ckan.plugins = abc scheming_datasets 
```

In CKAN 2.9, you need:
```
ckan.plugins = scheming_datasets abc
```

### Why it is important

This is pretty significant change, which AFAICT wasn't mentioned in the changelog.

After initial investigation it looks like the issue is not how we parse the config option or load the plugins, but how the `PluginImplementations` iterator returns them. We use them in all places where we let plugins integrate with CKAN core. For instance in `environment.py` we call:

```python
      for plugin in p.PluginImplementations(p.IConfigurer):                                                                                                                                                                                                                          
          plugin.update_config(config) 
```
This is one is relevant to my issue, as it registers template directories from plugins and stores them on a list in `config['extra_template_paths']`. Order is important, as the first template path found will be used to render.

At [this point](https://github.com/ckan/ckan/blob/8eec3e27c320baf29e0d99b2ce20ed14ae10b0d3/ckan/config/environment.py#L173) we get the following behaviour:

* On CKAN 2.8:

```python 
[plugin for plugin in p.PluginImplementations(p.IConfigurer)]

# [<Plugin AbcPlugin 'abc'>, <Plugin SchemingDatasetsPlugin 'scheming_datasets'>]

config['extra_template_paths'].split(',')

# [
#  u'/home/adria/dev/pyenvs/ckan/src/ckanext-abc/ckanext/abc/templates',
#  u'/home/adria/dev/pyenvs/ckan/src/ckanext-scheming/ckanext/scheming/templates',
# ]
```
* On CKAN 2.9:

```python
[plugin for plugin in p.PluginImplementations(p.IConfigurer)]

# [<Plugin SchemingDatasetsPlugin 'scheming_datasets'>, <Plugin AbcPlugin 'abc'>]

config['extra_template_paths'].split(',')

# [
#  u'/home/adria/dev/pyenvs/ckan/src/ckanext-scheming/ckanext/scheming/templates',
#  u'/home/adria/dev/pyenvs/ckan/src/ckanext-abc/ckanext/abc/templates',
# ]
```

Apart from template loading issues this is likely to affect everywhere where the order of plugins is important, eg chained actions, chained auth functions.

### Root cause

After looking at [ckan/plugins/core.py](https://github.com/ckan/ckan/blob/master/ckan/plugins/core.py) my current thinking is that this is *not* related to the loading of the plugins. AFAICT we´ve always loaded them in the order that they are defined in `ckan.plugins`. It´s the actual iterator returned by `PluginImplementations` that changed the order of the returned plugins at some point between the two versions (pyutilib.component.core==4.6.4 in CKAN 2.8, PyUtilib==5.7.1 in CKAN 2.9). We are importing this class directly from Pyutillib. The only work done on this code between these two versions was https://github.com/ckan/ckan/pull/4886, and I don´t think it should affect the ordering (apart from upgrading the library of course)

### What should we do?

My ideas so far:

1. Change nothing and assume this is the new behaviour, *but* documenting it in the relevant places (2.9 Changelog, plugins docs, mail to ckan-dev). I don´t think we can leave a change like this undocumented
2. Create our own `PluginImplementations` wrapper that restores the old ordering (maybe optionally based on a config option). We would need to override the [`__iter__()`](https://github.com/PyUtilib/pyutilib/blob/5.7.3/pyutilib/component/core/core.py#L222) method, not sure how easy that is

Any thoughts or other ideas on what to do? @ckan/core 



Discussion:
Personally I think we should try to preserve the order if we can. People don't read change logs or understand them, based on solely on the amount of support requests we've had since 2.9 release.

Resolving patch:
diff --git a/ckan/plugins/core.py b/ckan/plugins/core.py
--- a/ckan/plugins/core.py
+++ b/ckan/plugins/core.py
@@ -8,7 +8,7 @@
 import logging
 from pkg_resources import iter_entry_points
 from pyutilib.component.core import PluginGlobals, implements
-from pyutilib.component.core import ExtensionPoint as PluginImplementations
+from pyutilib.component.core import ExtensionPoint
 from pyutilib.component.core import SingletonPlugin as _pca_SingletonPlugin
 from pyutilib.component.core import Plugin as _pca_Plugin
 from ckan.common import asbool
@@ -71,6 +71,21 @@ def use_plugin(*plugins):
         unload(*plugins)
 
 
+class PluginImplementations(ExtensionPoint):
+
+    def __iter__(self):
+        '''
+        When we upgraded pyutilib on CKAN 2.9 the order in which
+        plugins were returned by `PluginImplementations` changed
+        so we use this wrapper to maintain the previous order
+        (which is the same as the ckan.plugins config option)
+        '''
+
+        iterator = super(PluginImplementations, self).__iter__()
+
+        return reversed(list(iterator))
+
+
 class PluginNotFoundException(Exception):
     '''
     Raised when a requested plugin cannot be found.


Test patch:
diff --git a/ckan/tests/plugins/test_core.py b/ckan/tests/plugins/test_core.py
new file mode 100644
--- /dev/null
+++ b/ckan/tests/plugins/test_core.py
@@ -0,0 +1,21 @@
+# encoding: utf-8
+
+import pytest
+
+import ckan.plugins as p
+
+
+@pytest.mark.usefixtures(u"with_plugins")
+@pytest.mark.ckan_config(
+    u"ckan.plugins",
+    u"example_idatasetform_v1 example_idatasetform_v2 example_idatasetform_v3")
+def test_plugins_order_in_pluginimplementations():
+
+    assert (
+        [plugin.name for plugin in p.PluginImplementations(p.IDatasetForm)] ==
+        [
+            u"example_idatasetform_v1",
+            u"example_idatasetform_v2",
+            u"example_idatasetform_v3"
+        ]
+    )