diff options
-rw-r--r-- | ipalib/exceptions.py | 72 | ||||
-rw-r--r-- | ipalib/plugable.py | 95 | ||||
-rw-r--r-- | ipalib/tests/test_plugable.py | 115 |
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 |