import textwrap
import types
from bonobo.util.inspect import istype
[docs]class Option:
"""
An Option is a descriptor for Configurable's parameters.
.. attribute:: type
Option type allows to provide a callable used to cast, clean or validate the option value. If not provided, or
None, the option's value will be the exact value user provided.
(default: None)
.. attribute:: required
If an option is required, an error will be raised if no value is provided (at runtime). If it is not, option
will have the default value if user does not override it at runtime.
Ignored if a default is provided, meaning that the option cannot be required.
(default: True)
.. attribute:: positional
If this is true, it'll be possible to provide the option value as a positional argument. Otherwise, it must
be provided as a keyword argument.
(default: False)
.. attribute:: default
Default value for non-required options.
(default: None)
Example:
.. code-block:: python
from bonobo.config import Configurable, Option
class Example(Configurable):
title = Option(str, required=True, positional=True)
keyword = Option(str, default='foo')
def __call__(self, s):
return self.title + ': ' + s + ' (' + self.keyword + ')'
example = Example('hello', keyword='bar')
"""
_creation_counter = 0
def __init__(self, type=None, *, required=True, positional=False, default=None, __doc__=None):
self.name = None
self.type = type
self.required = required if default is None else False
self.positional = positional
self.default = default
# Docstring formating
self.__doc__ = __doc__ or None
if self.__doc__:
self.__doc__ = textwrap.dedent(self.__doc__.strip('\n')).strip()
if default:
self.__doc__ += '\n\nDefault: {!r}'.format(default)
# This hack is necessary for python3.5
self._creation_counter = Option._creation_counter
Option._creation_counter += 1
def __get__(self, inst, type_):
# XXX If we call this on the type, then either return overriden value or ... ???
if inst is None:
return vars(type_).get(self.name, self)
if not self.name in inst._options_values:
inst._options_values[self.name] = self.get_default()
return inst._options_values[self.name]
def __set__(self, inst, value):
inst._options_values[self.name] = self.clean(value)
def __repr__(self):
return '<{positional}{typename} {type}{name} default={default!r}{required}>'.format(
typename=type(self).__name__,
type='({})'.format(self.type) if istype(self.type) else '',
name=self.name,
positional='*' if self.positional else '**',
default=self.default,
required=' (required)' if self.required else '',
)
[docs] def clean(self, value):
return self.type(value) if self.type else value
[docs] def get_default(self):
return self.default() if callable(self.default) else self.default
class RemovedOption(Option):
def __init__(self, *args, value, **kwargs):
kwargs['required'] = False
super(RemovedOption, self).__init__(*args, **kwargs)
self.value = value
def clean(self, value):
if value != self.value:
raise ValueError(
'Removed options cannot change value, {!r} must now be {!r} (and you should remove setting the value explicitely, as it is deprecated and will be removed quite soon.'.format(
self.name, self.value
)
)
return self.value
def get_default(self):
return self.value
class RenamedOption(Option):
def __init__(self, target, *, positional=False):
super(RenamedOption, self).__init__(required=False, positional=False)
self.target = target
def __get__(self, instance, owner):
raise ValueError(
'Trying to get value from renamed option {}, try getting {} instead.'.format(self.name, self.target)
)
def clean(self, value):
raise ValueError(
'Trying to set value of renamed option {}, try setting {} instead.'.format(self.name, self.target)
)
[docs]class Method(Option):
"""
A Method is a special callable-valued option, that can be used in three different ways (but for same purpose).
* Like a normal option, the value can be provided to the Configurable constructor.
>>> from bonobo.config import Configurable, Method
>>> class MethodExample(Configurable):
... handler = Method()
>>> example1 = MethodExample(handler=str.upper)
* It can be used by a child class that overrides the Method with a normal method.
>>> class ChildMethodExample(MethodExample):
... def handler(self, s: str):
... return s.upper()
>>> example2 = ChildMethodExample()
* Finally, it also enables the class to be used as a decorator, to generate a subclass providing the Method a value.
>>> @MethodExample
... def OtherChildMethodExample(s):
... return s.upper()
>>> example3 = OtherChildMethodExample()
It's possible to pass a default implementation to a Method by calling it, making it suitable to use as a decorator.
>>> class MethodExampleWithDefault(Configurable):
... @Method()
... def handler(self):
... pass
"""
def __init__(self, *, default=None, required=True, positional=True, __doc__=None):
super().__init__(None, required=required, positional=positional, __doc__=__doc__)
# If a callable is provided as default, then use self as if it was used as a decorator
if default is not None:
if not callable(default):
raise ValueError('Method defaults should be callable, if provided.')
self(default)
def __get__(self, inst, type_):
x = super(Method, self).__get__(inst, type_)
if inst:
x = types.MethodType(x, inst)
return x
def __set__(self, inst, value):
if not callable(value):
raise TypeError(
'Option {!r} ({}) is expecting a callable value, got {!r} object: {!r}.'.format(
self.name, type(self).__name__, type(value).__name__, value
)
)
inst._options_values[self.name] = self.type(value) if self.type else value
def __call__(self, impl):
if self.default:
raise RuntimeError('Can only be used once as a decorator.')
self.default = impl
self.required = False
return self
[docs] def get_default(self):
return self.default