30: Added plugable module with more generic implementation of Registrar; added corresponding unit tests

This commit is contained in:
Jason Gerard DeRose
2008-07-28 04:34:25 +00:00
parent 8b64314359
commit bc1675dc38
3 changed files with 275 additions and 7 deletions

View File

@@ -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 PrefixError(IPAError):
msg = 'class name %r must start with %r'
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 TwiceSetError(IPAError):

95
ipalib/plugable.py Normal file
View File

@@ -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

View File

@@ -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