From 917b22725b79a39d70c5a88f9cd47a8f72b83228 Mon Sep 17 00:00:00 2001
From: Pavel Zuna <pzuna@redhat.com>
Date: Tue, 2 Mar 2010 09:05:24 +0100
Subject: [PATCH] Add plugin versioning and dependency checking.

---
 ipalib/errors.py   |   15 +++++++++++++++
 ipalib/plugable.py |   44 ++++++++++++++++++++++----------------------
 2 files changed, 37 insertions(+), 22 deletions(-)

diff --git a/ipalib/errors.py b/ipalib/errors.py
index d1d39a3..72d64e4 100644
--- a/ipalib/errors.py
+++ b/ipalib/errors.py
@@ -211,6 +211,21 @@ class PluginMissingOverrideError(PrivateError):
     format = '%(base)s.%(name)s not registered, cannot override with %(plugin)r'
 
 
+class PluginMissingDependencyError(PrivateError):
+    """
+    Raised when a plugin dependency is not found when finalizing API.
+
+    For example:
+
+    >>> raise PluginMissingDependencyError(plugin='service_add', dependency='host', major=1, minor=0)
+    Traceback (most recent call last):
+      ...
+    PluginMissingDependencyError: Plugin 'service_add' is missing dependency plugin 'host' (version 1)
+    """
+
+    format = 'Plugin \'%(plugin)s\' is missing dependency plugin \'%(dependency)s\' (version %(version)d)'
+
+
 class SkipPluginModule(PrivateError):
     """
     Raised to abort the loading of a plugin module.
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index ded762d..8cfb5d3 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -158,7 +158,8 @@ class Plugin(ReadOnly):
     """
     Base class for all plugins.
     """
-
+    VERSION = (1, 0) # (major version number, minor version number)
+    DEPENDENCIES = tuple() # ((plugin_name, major version number), ...)
     label = None
 
     def __init__(self):
@@ -306,40 +307,27 @@ class Registrar(DictProxy):
                 plugin=klass, bases=self.__allowed.keys()
             )
 
-    def __call__(self, klass, override=False):
+    def __call__(self, klass):
         """
         Register the plugin ``klass``.
 
         :param klass: A subclass of `Plugin` to attempt to register.
-        :param override: If true, override an already registered plugin.
         """
         if not inspect.isclass(klass):
             raise TypeError('plugin must be a class; got %r' % klass)
 
         # Raise DuplicateError if this exact class was already registered:
         if klass in self.__registered:
-            raise errors.PluginDuplicateError(plugin=klass)
+            v1 = klass.VERSION
+            v2 = self.__registered[klass].VERSION
+            if v1 == v2:
+                raise errors.PluginDuplicateError(plugin=klass)
+            if (v1[0] < v2[0]) or (v1[0] == v2[0] and v1[1] < v2[1]):
+                # Ignore lower version plugins
+                return
 
         # Find the base class or raise SubclassError:
         for (base, sub_d) in self.__findbases(klass):
-            # Check override:
-            if klass.__name__ in sub_d:
-                if not override:
-                    # Must use override=True to override:
-                    raise errors.PluginOverrideError(
-                        base=base.__name__,
-                        name=klass.__name__,
-                        plugin=klass,
-                    )
-            else:
-                if override:
-                    # There was nothing already registered to override:
-                    raise errors.PluginMissingOverrideError(
-                        base=base.__name__,
-                        name=klass.__name__,
-                        plugin=klass,
-                    )
-
             # The plugin is okay, add to sub_d:
             sub_d[klass.__name__] = klass
 
@@ -601,13 +589,25 @@ class API(DictProxy):
             self.__d[name] = namespace
             object.__setattr__(self, name, namespace)
 
+        names = {}
         for p in plugins.itervalues():
             p.instance.set_api(self)
             assert p.instance.api is self
+            # we're going to use this later to check plugin dependencies
+            names[p.instance.name] = p.klass
 
         for p in plugins.itervalues():
             p.instance.finalize()
             assert islocked(p.instance) is True
+
+        for p in plugins.itervalues():
+            for (pname, major) in p.instance.DEPENDENCIES:
+                if pname not in names or major != names[pname].VERSION[0]:
+                    raise errors.PluginMissingDependencyError(
+                        plugin=p.instance.name,
+                        dependency=pname,
+                        version=major
+                    )
         object.__setattr__(self, '_API__finalized', True)
         tuple(PluginInfo(p) for p in plugins.itervalues())
         object.__setattr__(self, 'plugins',
-- 
1.6.6

