mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-22 23:23:30 -06:00
b8480549c6
https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/useless-object-inheritance.html: > Used when a class inherit from object, which under python3 is implicit, hence can be safely removed from bases. Fixes: https://pagure.io/freeipa/issue/9278 Signed-off-by: Stanislav Levin <slev@altlinux.org> Reviewed-By: Stanislav Levin <slev@altlinux.org>
673 lines
18 KiB
Python
673 lines
18 KiB
Python
#
|
|
# Copyright (C) 2015 FreeIPA Contributors see COPYING for license
|
|
#
|
|
|
|
"""
|
|
The framework core.
|
|
"""
|
|
|
|
import abc
|
|
import collections
|
|
import functools
|
|
import itertools
|
|
import sys
|
|
|
|
import six
|
|
|
|
from . import util
|
|
from .util import from_
|
|
|
|
__all__ = ['InvalidStateError', 'KnobValueError', 'Property', 'knob',
|
|
'Configurable', 'group', 'Component', 'Composite']
|
|
|
|
NoneType = type(None)
|
|
builtin_type = type
|
|
|
|
# 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()
|
|
|
|
|
|
@functools.cmp_to_key
|
|
def _class_key(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 PropertyBase(metaclass=util.InnerClassMeta):
|
|
# shut up pylint
|
|
__outer_class__ = None
|
|
__outer_name__ = None
|
|
|
|
_order = None
|
|
|
|
@property
|
|
def default(self):
|
|
raise AttributeError('default')
|
|
|
|
def __init__(self, outer):
|
|
pass
|
|
|
|
def __get__(self, obj, obj_type):
|
|
while obj is not None:
|
|
try:
|
|
return obj.__dict__[self.__outer_name__]
|
|
except KeyError:
|
|
pass
|
|
obj = obj._get_fallback()
|
|
|
|
try:
|
|
return self.default
|
|
except AttributeError:
|
|
raise AttributeError(self.__outer_name__)
|
|
|
|
def __set__(self, obj, value):
|
|
try:
|
|
obj.__dict__[self.__outer_name__] = value
|
|
except KeyError:
|
|
raise AttributeError(self.__outer_name__)
|
|
|
|
def __delete__(self, obj):
|
|
try:
|
|
del obj.__dict__[self.__outer_name__]
|
|
except KeyError:
|
|
raise AttributeError(self.__outer_name__)
|
|
|
|
|
|
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
|
|
sensitive = False
|
|
deprecated = False
|
|
description = None
|
|
cli_names = (None,)
|
|
cli_deprecated_names = ()
|
|
cli_metavar = None
|
|
|
|
def __init__(self, outer):
|
|
self.outer = outer
|
|
|
|
def validate(self, value):
|
|
pass
|
|
|
|
@classmethod
|
|
def group(cls):
|
|
return cls.__outer_class__.group()
|
|
|
|
@classmethod
|
|
def is_cli_positional(cls):
|
|
return all(n is not None and not n.startswith('-')
|
|
for n in cls.cli_names)
|
|
|
|
@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=_missing, default=_missing, bases=_missing, _order=_missing,
|
|
sensitive=_missing, deprecated=_missing, description=_missing,
|
|
group=_missing, cli_names=_missing, cli_deprecated_names=_missing,
|
|
cli_metavar=_missing):
|
|
if type is None:
|
|
type = NoneType
|
|
|
|
if bases is _missing:
|
|
bases = (KnobBase,)
|
|
elif isinstance(bases, builtin_type):
|
|
bases = (bases,)
|
|
|
|
if cli_names is None or isinstance(cli_names, str):
|
|
cli_names = (cli_names,)
|
|
elif cli_names is not _missing:
|
|
cli_names = tuple(cli_names)
|
|
|
|
if isinstance(cli_deprecated_names, str):
|
|
cli_deprecated_names = (cli_deprecated_names,)
|
|
elif cli_deprecated_names is not _missing:
|
|
cli_deprecated_names = tuple(cli_deprecated_names)
|
|
|
|
class_dict = {}
|
|
if type is not _missing:
|
|
class_dict['type'] = type
|
|
if default is not _missing:
|
|
class_dict['default'] = default
|
|
if _order is not _missing:
|
|
class_dict['_order'] = _order
|
|
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 group is not _missing:
|
|
class_dict['group'] = group
|
|
if cli_names is not _missing:
|
|
class_dict['cli_names'] = cli_names
|
|
if cli_deprecated_names is not _missing:
|
|
class_dict['cli_deprecated_names'] = cli_deprecated_names
|
|
if cli_metavar is not _missing:
|
|
class_dict['cli_metavar'] = cli_metavar
|
|
|
|
return util.InnerClassMeta('Knob', bases, class_dict)
|
|
|
|
|
|
def knob(type, default=_missing, **kwargs):
|
|
"""
|
|
Define a new knob.
|
|
"""
|
|
return _knob(
|
|
type, default,
|
|
_order=next(_counter),
|
|
**kwargs
|
|
)
|
|
|
|
|
|
def extend_knob(base, default=_missing, bases=_missing, group=_missing,
|
|
**kwargs):
|
|
"""
|
|
Extend an existing knob.
|
|
"""
|
|
if bases is _missing:
|
|
bases = (base,)
|
|
|
|
if group is _missing:
|
|
group = staticmethod(base.group)
|
|
|
|
return _knob(
|
|
_missing, default,
|
|
bases=bases,
|
|
_order=_missing,
|
|
group=group,
|
|
**kwargs
|
|
)
|
|
|
|
|
|
class Configurable(metaclass=abc.ABCMeta):
|
|
"""
|
|
Base class of all configurables.
|
|
|
|
FIXME: details of validate/execute, properties and knobs
|
|
"""
|
|
|
|
@classmethod
|
|
def properties(cls):
|
|
"""
|
|
Iterate over properties defined for the configurable.
|
|
"""
|
|
|
|
assert not hasattr(super(Configurable, cls), 'properties')
|
|
|
|
seen = set()
|
|
|
|
for owner_cls in cls.__mro__:
|
|
result = []
|
|
|
|
for name, prop_cls in owner_cls.__dict__.items():
|
|
if name in seen:
|
|
continue
|
|
seen.add(name)
|
|
|
|
if not isinstance(prop_cls, type):
|
|
continue
|
|
if not issubclass(prop_cls, PropertyBase):
|
|
continue
|
|
|
|
result.append((prop_cls._order, owner_cls, name))
|
|
|
|
result = sorted(result, key=lambda r: r[0])
|
|
|
|
for _order, owner_cls, name in result:
|
|
yield owner_cls, name
|
|
|
|
@classmethod
|
|
def knobs(cls):
|
|
for owner_cls, name in cls.properties():
|
|
prop_cls = getattr(owner_cls, name)
|
|
if issubclass(prop_cls, KnobBase):
|
|
yield owner_cls, name
|
|
|
|
@classmethod
|
|
def group(cls):
|
|
assert not hasattr(super(Configurable, cls), 'group')
|
|
|
|
def __init__(self, **kwargs):
|
|
"""
|
|
Initialize the configurable.
|
|
"""
|
|
|
|
cls = self.__class__
|
|
for owner_cls, name in cls.properties():
|
|
if name.startswith('_'):
|
|
continue
|
|
prop_cls = getattr(owner_cls, name)
|
|
if not isinstance(prop_cls, type):
|
|
continue
|
|
if not issubclass(prop_cls, PropertyBase):
|
|
continue
|
|
|
|
try:
|
|
value = kwargs.pop(name)
|
|
except KeyError:
|
|
pass
|
|
else:
|
|
setattr(self, name, value)
|
|
|
|
for owner_cls, name in cls.knobs():
|
|
if name.startswith('_'):
|
|
continue
|
|
if not isinstance(self, owner_cls):
|
|
continue
|
|
value = getattr(self, name, None)
|
|
if value is None:
|
|
continue
|
|
|
|
prop_cls = getattr(owner_cls, name)
|
|
prop = prop_cls(self)
|
|
try:
|
|
prop.validate(value)
|
|
except ValueError as e:
|
|
raise KnobValueError(name, str(e))
|
|
|
|
if kwargs:
|
|
extra = sorted(kwargs)
|
|
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_fallback(self):
|
|
pass
|
|
|
|
@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:
|
|
return self.execute()
|
|
return None
|
|
|
|
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,
|
|
self._handle_validate_exception)
|
|
|
|
def execute(self):
|
|
"""
|
|
Run the execution part of the configurable.
|
|
"""
|
|
return_value = 0
|
|
|
|
for rval in self._executor():
|
|
if rval is not None and rval > return_value:
|
|
return_value = rval
|
|
|
|
return return_value
|
|
|
|
def _executor(self):
|
|
"""
|
|
Coroutine which runs the execution part of the configurable.
|
|
"""
|
|
|
|
return self.__runner(_EXECUTE_PENDING,
|
|
_EXECUTE_RUNNING,
|
|
self._handle_execute_exception)
|
|
|
|
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 next(gen)
|
|
except StopIteration:
|
|
break
|
|
|
|
def __runner(self, pending_state, running_state, exc_handler):
|
|
self.__transition(pending_state, running_state)
|
|
|
|
def step_next():
|
|
return next(self.__gen)
|
|
|
|
step = step_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:
|
|
exc_handler(exc_info)
|
|
except BaseException:
|
|
self.__transition(running_state, _FAILED)
|
|
raise
|
|
|
|
if self.__state != running_state:
|
|
break
|
|
|
|
try:
|
|
yield
|
|
except BaseException:
|
|
exc_info = sys.exc_info()
|
|
|
|
def step_throw():
|
|
return self.__gen.throw(*exc_info)
|
|
|
|
step = step_throw
|
|
else:
|
|
step = step_next
|
|
|
|
def _handle_exception(self, exc_info):
|
|
assert not hasattr(super(Configurable, self), '_handle_exception')
|
|
|
|
six.reraise(*exc_info)
|
|
|
|
def _handle_validate_exception(self, exc_info):
|
|
assert not hasattr(super(Configurable, self),
|
|
'_handle_validate_exception')
|
|
self._handle_exception(exc_info)
|
|
|
|
def _handle_execute_exception(self, exc_info):
|
|
assert not hasattr(super(Configurable, self),
|
|
'_handle_execute_exception')
|
|
self._handle_exception(exc_info)
|
|
|
|
def __transition(self, from_state, to_state):
|
|
if self.__state != from_state:
|
|
raise InvalidStateError(self.__state)
|
|
|
|
self.__state = to_state
|
|
|
|
|
|
def group(cls):
|
|
def group():
|
|
return cls
|
|
|
|
cls.group = staticmethod(group)
|
|
|
|
return cls
|
|
|
|
|
|
class ComponentMeta(util.InnerClassMeta, abc.ABCMeta):
|
|
pass
|
|
|
|
|
|
class ComponentBase(Configurable, metaclass=ComponentMeta):
|
|
# shut up pylint
|
|
__outer_class__ = None
|
|
__outer_name__ = None
|
|
|
|
_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_fallback(self):
|
|
return self.__parent
|
|
|
|
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 properties(cls):
|
|
name_dict = {}
|
|
owner_dict = collections.OrderedDict()
|
|
|
|
for owner_cls, name in super(Composite, cls).properties():
|
|
name_dict[name] = owner_cls
|
|
owner_dict.setdefault(owner_cls, []).append(name)
|
|
|
|
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
|
|
|
|
try:
|
|
last_owner_cls = name_dict[name]
|
|
except KeyError:
|
|
name_dict[name] = owner_cls
|
|
owner_dict.setdefault(owner_cls, []).append(name)
|
|
else:
|
|
knob_cls = getattr(owner_cls, name)
|
|
last_knob_cls = getattr(last_owner_cls, name)
|
|
if issubclass(knob_cls, last_knob_cls):
|
|
name_dict[name] = owner_cls
|
|
owner_dict[last_owner_cls].remove(name)
|
|
owner_dict.setdefault(owner_cls, [])
|
|
if name not in owner_dict[owner_cls]:
|
|
owner_dict[owner_cls].append(name)
|
|
elif not issubclass(last_knob_cls, knob_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, key=_class_key):
|
|
for name in owner_dict[owner_cls]:
|
|
yield owner_cls, name
|
|
|
|
@classmethod
|
|
def components(cls):
|
|
assert not hasattr(super(Composite, cls), 'components')
|
|
|
|
seen = set()
|
|
|
|
for owner_cls in cls.__mro__:
|
|
result = []
|
|
|
|
for name, comp_cls in owner_cls.__dict__.items():
|
|
if name in seen:
|
|
continue
|
|
seen.add(name)
|
|
|
|
if not isinstance(comp_cls, type):
|
|
continue
|
|
if not issubclass(comp_cls, ComponentBase):
|
|
continue
|
|
|
|
result.append((comp_cls._order, owner_cls, name))
|
|
|
|
result = sorted(result, key=lambda r: r[0])
|
|
|
|
for _order, owner_cls, name in result:
|
|
yield owner_cls, name
|
|
|
|
def __getattr__(self, name):
|
|
for owner_cls, knob_name in self.knobs():
|
|
if knob_name == name:
|
|
break
|
|
else:
|
|
raise AttributeError(name)
|
|
|
|
for component in self.__components:
|
|
if isinstance(component, owner_cls):
|
|
break
|
|
else:
|
|
raise AttributeError(name)
|
|
|
|
return getattr(component, 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:
|
|
next(validator)
|
|
except StopIteration:
|
|
pass
|
|
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
|
|
if not c.done()]
|
|
while True:
|
|
new_execute = []
|
|
for child, executor in execute:
|
|
try:
|
|
next(executor)
|
|
except StopIteration:
|
|
pass
|
|
else:
|
|
new_execute.append((child, executor))
|
|
if not new_execute:
|
|
break
|
|
execute = new_execute
|
|
|
|
yield
|