mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
ipalib: Allow multiple API instances
Merged the Registrar class into the Registry class. Plugins are now registered globally instead of in ipalib.api and are instantiated per-API instance. Different set of plugin base classes can be used in each API instance. https://fedorahosted.org/freeipa/ticket/3090 Reviewed-By: Tomas Babej <tbabej@redhat.com>
This commit is contained in:
committed by
Tomas Babej
parent
aa745b31d3
commit
2db741e847
@@ -27,7 +27,10 @@ import os
|
||||
from errors import PublicError, InternalError, CommandError
|
||||
from request import context, Connection, destroy_context
|
||||
|
||||
register = plugable.Registry()
|
||||
|
||||
|
||||
@register.base()
|
||||
class Backend(plugable.Plugin):
|
||||
"""
|
||||
Base class for all backend plugins.
|
||||
|
||||
@@ -27,7 +27,7 @@ from distutils import version
|
||||
from ipapython.version import API_VERSION
|
||||
from ipapython.ipa_log_manager import root_logger
|
||||
from base import NameSpace
|
||||
from plugable import Plugin, is_production_mode
|
||||
from plugable import Plugin, Registry, is_production_mode
|
||||
from parameters import create_param, Param, Str, Flag, Password
|
||||
from output import Output, Entry, ListOfEntries
|
||||
from text import _
|
||||
@@ -40,6 +40,9 @@ from textwrap import wrap
|
||||
|
||||
RULE_FLAG = 'validation_rule'
|
||||
|
||||
register = Registry()
|
||||
|
||||
|
||||
def rule(obj):
|
||||
assert not hasattr(obj, RULE_FLAG)
|
||||
setattr(obj, RULE_FLAG, True)
|
||||
@@ -366,6 +369,7 @@ class HasParam(Plugin):
|
||||
setattr(self, name, namespace)
|
||||
|
||||
|
||||
@register.base()
|
||||
class Command(HasParam):
|
||||
"""
|
||||
A public IPA atomic operation.
|
||||
@@ -1120,6 +1124,7 @@ class Local(Command):
|
||||
return self.forward(*args, **options)
|
||||
|
||||
|
||||
@register.base()
|
||||
class Object(HasParam):
|
||||
finalize_early = False
|
||||
|
||||
@@ -1278,6 +1283,7 @@ class Attribute(Plugin):
|
||||
super(Attribute, self)._on_finalize()
|
||||
|
||||
|
||||
@register.base()
|
||||
class Method(Attribute, Command):
|
||||
"""
|
||||
A command with an associated object.
|
||||
@@ -1364,6 +1370,7 @@ class Method(Attribute, Command):
|
||||
yield param
|
||||
|
||||
|
||||
@register.base()
|
||||
class Updater(Method):
|
||||
"""
|
||||
An LDAP update with an associated object (always update).
|
||||
@@ -1423,6 +1430,7 @@ class _AdviceOutput(object):
|
||||
self.content.append(line)
|
||||
|
||||
|
||||
@register.base()
|
||||
class Advice(Plugin):
|
||||
"""
|
||||
Base class for advices, plugins for ipa-advise.
|
||||
|
||||
@@ -74,18 +74,94 @@ class Registry(object):
|
||||
For forward compatibility, make sure that the module-level instance of
|
||||
this object is named "register".
|
||||
"""
|
||||
# TODO: Instead of auto-loading when plugin modules are imported,
|
||||
# plugins should be stored in this object.
|
||||
# The API should examine it and load plugins explicitly.
|
||||
def __call__(self):
|
||||
from ipalib import api
|
||||
|
||||
def decorator(cls):
|
||||
api.register(cls)
|
||||
return cls
|
||||
__allowed = {}
|
||||
__registered = set()
|
||||
|
||||
def base(self):
|
||||
def decorator(base):
|
||||
if not inspect.isclass(base):
|
||||
raise TypeError('plugin base must be a class; got %r' % base)
|
||||
|
||||
if base in self.__allowed:
|
||||
raise errors.PluginDuplicateError(plugin=base)
|
||||
|
||||
self.__allowed[base] = {}
|
||||
|
||||
return base
|
||||
|
||||
return decorator
|
||||
|
||||
def __findbases(self, klass):
|
||||
"""
|
||||
Iterates through allowed bases that ``klass`` is a subclass of.
|
||||
|
||||
Raises `errors.PluginSubclassError` if ``klass`` is not a subclass of
|
||||
any allowed base.
|
||||
|
||||
:param klass: The plugin class to find bases for.
|
||||
"""
|
||||
found = False
|
||||
for (base, sub_d) in self.__allowed.iteritems():
|
||||
if issubclass(klass, base):
|
||||
found = True
|
||||
yield (base, sub_d)
|
||||
if not found:
|
||||
raise errors.PluginSubclassError(
|
||||
plugin=klass, bases=self.__allowed.keys()
|
||||
)
|
||||
|
||||
def __call__(self, override=False):
|
||||
def decorator(klass):
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
# The plugin is okay, add to __registered:
|
||||
self.__registered.add(klass)
|
||||
|
||||
return klass
|
||||
|
||||
return decorator
|
||||
|
||||
def __base_iter(self, *allowed):
|
||||
for base in allowed:
|
||||
sub_d = self.__allowed[base]
|
||||
subclasses = set(sub_d.itervalues())
|
||||
yield (base, subclasses)
|
||||
|
||||
def iter(self, *allowed):
|
||||
for base in allowed:
|
||||
if base not in self.__allowed:
|
||||
raise TypeError("unknown plugin base %r" % base)
|
||||
return self.__base_iter(*allowed)
|
||||
|
||||
|
||||
class SetProxy(ReadOnly):
|
||||
"""
|
||||
@@ -365,111 +441,28 @@ class Plugin(ReadOnly):
|
||||
)
|
||||
|
||||
|
||||
class Registrar(DictProxy):
|
||||
"""
|
||||
Collects plugin classes as they are registered.
|
||||
|
||||
The Registrar does not instantiate plugins... it only implements the
|
||||
override logic and stores the plugins in a namespace per allowed base
|
||||
class.
|
||||
|
||||
The plugins are instantiated when `API.finalize()` is called.
|
||||
"""
|
||||
def __init__(self, *allowed):
|
||||
"""
|
||||
:param allowed: Base classes from which plugins accepted by this
|
||||
Registrar must subclass.
|
||||
"""
|
||||
self.__allowed = dict((base, {}) for base in allowed)
|
||||
self.__registered = set()
|
||||
super(Registrar, self).__init__(
|
||||
dict(self.__base_iter())
|
||||
)
|
||||
|
||||
def __base_iter(self):
|
||||
for (base, sub_d) in self.__allowed.iteritems():
|
||||
if not is_production_mode(self):
|
||||
assert inspect.isclass(base)
|
||||
name = base.__name__
|
||||
if not is_production_mode(self):
|
||||
assert not hasattr(self, name)
|
||||
setattr(self, name, MagicDict(sub_d))
|
||||
yield (name, base)
|
||||
|
||||
def __findbases(self, klass):
|
||||
"""
|
||||
Iterates through allowed bases that ``klass`` is a subclass of.
|
||||
|
||||
Raises `errors.PluginSubclassError` if ``klass`` is not a subclass of
|
||||
any allowed base.
|
||||
|
||||
:param klass: The plugin class to find bases for.
|
||||
"""
|
||||
if not is_production_mode(self):
|
||||
assert inspect.isclass(klass)
|
||||
found = False
|
||||
for (base, sub_d) in self.__allowed.iteritems():
|
||||
if issubclass(klass, base):
|
||||
found = True
|
||||
yield (base, sub_d)
|
||||
if not found:
|
||||
raise errors.PluginSubclassError(
|
||||
plugin=klass, bases=self.__allowed.keys()
|
||||
)
|
||||
|
||||
def __call__(self, klass, override=False):
|
||||
"""
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
# The plugin is okay, add to __registered:
|
||||
self.__registered.add(klass)
|
||||
|
||||
|
||||
class API(DictProxy):
|
||||
"""
|
||||
Dynamic API object through which `Plugin` instances are accessed.
|
||||
"""
|
||||
|
||||
def __init__(self, *allowed):
|
||||
self.__allowed = allowed
|
||||
self.__d = dict()
|
||||
self.__done = set()
|
||||
self.register = Registrar(*allowed)
|
||||
self.__registry = Registry()
|
||||
self.env = Env()
|
||||
super(API, self).__init__(self.__d)
|
||||
|
||||
def register(self, klass, override=False):
|
||||
"""
|
||||
Register the plugin ``klass``.
|
||||
|
||||
:param klass: A subclass of `Plugin` to attempt to register.
|
||||
:param override: If true, override an already registered plugin.
|
||||
"""
|
||||
self.__registry(override)(klass)
|
||||
|
||||
def __doing(self, name):
|
||||
if name in self.__done:
|
||||
raise StandardError(
|
||||
@@ -752,11 +745,10 @@ class API(DictProxy):
|
||||
yield p.instance
|
||||
|
||||
production_mode = is_production_mode(self)
|
||||
for name in self.register:
|
||||
base = self.register[name]
|
||||
magic = getattr(self.register, name)
|
||||
for base, subclasses in self.__registry.iter(*self.__allowed):
|
||||
name = base.__name__
|
||||
namespace = NameSpace(
|
||||
plugin_iter(base, (magic[k] for k in magic))
|
||||
plugin_iter(base, subclasses)
|
||||
)
|
||||
if not production_mode:
|
||||
assert not (
|
||||
|
||||
@@ -287,9 +287,9 @@ class test_Plugin(ClassChecker):
|
||||
assert e.argv == (paths.BIN_FALSE,)
|
||||
|
||||
|
||||
def test_Registrar():
|
||||
def test_Registry():
|
||||
"""
|
||||
Test the `ipalib.plugable.Registrar` class
|
||||
Test the `ipalib.plugable.Registry` class
|
||||
"""
|
||||
class Base1(object):
|
||||
pass
|
||||
@@ -304,20 +304,47 @@ def test_Registrar():
|
||||
class plugin3(Base3):
|
||||
pass
|
||||
|
||||
# Test creation of Registrar:
|
||||
r = plugable.Registrar(Base1, Base2)
|
||||
# Test creation of Registry:
|
||||
register = plugable.Registry()
|
||||
def b(klass):
|
||||
register.base()(klass)
|
||||
def r(klass, override=False):
|
||||
register(override=override)(klass)
|
||||
|
||||
# Test __iter__:
|
||||
assert list(r) == ['Base1', 'Base2']
|
||||
# Check that TypeError is raised trying to register base that isn't
|
||||
# a class:
|
||||
p = Base1()
|
||||
e = raises(TypeError, b, p)
|
||||
assert str(e) == 'plugin base must be a class; got %r' % p
|
||||
|
||||
# Test __hasitem__, __getitem__:
|
||||
for base in [Base1, Base2]:
|
||||
name = base.__name__
|
||||
assert name in r
|
||||
assert r[name] is base
|
||||
magic = getattr(r, name)
|
||||
assert type(magic) is plugable.MagicDict
|
||||
assert len(magic) == 0
|
||||
# Check that base registration works
|
||||
b(Base1)
|
||||
i = tuple(register.iter(Base1))
|
||||
assert len(i) == 1
|
||||
assert i[0][0] is Base1
|
||||
assert not i[0][1]
|
||||
|
||||
# Check that DuplicateError is raised trying to register exact class
|
||||
# again:
|
||||
e = raises(errors.PluginDuplicateError, b, Base1)
|
||||
assert e.plugin is Base1
|
||||
|
||||
# Test that another base can be registered:
|
||||
b(Base2)
|
||||
i = tuple(register.iter(Base2))
|
||||
assert len(i) == 1
|
||||
assert i[0][0] is Base2
|
||||
assert not i[0][1]
|
||||
|
||||
# Test iter:
|
||||
i = tuple(register.iter(Base1, Base2))
|
||||
assert len(i) == 2
|
||||
assert i[0][0] is Base1
|
||||
assert not i[0][1]
|
||||
assert i[1][0] is Base2
|
||||
assert not i[1][1]
|
||||
e = raises(TypeError, register.iter, Base1, Base2, Base3)
|
||||
assert str(e) == 'unknown plugin base %r' % Base3
|
||||
|
||||
# Check that TypeError is raised trying to register something that isn't
|
||||
# a class:
|
||||
@@ -332,9 +359,10 @@ def test_Registrar():
|
||||
|
||||
# Check that registration works
|
||||
r(plugin1)
|
||||
assert len(r.Base1) == 1
|
||||
assert r.Base1['plugin1'] is plugin1
|
||||
assert r.Base1.plugin1 is plugin1
|
||||
i = tuple(register.iter(Base1))
|
||||
assert len(i) == 1
|
||||
assert i[0][0] is Base1
|
||||
assert i[0][1] == {plugin1}
|
||||
|
||||
# Check that DuplicateError is raised trying to register exact class
|
||||
# again:
|
||||
@@ -355,9 +383,10 @@ def test_Registrar():
|
||||
|
||||
# Check that overriding works
|
||||
r(plugin1, override=True)
|
||||
assert len(r.Base1) == 1
|
||||
assert r.Base1.plugin1 is plugin1
|
||||
assert r.Base1.plugin1 is not orig1
|
||||
i = tuple(register.iter(Base1))
|
||||
assert len(i) == 1
|
||||
assert i[0][0] is Base1
|
||||
assert i[0][1] == {plugin1}
|
||||
|
||||
# Check that MissingOverrideError is raised trying to override a name
|
||||
# not yet registerd:
|
||||
@@ -367,10 +396,15 @@ def test_Registrar():
|
||||
assert e.plugin is plugin2
|
||||
|
||||
# Test that another plugin can be registered:
|
||||
assert len(r.Base2) == 0
|
||||
i = tuple(register.iter(Base2))
|
||||
assert len(i) == 1
|
||||
assert i[0][0] is Base2
|
||||
assert not i[0][1]
|
||||
r(plugin2)
|
||||
assert len(r.Base2) == 1
|
||||
assert r.Base2.plugin2 is plugin2
|
||||
i = tuple(register.iter(Base2))
|
||||
assert len(i) == 1
|
||||
assert i[0][0] is Base2
|
||||
assert i[0][1] == {plugin2}
|
||||
|
||||
# Setup to test more registration:
|
||||
class plugin1a(Base1):
|
||||
@@ -389,17 +423,13 @@ def test_Registrar():
|
||||
pass
|
||||
r(plugin2b)
|
||||
|
||||
# Again test __hasitem__, __getitem__:
|
||||
for base in [Base1, Base2]:
|
||||
name = base.__name__
|
||||
assert name in r
|
||||
assert r[name] is base
|
||||
magic = getattr(r, name)
|
||||
assert len(magic) == 3
|
||||
for key in magic:
|
||||
klass = magic[key]
|
||||
assert getattr(magic, key) is klass
|
||||
assert issubclass(klass, base)
|
||||
# Again test iter:
|
||||
i = tuple(register.iter(Base1, Base2))
|
||||
assert len(i) == 2
|
||||
assert i[0][0] is Base1
|
||||
assert i[0][1] == {plugin1, plugin1a, plugin1b}
|
||||
assert i[1][0] is Base2
|
||||
assert i[1][1] == {plugin2, plugin2a, plugin2b}
|
||||
|
||||
|
||||
class test_API(ClassChecker):
|
||||
@@ -415,45 +445,46 @@ class test_API(ClassChecker):
|
||||
"""
|
||||
assert issubclass(plugable.API, plugable.ReadOnly)
|
||||
|
||||
register = plugable.Registry()
|
||||
|
||||
# Setup the test bases, create the API:
|
||||
@register.base()
|
||||
class base0(plugable.Plugin):
|
||||
def method(self, n):
|
||||
return n
|
||||
|
||||
@register.base()
|
||||
class base1(plugable.Plugin):
|
||||
def method(self, n):
|
||||
return n + 1
|
||||
|
||||
api = plugable.API(base0, base1)
|
||||
api = plugable.API([base0, base1], [])
|
||||
api.env.mode = 'unit_test'
|
||||
api.env.in_tree = True
|
||||
r = api.register
|
||||
assert isinstance(r, plugable.Registrar)
|
||||
assert read_only(api, 'register') is r
|
||||
|
||||
@register()
|
||||
class base0_plugin0(base0):
|
||||
pass
|
||||
r(base0_plugin0)
|
||||
|
||||
@register()
|
||||
class base0_plugin1(base0):
|
||||
pass
|
||||
r(base0_plugin1)
|
||||
|
||||
@register()
|
||||
class base0_plugin2(base0):
|
||||
pass
|
||||
r(base0_plugin2)
|
||||
|
||||
@register()
|
||||
class base1_plugin0(base1):
|
||||
pass
|
||||
r(base1_plugin0)
|
||||
|
||||
@register()
|
||||
class base1_plugin1(base1):
|
||||
pass
|
||||
r(base1_plugin1)
|
||||
|
||||
@register()
|
||||
class base1_plugin2(base1):
|
||||
pass
|
||||
r(base1_plugin2)
|
||||
|
||||
# Test API instance:
|
||||
assert api.isdone('bootstrap') is False
|
||||
|
||||
Reference in New Issue
Block a user