summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ipalib/backend.py3
-rw-r--r--ipalib/frontend.py10
-rw-r--r--ipalib/plugable.py204
-rw-r--r--ipatests/test_ipalib/test_plugable.py119
4 files changed, 185 insertions, 151 deletions
diff --git a/ipalib/backend.py b/ipalib/backend.py
index 210058981..4c1001d4d 100644
--- a/ipalib/backend.py
+++ b/ipalib/backend.py
@@ -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.
diff --git a/ipalib/frontend.py b/ipalib/frontend.py
index 98070b843..e82a03a2a 100644
--- a/ipalib/frontend.py
+++ b/ipalib/frontend.py
@@ -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.
diff --git a/ipalib/plugable.py b/ipalib/plugable.py
index a6504d162..aae762649 100644
--- a/ipalib/plugable.py
+++ b/ipalib/plugable.py
@@ -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 (
diff --git a/ipatests/test_ipalib/test_plugable.py b/ipatests/test_ipalib/test_plugable.py
index 6762e70e0..ad1f79fbb 100644
--- a/ipatests/test_ipalib/test_plugable.py
+++ b/ipatests/test_ipalib/test_plugable.py
@@ -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