summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJason Gerard DeRose <jderose@redhat.com>2008-07-28 04:34:25 +0000
committerJason Gerard DeRose <jderose@redhat.com>2008-07-28 04:34:25 +0000
commitbc1675dc3853748064dbf1485bf58bce0e344add (patch)
tree1403fd2ae76c21b49f3ee16f91d72c79aac2d76e
parent8b64314359950801f1b3220f655261bcee2ead85 (diff)
downloadfreeipa-bc1675dc3853748064dbf1485bf58bce0e344add.tar.gz
freeipa-bc1675dc3853748064dbf1485bf58bce0e344add.tar.xz
freeipa-bc1675dc3853748064dbf1485bf58bce0e344add.zip
30: Added plugable module with more generic implementation of Registrar; added corresponding unit tests
-rw-r--r--ipalib/exceptions.py72
-rw-r--r--ipalib/plugable.py95
-rw-r--r--ipalib/tests/test_plugable.py115
3 files changed, 275 insertions, 7 deletions
diff --git a/ipalib/exceptions.py b/ipalib/exceptions.py
index 4c307177..376a7a56 100644
--- a/ipalib/exceptions.py
+++ b/ipalib/exceptions.py
@@ -45,24 +45,82 @@ class IPAError(Exception):
return self.msg % self.kw
+
+
+
class SetError(IPAError):
msg = 'setting %r, but NameSpace does not allow attribute setting'
-class OverrideError(IPAError):
- msg = 'unexpected override of %r (use override=True if intended)'
-class DuplicateError(IPAError):
- msg = 'class %r at %d was already registered'
+
class RegistrationError(IPAError):
- msg = '%s: %r'
+ """
+ Base class for errors that occur during plugin registration.
+ """
+
+
+class SubclassError(RegistrationError):
+ """
+ Raised when registering a plugin that is not a subclass of one of the
+ allowed bases.
+ """
+ msg = 'plugin %r not subclass of any base in %r'
+
+ def __init__(self, cls, allowed):
+ self.cls = cls
+ self.allowed = allowed
+
+ def __str__(self):
+ return self.msg % (self.cls, self.allowed)
+
+
+class DuplicateError(RegistrationError):
+ """
+ Raised when registering a plugin whose exact class has already been
+ registered.
+ """
+ msg = '%r at %d was already registered'
+ def __init__(self, cls):
+ self.cls = cls
+
+ def __str__(self):
+ return self.msg % (self.cls, id(self.cls))
+
+
+class OverrideError(RegistrationError):
+ """
+ Raised when override=False yet registering a plugin that overrides an
+ existing plugin in the same namespace.
+ """
+ msg = 'unexpected override of %s.%s with %r (use override=True if intended)'
+
+ def __init__(self, base, cls):
+ self.base = base
+ self.cls = cls
+
+ def __str__(self):
+ return self.msg % (self.base.__name__, self.cls.__name__, self.cls)
+
+
+class MissingOverrideError(RegistrationError):
+ """
+ Raised when override=True yet no preexisting plugin with the same name
+ and base has been registered.
+ """
+ msg = '%s.%s has not been registered, cannot override with %r'
+
+ def __init__(self, base, cls):
+ self.base = base
+ self.cls = cls
+
+ def __str__(self):
+ return self.msg % (self.base.__name__, self.cls.__name__, self.cls)
-class PrefixError(IPAError):
- msg = 'class name %r must start with %r'
class TwiceSetError(IPAError):
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
new file mode 100644
index 00000000..ba2241b9
--- /dev/null
+++ b/ipalib/plugable.py
@@ -0,0 +1,95 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+"""
+Utility classes for registering plugins, base classe for writing plugins.
+"""
+
+import inspect
+import exceptions
+
+
+
+class Registrar(object):
+ def __init__(self, *allowed):
+ """
+ `*allowed` is a list of the base classes plugins can be subclassed
+ from.
+ """
+ self.__allowed = frozenset(allowed)
+ self.__d = {}
+ self.__registered = set()
+ assert len(self.__allowed) == len(allowed)
+ for base in self.__allowed:
+ assert inspect.isclass(base)
+ assert base.__name__ not in self.__d
+ self.__d[base.__name__] = {}
+
+ def __findbase(self, cls):
+ """
+ If `cls` is a subclass of a base in self.__allowed, returns that
+ base; otherwise raises SubclassError.
+ """
+ assert inspect.isclass(cls)
+ for base in self.__allowed:
+ if issubclass(cls, base):
+ return base
+ raise exceptions.SubclassError(cls, self.__allowed)
+
+ def __call__(self, cls, override=False):
+ """
+ Register the plugin `cls`.
+ """
+ if not inspect.isclass(cls):
+ raise TypeError('plugin must be a class: %r' % cls)
+
+ # Find the base class or raise SubclassError:
+ base = self.__findbase(cls)
+ sub_d = self.__d[base.__name__]
+
+ # Raise DuplicateError if this exact class was already registered:
+ if cls in self.__registered:
+ raise exceptions.DuplicateError(cls)
+
+ # Check override:
+ if cls.__name__ in sub_d:
+ # Must use override=True to override:
+ if not override:
+ raise exceptions.OverrideError(base, cls)
+ else:
+ # There was nothing already registered to override:
+ if override:
+ raise exceptions.MissingOverrideError(base, cls)
+
+ # The plugin is okay, add to __registered and sub_d:
+ self.__registered.add(cls)
+ sub_d[cls.__name__] = cls
+
+ def __getitem__(self, name):
+ """
+ Returns a copy of the namespace dict of the base class named `name`.
+ """
+ return dict(self.__d[name])
+
+ def __iter__(self):
+ """
+ Iterates through the names of the allowed base classes.
+ """
+ for key in self.__d:
+ yield key
diff --git a/ipalib/tests/test_plugable.py b/ipalib/tests/test_plugable.py
new file mode 100644
index 00000000..1ba02113
--- /dev/null
+++ b/ipalib/tests/test_plugable.py
@@ -0,0 +1,115 @@
+# Authors:
+# Jason Gerard DeRose <jderose@redhat.com>
+#
+# Copyright (C) 2008 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2 only
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+"""
+Unit tests for `ipalib.plugable` module.
+"""
+
+from ipalib import plugable, exceptions
+
+
+def test_Registrar():
+ class Base1(object):
+ pass
+ class Base2(object):
+ pass
+ class Base3(object):
+ pass
+ class plugin1(Base1):
+ pass
+ class plugin2(Base2):
+ pass
+ class plugin3(Base3):
+ pass
+
+ # Test creation of Registrar:
+ r = plugable.Registrar(Base1, Base2)
+ assert sorted(r) == ['Base1', 'Base2']
+
+ # Check that TypeError is raised trying to register something that isn't
+ # a class:
+ raised = False
+ try:
+ r(plugin1())
+ except TypeError:
+ raised = True
+ assert raised
+
+ # Check that SubclassError is raised trying to register a class that is
+ # not a subclass of an allowed base:
+ raised = False
+ try:
+ r(plugin3)
+ except exceptions.SubclassError:
+ raised = True
+ assert raised
+
+ # Check that registration works
+ r(plugin1)
+ sub_d = r['Base1']
+ assert len(sub_d) == 1
+ assert sub_d['plugin1'] is plugin1
+ # Check that a copy is returned
+ assert sub_d is not r['Base1']
+ assert sub_d == r['Base1']
+
+ # Check that DuplicateError is raised trying to register exact class
+ # again:
+ raised = False
+ try:
+ r(plugin1)
+ except exceptions.DuplicateError:
+ raised = True
+ assert raised
+
+ # Check that OverrideError is raised trying to register class with same
+ # name and same base:
+ orig1 = plugin1
+ class base1_extended(Base1):
+ pass
+ class plugin1(base1_extended):
+ pass
+ raised = False
+ try:
+ r(plugin1)
+ except exceptions.OverrideError:
+ raised = True
+ assert raised
+
+ # Check that overriding works
+ r(plugin1, override=True)
+ sub_d = r['Base1']
+ assert len(sub_d) == 1
+ assert sub_d['plugin1'] is plugin1
+ assert sub_d['plugin1'] is not orig1
+
+ # Check that MissingOverrideError is raised trying to override a name
+ # not yet registerd:
+ raised = False
+ try:
+ r(plugin2, override=True)
+ except exceptions.MissingOverrideError:
+ raised = True
+ assert raised
+
+ # Check that additional plugin can be registered:
+ r(plugin2)
+ sub_d = r['Base2']
+ assert len(sub_d) == 1
+ assert sub_d['plugin2'] is plugin2