From 9e9c01fba2938f26843a6c4f44622c86416ef525 Mon Sep 17 00:00:00 2001 From: Jan Cholasta Date: Tue, 2 Jun 2015 12:04:25 +0000 Subject: install: Introduce installer framework ipapython.install https://fedorahosted.org/freeipa/ticket/4468 Reviewed-By: Martin Basti --- ipapython/Makefile | 1 + ipapython/install/__init__.py | 7 + ipapython/install/cli.py | 255 ++++++++++++++++++++ ipapython/install/common.py | 115 +++++++++ ipapython/install/core.py | 532 ++++++++++++++++++++++++++++++++++++++++++ ipapython/install/util.py | 169 ++++++++++++++ ipapython/setup.py.in | 4 +- 7 files changed, 1082 insertions(+), 1 deletion(-) create mode 100644 ipapython/install/__init__.py create mode 100644 ipapython/install/cli.py create mode 100644 ipapython/install/common.py create mode 100644 ipapython/install/core.py create mode 100644 ipapython/install/util.py (limited to 'ipapython') diff --git a/ipapython/Makefile b/ipapython/Makefile index b2cf719fd..852764323 100644 --- a/ipapython/Makefile +++ b/ipapython/Makefile @@ -10,6 +10,7 @@ all: (cd $$subdir && $(MAKE) $@) || exit 1; \ done +.PHONY: install install: if [ "$(DESTDIR)" = "" ]; then \ python2 setup.py install; \ diff --git a/ipapython/install/__init__.py b/ipapython/install/__init__.py new file mode 100644 index 000000000..2be73027c --- /dev/null +++ b/ipapython/install/__init__.py @@ -0,0 +1,7 @@ +# +# Copyright (C) 2015 FreeIPA Contributors see COPYING for license +# + +""" +Installer framework. +""" diff --git a/ipapython/install/cli.py b/ipapython/install/cli.py new file mode 100644 index 000000000..b83fd9a2f --- /dev/null +++ b/ipapython/install/cli.py @@ -0,0 +1,255 @@ +# +# Copyright (C) 2015 FreeIPA Contributors see COPYING for license +# + +""" +Command line support. +""" + +import collections +import optparse +import signal + +from ipapython import admintool +from ipapython.ipautil import CheckedIPAddress, private_ccache + +from . import core, common + +__all__ = ['install_tool', 'uninstall_tool'] + + +def install_tool(configurable_class, command_name, log_file_name, + debug_option=False, uninstall_log_file_name=None): + if uninstall_log_file_name is not None: + uninstall_kwargs = dict( + configurable_class=configurable_class, + command_name=command_name, + log_file_name=uninstall_log_file_name, + debug_option=debug_option, + ) + else: + uninstall_kwargs = None + + return type( + 'install_tool({0})'.format(configurable_class.__name__), + (InstallTool,), + dict( + configurable_class=configurable_class, + command_name=command_name, + log_file_name=log_file_name, + debug_option=debug_option, + uninstall_kwargs=uninstall_kwargs, + ) + ) + + +def uninstall_tool(configurable_class, command_name, log_file_name, + debug_option=False): + return type( + 'uninstall_tool({0})'.format(configurable_class.__name__), + (UninstallTool,), + dict( + configurable_class=configurable_class, + command_name=command_name, + log_file_name=log_file_name, + debug_option=debug_option, + ) + ) + + +class ConfigureTool(admintool.AdminTool): + configurable_class = None + debug_option = False + + @staticmethod + def _transform(configurable_class): + raise NotImplementedError + + @classmethod + def add_options(cls, parser): + basic_group = optparse.OptionGroup(parser, "basic options") + + groups = collections.OrderedDict() + groups[None] = basic_group + + transformed_cls = cls._transform(cls.configurable_class) + for owner_cls, name in transformed_cls.knobs(): + knob_cls = getattr(owner_cls, name) + if not knob_cls.initializable: + continue + + group_cls = owner_cls.group() + try: + opt_group = groups[group_cls] + except KeyError: + opt_group = groups[group_cls] = optparse.OptionGroup( + parser, "{0} options".format(group_cls.description)) + + kwargs = dict() + if knob_cls.type is bool: + kwargs['type'] = None + elif knob_cls.type is int: + kwargs['type'] = 'int' + elif knob_cls.type is long: + kwargs['type'] = 'long' + elif knob_cls.type is float: + kwargs['type'] = 'float' + elif knob_cls.type is complex: + kwargs['type'] = 'complex' + elif isinstance(knob_cls.type, set): + kwargs['type'] = 'choice' + kwargs['choices'] = list(knob_cls.type) + else: + kwargs['type'] = 'string' + kwargs['dest'] = name + kwargs['action'] = 'callback' + kwargs['callback'] = cls._option_callback + kwargs['callback_args'] = (knob_cls,) + if knob_cls.sensitive: + kwargs['sensitive'] = True + if knob_cls.cli_metavar: + kwargs['metavar'] = knob_cls.cli_metavar + + if knob_cls.cli_short_name: + short_opt_str = '-{0}'.format(knob_cls.cli_short_name) + else: + short_opt_str = '' + cli_name = knob_cls.cli_name or name + opt_str = '--{0}'.format(cli_name.replace('_', '-')) + if not knob_cls.deprecated: + help = knob_cls.description + else: + help = optparse.SUPPRESS_HELP + opt_group.add_option( + short_opt_str, opt_str, + help=help, + **kwargs + ) + + if knob_cls.cli_aliases: + opt_group.add_option( + *knob_cls.cli_aliases, + help=optparse.SUPPRESS_HELP, + **kwargs + ) + + if issubclass(transformed_cls, common.Interactive): + basic_group.add_option( + '-U', '--unattended', + dest='unattended', + default=False, + action='store_true', + help="unattended (un)installation never prompts the user", + ) + + for group, opt_group in groups.iteritems(): + parser.add_option_group(opt_group) + + super(ConfigureTool, cls).add_options(parser, + debug_option=cls.debug_option) + + @classmethod + def _option_callback(cls, option, opt_str, value, parser, knob): + if knob.type is bool: + value_type = bool + is_list = False + value = True + else: + if isinstance(knob.type, tuple): + assert knob.type[0] is list + value_type = knob.type[1] + is_list = True + else: + value_type = knob.type + is_list = False + + if value_type == 'ip': + value_type = CheckedIPAddress + elif value_type == 'ip-local': + value_type = lambda v: CheckedIPAddress(v, match_local=True) + + try: + value = value_type(value) + except ValueError as e: + raise optparse.OptionValueError( + "option {0}: {1}".format(opt_str, e)) + + if is_list: + old_value = getattr(parser.values, option.dest) or [] + old_value.append(value) + value = old_value + + setattr(parser.values, option.dest, value) + + def validate_options(self, needs_root=True): + super(ConfigureTool, self).validate_options(needs_root=needs_root) + + def run(self): + kwargs = {} + + transformed_cls = self._transform(self.configurable_class) + for owner_cls, name in transformed_cls.knobs(): + value = getattr(self.options, name, None) + if value is not None: + kwargs[name] = value + + if (issubclass(self.configurable_class, common.Interactive) and + not self.options.unattended): + kwargs['interactive'] = True + + try: + cfgr = transformed_cls(**kwargs) + except core.KnobValueError as e: + knob_cls = getattr(transformed_cls, e.name) + cli_name = knob_cls.cli_name or e.name + opt_str = '--{0}'.format(cli_name.replace('_', '-')) + self.option_parser.error("option {0}: {1}".format(opt_str, e)) + except RuntimeError as e: + self.option_parser.error(str(e)) + + signal.signal(signal.SIGTERM, self.__signal_handler) + + # Use private ccache + with private_ccache(): + super(ConfigureTool, self).run() + + cfgr.run() + + @staticmethod + def __signal_handler(signum, frame): + raise KeyboardInterrupt + + +class InstallTool(ConfigureTool): + uninstall_kwargs = None + + _transform = staticmethod(common.installer) + + @classmethod + def add_options(cls, parser): + super(InstallTool, cls).add_options(parser) + + if cls.uninstall_kwargs is not None: + uninstall_group = optparse.OptionGroup(parser, "uninstall options") + uninstall_group.add_option( + '--uninstall', + dest='uninstall', + default=False, + action='store_true', + help=("uninstall an existing installation. The uninstall can " + "be run with --unattended option"), + ) + parser.add_option_group(uninstall_group) + + @classmethod + def get_command_class(cls, options, args): + if cls.uninstall_kwargs is not None and options.uninstall: + uninstall_cls = uninstall_tool(**cls.uninstall_kwargs) + uninstall_cls.option_parser = cls.option_parser + return uninstall_cls + else: + return super(InstallTool, cls).get_command_class(options, args) + + +class UninstallTool(ConfigureTool): + _transform = staticmethod(common.uninstaller) diff --git a/ipapython/install/common.py b/ipapython/install/common.py new file mode 100644 index 000000000..799ce5009 --- /dev/null +++ b/ipapython/install/common.py @@ -0,0 +1,115 @@ +# +# Copyright (C) 2015 FreeIPA Contributors see COPYING for license +# + +""" +Common stuff. +""" + +import traceback + +from . import core +from .util import from_ + +__all__ = ['step', 'Installable', 'Interactive', 'Continuous', 'installer', + 'uninstaller'] + + +def step(): + def decorator(func): + cls = core.Component(Step) + cls._installer = staticmethod(func) + return cls + + return decorator + + +class Installable(core.Configurable): + """ + Configurable which does install or uninstall. + """ + + uninstalling = core.Property(False) + + def _get_components(self): + components = super(Installable, self)._get_components() + if self.uninstalling: + components = reversed(list(components)) + return components + + def _configure(self): + if self.uninstalling: + return self._uninstall() + else: + return self._install() + + def _install(self): + assert not hasattr(super(Installable, self), '_install') + + return super(Installable, self)._configure() + + def _uninstall(self): + assert not hasattr(super(Installable, self), '_uninstall') + + return super(Installable, self)._configure() + + +class Step(Installable): + @property + def parent(self): + raise AttributeError('parent') + + def _install(self): + for nothing in self._installer(self.parent): + yield from_(super(Step, self)._install()) + + @staticmethod + def _installer(obj): + yield + + def _uninstall(self): + for nothing in self._uninstaller(self.parent): + yield from_(super(Step, self)._uninstall()) + + @staticmethod + def _uninstaller(obj): + yield + + @classmethod + def uninstaller(cls, func): + cls._uninstaller = staticmethod(func) + return cls + + +class Interactive(core.Configurable): + interactive = core.Property(False) + + +class Continuous(core.Configurable): + def _handle_exception(self, exc_info): + try: + super(Continuous, self)._handle_exception(exc_info) + except BaseException as e: + self.log.debug(traceback.format_exc()) + if isinstance(e, Exception): + self.log.error("%s", e) + + +def installer(cls): + class Installer(cls, Installable): + def __init__(self, **kwargs): + super(Installer, self).__init__(uninstalling=False, + **kwargs) + Installer.__name__ = 'installer({0})'.format(cls.__name__) + + return Installer + + +def uninstaller(cls): + class Uninstaller(Continuous, cls, Installable): + def __init__(self, **kwargs): + super(Uninstaller, self).__init__(uninstalling=True, + **kwargs) + Uninstaller.__name__ = 'uninstaller({0})'.format(cls.__name__) + + return Uninstaller diff --git a/ipapython/install/core.py b/ipapython/install/core.py new file mode 100644 index 000000000..c313c278e --- /dev/null +++ b/ipapython/install/core.py @@ -0,0 +1,532 @@ +# +# Copyright (C) 2015 FreeIPA Contributors see COPYING for license +# + +""" +The framework core. +""" + +import sys +import abc +import itertools + +from ipapython.ipa_log_manager import root_logger + +from . import util +from .util import from_ + +__all__ = ['InvalidStateError', 'KnobValueError', 'Property', 'Knob', + 'Configurable', 'Group', 'Component', 'Composite'] + +# Configurable states +_VALIDATE_PENDING = 'VALIDATE_PENDING' +_VALIDATE_RUNNING = 'VALIDATE_RUNNING' +_EXECUTE_PENDING = 'EXECUTE_PENDING' +_EXECUTE_RUNNING = 'EXECUTE_RUNNING' +_STOPPED = 'STOPPED' +_FAILED = 'FAILED' +_CLOSED = 'CLOSED' + +_missing = object() +_counter = itertools.count() + + +def _class_cmp(a, b): + if a is b: + return 0 + elif issubclass(a, b): + return -1 + elif issubclass(b, a): + return 1 + else: + return 0 + + +class InvalidStateError(Exception): + pass + + +class KnobValueError(ValueError): + def __init__(self, name, message): + super(KnobValueError, self).__init__(message) + self.name = name + + +class InnerClass(object): + __metaclass__ = util.InnerClassMeta + __outer_class__ = None + __outer_name__ = None + + +class PropertyBase(InnerClass): + @property + def default(self): + raise AttributeError('default') + + def __init__(self, outer): + self.outer = outer + + def __get__(self, obj, obj_type): + try: + return obj._get_property(self.__outer_name__) + except AttributeError: + if not hasattr(self, 'default'): + raise + return self.default + + +def Property(default=_missing): + class_dict = {} + if default is not _missing: + class_dict['default'] = default + + return util.InnerClassMeta('Property', (PropertyBase,), class_dict) + + +class KnobBase(PropertyBase): + type = None + initializable = True + sensitive = False + deprecated = False + description = None + cli_name = None + cli_short_name = None + cli_aliases = None + cli_metavar = None + + _order = None + + def __set__(self, obj, value): + try: + self.validate(value) + except KnobValueError: + raise + except ValueError as e: + raise KnobValueError(self.__outer_name__, str(e)) + + obj.__dict__[self.__outer_name__] = value + + def __delete__(self, obj): + try: + del obj.__dict__[self.__outer_name__] + except KeyError: + raise AttributeError(self.__outer_name__) + + def validate(self, value): + pass + + @classmethod + def default_getter(cls, func): + @property + def default(self): + return func(self.outer) + cls.default = default + + return cls + + @classmethod + def validator(cls, func): + def validate(self, value): + func(self.outer, value) + super(cls, self).validate(value) + cls.validate = validate + + return cls + + +def Knob(type, default=_missing, initializable=_missing, sensitive=_missing, + deprecated=_missing, description=_missing, cli_name=_missing, + cli_short_name=_missing, cli_aliases=_missing, cli_metavar=_missing): + class_dict = {} + class_dict['_order'] = next(_counter) + class_dict['type'] = type + if default is not _missing: + class_dict['default'] = default + if sensitive is not _missing: + class_dict['sensitive'] = sensitive + if deprecated is not _missing: + class_dict['deprecated'] = deprecated + if description is not _missing: + class_dict['description'] = description + if cli_name is not _missing: + class_dict['cli_name'] = cli_name + if cli_short_name is not _missing: + class_dict['cli_short_name'] = cli_short_name + if cli_aliases is not _missing: + class_dict['cli_aliases'] = cli_aliases + if cli_metavar is not _missing: + class_dict['cli_metavar'] = cli_metavar + + return util.InnerClassMeta('Knob', (KnobBase,), class_dict) + + +class Configurable(object): + """ + Base class of all configurables. + + FIXME: details of validate/execute, properties and knobs + """ + + __metaclass__ = abc.ABCMeta + + @classmethod + def knobs(cls): + """ + Iterate over knobs defined for the configurable. + """ + + assert not hasattr(super(Configurable, cls), 'knobs') + + result = [] + for name in dir(cls): + knob_cls = getattr(cls, name) + if isinstance(knob_cls, type) and issubclass(knob_cls, KnobBase): + result.append(knob_cls) + result = sorted(result, key=lambda knob_cls: knob_cls._order) + for knob_cls in result: + yield knob_cls.__outer_class__, knob_cls.__outer_name__ + + @classmethod + def group(cls): + assert not hasattr(super(Configurable, cls), 'group') + + return None + + def __init__(self, **kwargs): + """ + Initialize the configurable. + """ + + self.log = root_logger + + for name in dir(self.__class__): + if name.startswith('_'): + continue + property_cls = getattr(self.__class__, name) + if not isinstance(property_cls, type): + continue + if not issubclass(property_cls, PropertyBase): + continue + if issubclass(property_cls, KnobBase): + continue + try: + value = kwargs.pop(name) + except KeyError: + pass + else: + setattr(self, name, value) + + for owner_cls, name in self.knobs(): + knob_cls = getattr(owner_cls, name) + if not knob_cls.initializable: + continue + + try: + value = kwargs.pop(name) + except KeyError: + pass + else: + setattr(self, name, value) + + if kwargs: + extra = sorted(kwargs.keys()) + raise TypeError( + "{0}() got {1} unexpected keyword arguments: {2}".format( + type(self).__name__, + len(extra), + ', '.join(repr(name) for name in extra))) + + self._reset() + + def _reset(self): + assert not hasattr(super(Configurable, self), '_reset') + + self.__state = _VALIDATE_PENDING + self.__gen = util.run_generator_with_yield_from(self._configure()) + + def _get_components(self): + assert not hasattr(super(Configurable, self), '_get_components') + + raise TypeError("{0} is not composite".format(self)) + + def _get_property(self, name): + assert not hasattr(super(Configurable, self), '_get_property') + + try: + return self.__dict__[name] + except KeyError: + raise AttributeError(name) + + @abc.abstractmethod + def _configure(self): + """ + Coroutine which defines the logic of the configurable. + """ + + assert not hasattr(super(Configurable, self), '_configure') + + self.__transition(_VALIDATE_RUNNING, _EXECUTE_PENDING) + + while self.__state != _EXECUTE_RUNNING: + yield + + def run(self): + """ + Run the configurable. + """ + + self.validate() + if self.__state == _EXECUTE_PENDING: + self.execute() + + def validate(self): + """ + Run the validation part of the configurable. + """ + + for nothing in self._validator(): + pass + + def _validator(self): + """ + Coroutine which runs the validation part of the configurable. + """ + + return self.__runner(_VALIDATE_PENDING, _VALIDATE_RUNNING) + + def execute(self): + """ + Run the execution part of the configurable. + """ + + for nothing in self._executor(): + pass + + def _executor(self): + """ + Coroutine which runs the execution part of the configurable. + """ + + return self.__runner(_EXECUTE_PENDING, _EXECUTE_RUNNING) + + def done(self): + """ + Return True if the configurable has finished. + """ + + return self.__state in (_STOPPED, _FAILED, _CLOSED) + + def run_until_executing(self, gen): + while self.__state != _EXECUTE_RUNNING: + try: + yield gen.next() + except StopIteration: + break + + def __runner(self, pending_state, running_state): + self.__transition(pending_state, running_state) + + step = self.__gen.next + while True: + try: + step() + except StopIteration: + self.__transition(running_state, _STOPPED) + break + except GeneratorExit: + self.__transition(running_state, _CLOSED) + break + except BaseException: + exc_info = sys.exc_info() + try: + self._handle_exception(exc_info) + except BaseException: + raise + else: + break + finally: + self.__transition(running_state, _FAILED) + + if self.__state != running_state: + break + + try: + yield + except BaseException: + exc_info = sys.exc_info() + step = lambda: self.__gen.throw(*exc_info) + else: + step = self.__gen.next + + def _handle_exception(self, exc_info): + assert not hasattr(super(Configurable, self), '_handle_exception') + + util.raise_exc_info(exc_info) + + def __transition(self, from_state, to_state): + if self.__state != from_state: + raise InvalidStateError(self.__state) + + self.__state = to_state + + +class Group(Configurable): + @classmethod + def group(cls): + return cls + + +class ComponentMeta(util.InnerClassMeta, abc.ABCMeta): + pass + + +class ComponentBase(InnerClass, Configurable): + __metaclass__ = ComponentMeta + + _order = None + + @classmethod + def group(cls): + result = super(ComponentBase, cls).group() + if result is not None: + return result + else: + return cls.__outer_class__.group() + + def __init__(self, parent, **kwargs): + self.__parent = parent + + super(ComponentBase, self).__init__(**kwargs) + + @property + def parent(self): + return self.__parent + + def __get__(self, obj, obj_type): + obj.__dict__[self.__outer_name__] = self + return self + + def _get_property(self, name): + try: + return super(ComponentBase, self)._get_property(name) + except AttributeError: + return self.__parent._get_property(name) + + def _handle_exception(self, exc_info): + try: + super(ComponentBase, self)._handle_exception(exc_info) + except BaseException: + exc_info = sys.exc_info() + self.__parent._handle_exception(exc_info) + + +def Component(cls): + class_dict = {} + class_dict['_order'] = next(_counter) + + return ComponentMeta('Component', (ComponentBase, cls), class_dict) + + +class Composite(Configurable): + """ + Configurable composed of any number of components. + + Provides knobs of all child components. + """ + + @classmethod + def knobs(cls): + name_dict = {} + owner_dict = {} + + for owner_cls, name in super(Composite, cls).knobs(): + knob_cls = getattr(owner_cls, name) + name_dict[name] = owner_cls + owner_dict.setdefault(owner_cls, []).append(knob_cls) + + for owner_cls, name in cls.components(): + comp_cls = getattr(cls, name) + for owner_cls, name in comp_cls.knobs(): + if hasattr(cls, name): + continue + + knob_cls = getattr(owner_cls, name) + try: + last_owner_cls = name_dict[name] + except KeyError: + name_dict[name] = owner_cls + owner_dict.setdefault(owner_cls, []).append(knob_cls) + else: + if last_owner_cls is not owner_cls: + raise TypeError("{0}.knobs(): conflicting definitions " + "of '{1}' in {2} and {3}".format( + cls.__name__, + name, + last_owner_cls.__name__, + owner_cls.__name__)) + + for owner_cls in sorted(owner_dict, _class_cmp): + for knob_cls in owner_dict[owner_cls]: + yield knob_cls.__outer_class__, knob_cls.__outer_name__ + + @classmethod + def components(cls): + assert not hasattr(super(Composite, cls), 'components') + + result = [] + for name in dir(cls): + comp_cls = getattr(cls, name) + if (isinstance(comp_cls, type) and + issubclass(comp_cls, ComponentBase)): + result.append(comp_cls) + result = sorted(result, key=lambda comp_cls: comp_cls._order) + for comp_cls in result: + yield comp_cls.__outer_class__, comp_cls.__outer_name__ + + def _reset(self): + self.__components = list(self._get_components()) + + super(Composite, self)._reset() + + def _get_components(self): + for owner_cls, name in self.components(): + yield getattr(self, name) + + def _configure(self): + validate = [(c, c._validator()) for c in self.__components] + while True: + new_validate = [] + for child, validator in validate: + try: + validator.next() + except StopIteration: + if child.done(): + self.__components.remove(child) + else: + new_validate.append((child, validator)) + if not new_validate: + break + validate = new_validate + + yield + + if not self.__components: + return + + yield from_(super(Composite, self)._configure()) + + execute = [(c, c._executor()) for c in self.__components] + while True: + new_execute = [] + for child, executor in execute: + try: + executor.next() + except StopIteration: + pass + else: + new_execute.append((child, executor)) + if not new_execute: + break + execute = new_execute + + yield diff --git a/ipapython/install/util.py b/ipapython/install/util.py new file mode 100644 index 000000000..58da7bb77 --- /dev/null +++ b/ipapython/install/util.py @@ -0,0 +1,169 @@ +# +# Copyright (C) 2015 FreeIPA Contributors see COPYING for license +# + +""" +Utilities. +""" + +import sys + + +def raise_exc_info(exc_info): + """ + Raise exception from exception info tuple as returned by `sys.exc_info()`. + """ + + raise exc_info[0], exc_info[1], exc_info[2] + + +class from_(object): + """ + Wrapper for delegating to a subgenerator. + + See `run_generator_with_yield_from`. + """ + __slots__ = ('obj',) + + def __init__(self, obj): + self.obj = obj + + +def run_generator_with_yield_from(gen): + """ + Iterate over a generator object with subgenerator delegation. + + This implements Python 3's ``yield from`` expressions, using Python 2 + syntax: + + >>> def subgen(): + ... yield 'B' + ... yield 'C' + ... + >>> def gen(): + ... yield 'A' + ... yield from_(subgen()) + ... yield 'D' + ... + >>> list(run_generator_with_yield_from(gen())) + ['A', 'B', 'C', 'D'] + + Returning value from a subgenerator is not supported. + """ + + exc_info = None + value = None + + stack = [gen] + while stack: + prev_exc_info, exc_info = exc_info, None + prev_value, value = value, None + + gen = stack[-1] + try: + if prev_exc_info is None: + value = gen.send(prev_value) + else: + value = gen.throw(*prev_exc_info) + except StopIteration: + stack.pop() + continue + except BaseException: + exc_info = sys.exc_info() + stack.pop() + continue + else: + if isinstance(value, from_): + stack.append(value.obj) + value = None + continue + + try: + value = (yield value) + except BaseException: + exc_info = sys.exc_info() + + if exc_info is not None: + raise_exc_info(exc_info) + + +class InnerClassMeta(type): + def __new__(cls, name, bases, class_dict): + class_dict.pop('__outer_class__', None) + class_dict.pop('__outer_name__', None) + + return super(InnerClassMeta, cls).__new__(cls, name, bases, class_dict) + + def __get__(self, obj, obj_type): + outer_class, outer_name = self.__bind(obj_type) + if obj is None: + return self + assert isinstance(obj, outer_class) + + try: + return obj.__dict__[outer_name] + except KeyError: + inner = self(obj) + try: + getter = inner.__get__ + except AttributeError: + return inner + else: + return getter(obj, obj_type) + + def __set__(self, obj, value): + outer_class, outer_name = self.__bind(obj.__class__) + assert isinstance(obj, outer_class) + + inner = self(obj) + try: + setter = inner.__set__ + except AttributeError: + try: + inner.__delete__ + except AttributeError: + obj.__dict__[outer_name] = value + else: + raise AttributeError('__set__') + else: + setter(obj, value) + + def __delete__(self, obj): + outer_class, outer_name = self.__bind(obj.__class__) + assert isinstance(obj, outer_class) + + inner = self(obj) + try: + deleter = inner.__delete__ + except AttributeError: + try: + inner.__set__ + except AttributeError: + try: + del obj.__dict__[outer_name] + except KeyError: + raise AttributeError(outer_name) + else: + raise AttributeError('__delete__') + else: + deleter(obj) + + def __bind(self, obj_type): + try: + cls = self.__dict__['__outer_class__'] + name = self.__dict__['__outer_name__'] + except KeyError: + cls, name, value = None, None, None + for cls in obj_type.__mro__: + for name, value in cls.__dict__.iteritems(): + if value is self: + break + if value is self: + break + assert value is self + + self.__outer_class__ = cls + self.__outer_name__ = name + self.__name__ = '.'.join((cls.__name__, name)) + + return cls, name diff --git a/ipapython/setup.py.in b/ipapython/setup.py.in index 6caf17905..6cba59cfc 100644 --- a/ipapython/setup.py.in +++ b/ipapython/setup.py.in @@ -65,7 +65,9 @@ def setup_package(): classifiers=filter(None, CLASSIFIERS.split('\n')), platforms = ["Linux", "Solaris", "Unix"], package_dir = {'ipapython': ''}, - packages = [ "ipapython", "ipapython.dnssec" ], + packages = ["ipapython", + "ipapython.dnssec", + "ipapython.install"], ) finally: del sys.path[0] -- cgit