diff --git a/src/octoprint/plugin/core.py b/src/octoprint/plugin/core.py index 913289de..599eb01d 100644 --- a/src/octoprint/plugin/core.py +++ b/src/octoprint/plugin/core.py @@ -23,7 +23,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import os import imp -from collections import defaultdict, namedtuple +from collections import defaultdict, namedtuple, OrderedDict import logging import pkg_resources @@ -449,10 +449,11 @@ class PluginManager(object): self.enabled_plugins = dict() self.disabled_plugins = dict() - self.plugin_hooks = defaultdict(list) self.plugin_implementations = dict() self.plugin_implementations_by_type = defaultdict(list) + self._plugin_hooks = defaultdict(list) + self.implementation_injects = dict() self.implementation_inject_factories = [] self.implementation_pre_inits = [] @@ -476,6 +477,10 @@ class PluginManager(object): plugins.update(self.disabled_plugins) return plugins + @property + def plugin_hooks(self): + return {key: map(lambda v: (v[1], v[2]), value) for key, value in self._plugin_hooks.items()} + def find_plugins(self, existing=None, ignore_uninstalled=True): if existing is None: existing = dict(self.plugins) @@ -780,7 +785,15 @@ class PluginManager(object): # evaluate registered hooks for hook, callback in plugin.hooks.items(): - self.plugin_hooks[hook].append((name, callback)) + if isinstance(callback, tuple): + if len(callback) != 2: + continue + callback, order = callback + else: + order = None + + self._plugin_hooks[hook].append((order, name, callback)) + self._sort_hooks(hook) # evaluate registered implementation if plugin.implementation: @@ -792,8 +805,16 @@ class PluginManager(object): def _deactivate_plugin(self, name, plugin): for hook, callback in plugin.hooks.items(): + if isinstance(callback, tuple): + if len(callback) != 2: + continue + callback, order = callback + else: + order = None + try: - self.plugin_hooks[hook].remove((name, callback)) + self._plugin_hooks[hook].remove((order, name, callback)) + self._sort_hooks(hook) except ValueError: # that's ok, the plugin was just not registered for the hook pass @@ -1019,7 +1040,11 @@ class PluginManager(object): if not hook in self.plugin_hooks: return dict() - return {hook[0]: hook[1] for hook in self.plugin_hooks[hook]} + + result = OrderedDict() + for h in self.plugin_hooks[hook]: + result[h[0]] = h[1] + return result def get_implementations(self, *types): """ @@ -1118,6 +1143,10 @@ class PluginManager(object): try: client(plugin, data) except: self.logger.exception("Exception while sending plugin data to client") + def _sort_hooks(self, hook): + self._plugin_hooks[hook] = sorted(self._plugin_hooks[hook], + key=lambda x: (x[0] is None, x[0], x[1], x[2])) + class InstalledEntryPoint(pkginfo.Installed): diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index aca5e1a6..be79b626 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -270,18 +270,20 @@ def index(): if len(missing_in_order) == 0: continue - # finally add anything that's not included in our order yet - sorted_missing = list(missing_in_order) - if template_sorting[t]["key"] is not None: - # default extractor: works with entries that are dicts and entries that are 2-tuples with the - # entry data at index 1 - def extractor(item, key): - if isinstance(item, dict) and key in item: - return item[key] - elif isinstance(item, tuple) and len(item) > 1 and isinstance(item[1], dict) and key in item[1]: - return item[1][key] + # works with entries that are dicts and entries that are 2-tuples with the + # entry data at index 1 + def config_extractor(item, key, default_value=None): + if isinstance(item, dict) and key in item: + return item[key] if key in item else default_value + elif isinstance(item, tuple) and len(item) > 1 and isinstance(item[1], dict) and key in item[1]: + return item[1][key] if key in item[1] else default_value - return None + return default_value + + # finally add anything that's not included in our order yet + if template_sorting[t]["key"] is not None: + # we'll use our config extractor as default key extractor + extractor = config_extractor # if template type provides custom extractor, make sure its exceptions are handled if "key_extractor" in template_sorting[t] and callable(template_sorting[t]["key_extractor"]): @@ -296,7 +298,20 @@ def index(): extractor = create_safe_extractor(template_sorting[t]["key_extractor"]) sort_key = template_sorting[t]["key"] - sorted_missing = sorted(missing_in_order, key=lambda x: extractor(templates[t]["entries"][x], sort_key)) + + def key_func(x): + config = templates[t]["entries"][x] + entry_order = config_extractor(config, "order", default_value=None) + return entry_order is None, entry_order, extractor(config, sort_key) + + sorted_missing = sorted(missing_in_order, key=key_func) + else: + def key_func(x): + config = templates[t]["entries"][x] + entry_order = config_extractor(config, "order", default_value=None) + return entry_order is None, entry_order + + sorted_missing = sorted(missing_in_order, key=key_func) if template_sorting[t]["add"] == "prepend": templates[t]["order"] = sorted_missing + templates[t]["order"]