mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
30: Added plugable module with more generic implementation of Registrar; added corresponding unit tests
This commit is contained in:
@@ -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
95
ipalib/plugable.py
Normal 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
|
||||
115
ipalib/tests/test_plugable.py
Normal file
115
ipalib/tests/test_plugable.py
Normal 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
|
||||
Reference in New Issue
Block a user