plugable: allow plugins to be non-classes

Allow registering any object that is callable and has `name` and `bases`
attributes as a plugin.

https://fedorahosted.org/freeipa/ticket/4739

Reviewed-By: David Kupka <dkupka@redhat.com>
This commit is contained in:
Jan Cholasta 2016-06-14 13:02:30 +02:00
parent 3e6af238bb
commit bebdce89b6
2 changed files with 53 additions and 18 deletions

View File

@ -26,7 +26,6 @@ http://docs.python.org/ref/sequence-types.html
""" """
import sys import sys
import inspect
import threading import threading
import os import os
from os import path from os import path
@ -40,6 +39,7 @@ import six
from ipalib import errors from ipalib import errors
from ipalib.config import Env from ipalib.config import Env
from ipalib.text import _ from ipalib.text import _
from ipalib.util import classproperty
from ipalib.base import ReadOnly, NameSpace, lock, islocked from ipalib.base import ReadOnly, NameSpace, lock, islocked
from ipalib.constants import DEFAULT_CONFIG from ipalib.constants import DEFAULT_CONFIG
from ipapython.ipa_log_manager import ( from ipapython.ipa_log_manager import (
@ -101,8 +101,8 @@ class Registry(object):
:param klass: A subclass of `Plugin` to attempt to register. :param klass: A subclass of `Plugin` to attempt to register.
""" """
if not inspect.isclass(klass): if not callable(klass):
raise TypeError('plugin must be a class; got %r' % klass) raise TypeError('plugin must be callable; got %r' % klass)
# Raise DuplicateError if this exact class was already registered: # Raise DuplicateError if this exact class was already registered:
if klass in self.__registry: if klass in self.__registry:
@ -134,9 +134,18 @@ class Plugin(ReadOnly):
self.__finalize_lock = threading.RLock() self.__finalize_lock = threading.RLock()
log_mgr.get_logger(self, True) log_mgr.get_logger(self, True)
@property @classmethod
def name(self): def __name_getter(cls):
return type(self).__name__ return cls.__name__
# you know nothing, pylint
name = classproperty(__name_getter)
@classmethod
def __bases_getter(cls):
return cls.__bases__
bases = classproperty(__bases_getter)
@property @property
def doc(self): def doc(self):
@ -571,12 +580,12 @@ class API(ReadOnly):
:param klass: A subclass of `Plugin` to attempt to add. :param klass: A subclass of `Plugin` to attempt to add.
:param override: If true, override an already added plugin. :param override: If true, override an already added plugin.
""" """
if not inspect.isclass(klass): if not callable(klass):
raise TypeError('plugin must be a class; got %r' % klass) raise TypeError('plugin must be callable; got %r' % klass)
# Find the base class or raise SubclassError: # Find the base class or raise SubclassError:
for base in self.bases: for base in klass.bases:
if issubclass(klass, self.bases): if issubclass(base, self.bases):
break break
else: else:
raise errors.PluginSubclassError( raise errors.PluginSubclassError(
@ -585,13 +594,13 @@ class API(ReadOnly):
) )
# Check override: # Check override:
prev = self.__plugins.get(klass.__name__) prev = self.__plugins.get(klass.name)
if prev: if prev:
if not override: if not override:
# Must use override=True to override: # Must use override=True to override:
raise errors.PluginOverrideError( raise errors.PluginOverrideError(
base=base.__name__, base=base.__name__,
name=klass.__name__, name=klass.name,
plugin=klass, plugin=klass,
) )
@ -601,12 +610,12 @@ class API(ReadOnly):
# There was nothing already registered to override: # There was nothing already registered to override:
raise errors.PluginMissingOverrideError( raise errors.PluginMissingOverrideError(
base=base.__name__, base=base.__name__,
name=klass.__name__, name=klass.name,
plugin=klass, plugin=klass,
) )
# The plugin is okay, add to sub_d: # The plugin is okay, add to sub_d:
self.__plugins[klass.__name__] = klass self.__plugins[klass.name] = klass
def finalize(self): def finalize(self):
""" """
@ -627,7 +636,7 @@ class API(ReadOnly):
members = [] members = []
for klass in self.__plugins.values(): for klass in self.__plugins.values():
if not issubclass(klass, base): if not any(issubclass(b, base) for b in klass.bases):
continue continue
try: try:
instance = plugins[klass] instance = plugins[klass]
@ -635,7 +644,7 @@ class API(ReadOnly):
instance = plugins[klass] = klass(self) instance = plugins[klass] = klass(self)
members.append(instance) members.append(instance)
plugin_info.setdefault( plugin_info.setdefault(
'%s.%s' % (klass.__module__, klass.__name__), '%s.%s' % (klass.__module__, klass.name),
[]).append(name) []).append(name)
if not production_mode: if not production_mode:
@ -657,8 +666,8 @@ class API(ReadOnly):
lock(self) lock(self)
def get_plugin_next(self, klass): def get_plugin_next(self, klass):
if not inspect.isclass(klass): if not callable(klass):
raise TypeError('plugin must be a class; got %r' % klass) raise TypeError('plugin must be callable; got %r' % klass)
return self.__next[klass] return self.__next[klass]

View File

@ -872,3 +872,29 @@ def detect_dns_zone_realm_type(api, domain):
def has_managed_topology(api): def has_managed_topology(api):
domainlevel = api.Command['domainlevel_get']().get('result', DOMAIN_LEVEL_0) domainlevel = api.Command['domainlevel_get']().get('result', DOMAIN_LEVEL_0)
return domainlevel > DOMAIN_LEVEL_0 return domainlevel > DOMAIN_LEVEL_0
class classproperty(object):
__slots__ = ('__doc__', 'fget')
def __init__(self, fget=None, doc=None):
if doc is None and fget is not None:
doc = fget.__doc__
self.fget = fget
self.__doc__ = doc
def __get__(self, obj, obj_type):
if self.fget is not None:
return self.fget.__get__(obj, obj_type)()
raise AttributeError("unreadable attribute")
def __set__(self, obj, value):
raise AttributeError("can't set attribute")
def __delete__(self, obj):
raise AttributeError("can't delete attribute")
def getter(self, fget):
self.fget = fget
return self