Merge branch 'dev_refactor_config' into develop
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
from bonobo.structs import Bag, Graph, Token
|
from bonobo.structs import Bag, Graph, Token
|
||||||
from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \
|
from bonobo.nodes import CsvReader, CsvWriter, FileReader, FileWriter, Filter, JsonReader, JsonWriter, Limit, \
|
||||||
PrettyPrinter, PickleWriter, PickleReader, Tee, count, identity, noop, pprint
|
PrettyPrinter, PickleWriter, PickleReader, RateLimited, Tee, count, identity, noop, pprint
|
||||||
from bonobo.strategies import create_strategy
|
from bonobo.strategies import create_strategy
|
||||||
from bonobo.util.objects import get_name
|
from bonobo.util.objects import get_name
|
||||||
|
|
||||||
@ -104,6 +104,7 @@ register_api_group(
|
|||||||
PrettyPrinter,
|
PrettyPrinter,
|
||||||
PickleReader,
|
PickleReader,
|
||||||
PickleWriter,
|
PickleWriter,
|
||||||
|
RateLimited,
|
||||||
Tee,
|
Tee,
|
||||||
count,
|
count,
|
||||||
identity,
|
identity,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ from bonobo.config.options import Method, Option
|
|||||||
from bonobo.config.processors import ContextProcessor
|
from bonobo.config.processors import ContextProcessor
|
||||||
from bonobo.config.services import Container, Exclusive, Service, requires
|
from bonobo.config.services import Container, Exclusive, Service, requires
|
||||||
|
|
||||||
# bonobo.config public programming interface
|
# Bonobo's Config API
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Configurable',
|
'Configurable',
|
||||||
'Container',
|
'Container',
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
from bonobo.config.options import Method, Option
|
from bonobo.util.inspect import isoption, iscontextprocessor
|
||||||
from bonobo.config.processors import ContextProcessor
|
from bonobo.errors import AbstractError
|
||||||
from bonobo.errors import ConfigurationError, AbstractError
|
from bonobo.util.collections import sortedlist
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Configurable',
|
'Configurable',
|
||||||
'Option',
|
'Option',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
get_creation_counter = lambda v: v._creation_counter
|
||||||
|
|
||||||
|
|
||||||
class ConfigurableMeta(type):
|
class ConfigurableMeta(type):
|
||||||
"""
|
"""
|
||||||
@ -15,36 +17,77 @@ class ConfigurableMeta(type):
|
|||||||
|
|
||||||
def __init__(cls, what, bases=None, dict=None):
|
def __init__(cls, what, bases=None, dict=None):
|
||||||
super().__init__(what, bases, dict)
|
super().__init__(what, bases, dict)
|
||||||
cls.__options__ = {}
|
|
||||||
cls.__positional_options__ = []
|
cls.__processors = sortedlist()
|
||||||
cls.__processors__ = []
|
cls.__methods = sortedlist()
|
||||||
cls.__wrappable__ = None
|
cls.__options = sortedlist()
|
||||||
|
cls.__names = set()
|
||||||
|
|
||||||
|
# cls.__kwoptions = []
|
||||||
|
|
||||||
for typ in cls.__mro__:
|
for typ in cls.__mro__:
|
||||||
for name, value in typ.__dict__.items():
|
for name, value in filter(lambda x: isoption(x[1]), typ.__dict__.items()):
|
||||||
if isinstance(value, Option):
|
if iscontextprocessor(value):
|
||||||
if isinstance(value, ContextProcessor):
|
cls.__processors.insort((value._creation_counter, value))
|
||||||
cls.__processors__.append(value)
|
continue
|
||||||
else:
|
|
||||||
if not value.name:
|
if not value.name:
|
||||||
value.name = name
|
value.name = name
|
||||||
|
|
||||||
if isinstance(value, Method):
|
if not name in cls.__names:
|
||||||
if cls.__wrappable__:
|
cls.__names.add(name)
|
||||||
raise ConfigurationError(
|
cls.__options.insort((not value.positional, value._creation_counter, name, value))
|
||||||
'Cannot define more than one "Method" option in a configurable. That may change in the future.'
|
|
||||||
)
|
|
||||||
cls.__wrappable__ = name
|
|
||||||
|
|
||||||
if not name in cls.__options__:
|
@property
|
||||||
cls.__options__[name] = value
|
def __options__(cls):
|
||||||
|
return ((name, option) for _, _, name, option in cls.__options)
|
||||||
|
|
||||||
if value.positional:
|
@property
|
||||||
cls.__positional_options__.append(name)
|
def __options_dict__(cls):
|
||||||
|
return dict(cls.__options__)
|
||||||
|
|
||||||
# This can be done before, more efficiently. Not so bad neither as this is only done at type() creation time
|
@property
|
||||||
# (aka class Xxx(...) time) and there should not be hundreds of processors. Still not very elegant.
|
def __processors__(cls):
|
||||||
cls.__processors__ = sorted(cls.__processors__, key=lambda v: v._creation_counter)
|
return (processor for _, processor in cls.__processors)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return ' '.join(('<Configurable', super(ConfigurableMeta, self).__repr__().split(' ', 1)[1],))
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import _functools
|
||||||
|
except:
|
||||||
|
import functools
|
||||||
|
|
||||||
|
PartiallyConfigured = functools.partial
|
||||||
|
else:
|
||||||
|
class PartiallyConfigured(_functools.partial):
|
||||||
|
@property # TODO XXX cache this shit
|
||||||
|
def _options_values(self):
|
||||||
|
""" Simulate option values for partially configured objects. """
|
||||||
|
try:
|
||||||
|
return self.__options_values
|
||||||
|
except AttributeError:
|
||||||
|
self.__options_values = {**self.keywords}
|
||||||
|
|
||||||
|
position = 0
|
||||||
|
|
||||||
|
for name, option in self.func.__options__:
|
||||||
|
if not option.positional:
|
||||||
|
break # no positional left
|
||||||
|
if name in self.keywords:
|
||||||
|
continue # already fulfilled
|
||||||
|
|
||||||
|
self.__options_values[name] = self.args[position] if len(self.args) >= position + 1 else None
|
||||||
|
position += 1
|
||||||
|
|
||||||
|
return self.__options_values
|
||||||
|
|
||||||
|
def __getattr__(self, item):
|
||||||
|
_dict = self.func.__options_dict__
|
||||||
|
if item in _dict:
|
||||||
|
return _dict[item].__get__(self, self.func)
|
||||||
|
return getattr(self.func, item)
|
||||||
|
|
||||||
|
|
||||||
class Configurable(metaclass=ConfigurableMeta):
|
class Configurable(metaclass=ConfigurableMeta):
|
||||||
@ -54,61 +97,108 @@ class Configurable(metaclass=ConfigurableMeta):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, _final=False, **kwargs):
|
||||||
if cls.__wrappable__ and len(args) == 1 and hasattr(args[0], '__call__'):
|
"""
|
||||||
return type(args[0].__name__, (cls, ), {cls.__wrappable__: args[0]})
|
Custom instance builder. If not all options are fulfilled, will return a :class:`PartiallyConfigured` instance
|
||||||
|
which is just a :class:`functools.partial` object that behaves like a :class:`Configurable` instance.
|
||||||
|
|
||||||
return super(Configurable, cls).__new__(cls)
|
The special `_final` argument can be used to force final instance to be created, or an error raised if options
|
||||||
|
are missing.
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
# initialize option's value dictionary, used by descriptor implementation (see Option).
|
|
||||||
self.__options_values__ = {}
|
|
||||||
|
|
||||||
|
:param args:
|
||||||
|
:param _final: bool
|
||||||
|
:param kwargs:
|
||||||
|
:return: Configurable or PartiallyConfigured
|
||||||
|
"""
|
||||||
|
options = tuple(cls.__options__)
|
||||||
# compute missing options, given the kwargs.
|
# compute missing options, given the kwargs.
|
||||||
missing = set()
|
missing = set()
|
||||||
for name, option in type(self).__options__.items():
|
for name, option in options:
|
||||||
if option.required and not option.name in kwargs:
|
if option.required and not option.name in kwargs:
|
||||||
missing.add(name)
|
missing.add(name)
|
||||||
|
|
||||||
# transform positional arguments in keyword arguments if possible.
|
# transform positional arguments in keyword arguments if possible.
|
||||||
position = 0
|
position = 0
|
||||||
for positional_option in self.__positional_options__:
|
for name, option in options:
|
||||||
if len(args) <= position:
|
if not option.positional:
|
||||||
break
|
break # option orders make all positional options first, job done.
|
||||||
kwargs[positional_option] = args[position]
|
|
||||||
position += 1
|
|
||||||
if positional_option in missing:
|
|
||||||
missing.remove(positional_option)
|
|
||||||
|
|
||||||
# complain if there are still missing options.
|
if not isoption(getattr(cls, name)):
|
||||||
if len(missing):
|
missing.remove(name)
|
||||||
raise TypeError(
|
continue
|
||||||
'{}() missing {} required option{}: {}.'.format(
|
|
||||||
type(self).__name__,
|
if len(args) <= position:
|
||||||
len(missing), 's' if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing)))
|
break # no more positional arguments given.
|
||||||
)
|
|
||||||
)
|
position += 1
|
||||||
|
if name in missing:
|
||||||
|
missing.remove(name)
|
||||||
|
|
||||||
# complain if there is more options than possible.
|
# complain if there is more options than possible.
|
||||||
extraneous = set(kwargs.keys()) - set(type(self).__options__.keys())
|
extraneous = set(kwargs.keys()) - (set(next(zip(*options))) if len(options) else set())
|
||||||
if len(extraneous):
|
if len(extraneous):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'{}() got {} unexpected option{}: {}.'.format(
|
'{}() got {} unexpected option{}: {}.'.format(
|
||||||
type(self).__name__,
|
cls.__name__,
|
||||||
len(extraneous), 's' if len(extraneous) > 1 else '', ', '.join(map(repr, sorted(extraneous)))
|
len(extraneous), 's' if len(extraneous) > 1 else '', ', '.join(map(repr, sorted(extraneous)))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# missing options? we'll return a partial instance to finish the work later, unless we're required to be
|
||||||
|
# "final".
|
||||||
|
if len(missing):
|
||||||
|
if _final:
|
||||||
|
raise TypeError(
|
||||||
|
'{}() missing {} required option{}: {}.'.format(
|
||||||
|
cls.__name__,
|
||||||
|
len(missing), 's' if len(missing) > 1 else '', ', '.join(map(repr, sorted(missing)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return PartiallyConfigured(cls, *args, **kwargs)
|
||||||
|
|
||||||
|
return super(Configurable, cls).__new__(cls)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# initialize option's value dictionary, used by descriptor implementation (see Option).
|
||||||
|
self._options_values = {
|
||||||
|
**kwargs
|
||||||
|
}
|
||||||
|
|
||||||
# set option values.
|
# set option values.
|
||||||
for name, value in kwargs.items():
|
for name, value in kwargs.items():
|
||||||
setattr(self, name, value)
|
setattr(self, name, value)
|
||||||
|
|
||||||
|
position = 0
|
||||||
|
for name, option in self.__options__:
|
||||||
|
if not option.positional:
|
||||||
|
break # option orders make all positional options first
|
||||||
|
|
||||||
|
# value was overriden? Skip.
|
||||||
|
maybe_value = getattr(type(self), name)
|
||||||
|
if not isoption(maybe_value):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(args) <= position:
|
||||||
|
break
|
||||||
|
|
||||||
|
if name in self._options_values:
|
||||||
|
raise ValueError('Already got a value for option {}'.format(name))
|
||||||
|
|
||||||
|
setattr(self, name, args[position])
|
||||||
|
position += 1
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
""" You can implement a configurable callable behaviour by implemenenting the call(...) method. Of course, it is also backward compatible with legacy __call__ override.
|
""" You can implement a configurable callable behaviour by implemenenting the call(...) method. Of course, it is also backward compatible with legacy __call__ override.
|
||||||
"""
|
"""
|
||||||
return self.call(*args, **kwargs)
|
return self.call(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __options__(self):
|
||||||
|
return type(self).__options__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def __processors__(self):
|
||||||
|
return type(self).__processors__
|
||||||
|
|
||||||
def call(self, *args, **kwargs):
|
def call(self, *args, **kwargs):
|
||||||
raise AbstractError('Not implemented.')
|
raise AbstractError('Not implemented.')
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
from bonobo.util.inspect import istype
|
||||||
|
|
||||||
|
|
||||||
class Option:
|
class Option:
|
||||||
"""
|
"""
|
||||||
An Option is a descriptor for Configurable's parameters.
|
An Option is a descriptor for Configurable's parameters.
|
||||||
@ -14,7 +17,9 @@ class Option:
|
|||||||
If an option is required, an error will be raised if no value is provided (at runtime). If it is not, option
|
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.
|
will have the default value if user does not override it at runtime.
|
||||||
|
|
||||||
(default: False)
|
Ignored if a default is provided, meaning that the option cannot be required.
|
||||||
|
|
||||||
|
(default: True)
|
||||||
|
|
||||||
.. attribute:: positional
|
.. attribute:: positional
|
||||||
|
|
||||||
@ -48,10 +53,10 @@ class Option:
|
|||||||
|
|
||||||
_creation_counter = 0
|
_creation_counter = 0
|
||||||
|
|
||||||
def __init__(self, type=None, *, required=False, positional=False, default=None):
|
def __init__(self, type=None, *, required=True, positional=False, default=None):
|
||||||
self.name = None
|
self.name = None
|
||||||
self.type = type
|
self.type = type
|
||||||
self.required = required
|
self.required = required if default is None else False
|
||||||
self.positional = positional
|
self.positional = positional
|
||||||
self.default = default
|
self.default = default
|
||||||
|
|
||||||
@ -60,12 +65,27 @@ class Option:
|
|||||||
Option._creation_counter += 1
|
Option._creation_counter += 1
|
||||||
|
|
||||||
def __get__(self, inst, typ):
|
def __get__(self, inst, typ):
|
||||||
if not self.name in inst.__options_values__:
|
# XXX If we call this on the type, then either return overriden value or ... ???
|
||||||
inst.__options_values__[self.name] = self.get_default()
|
if inst is None:
|
||||||
return inst.__options_values__[self.name]
|
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):
|
def __set__(self, inst, value):
|
||||||
inst.__options_values__[self.name] = self.clean(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 '',
|
||||||
|
)
|
||||||
|
|
||||||
def clean(self, value):
|
def clean(self, value):
|
||||||
return self.type(value) if self.type else value
|
return self.type(value) if self.type else value
|
||||||
@ -105,20 +125,18 @@ class Method(Option):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, *, required=True, positional=True):
|
||||||
super().__init__(None, required=False)
|
super().__init__(None, required=required, positional=positional)
|
||||||
|
|
||||||
def __get__(self, inst, typ):
|
|
||||||
if not self.name in inst.__options_values__:
|
|
||||||
inst.__options_values__[self.name] = getattr(inst, self.name)
|
|
||||||
return inst.__options_values__[self.name]
|
|
||||||
|
|
||||||
def __set__(self, inst, value):
|
def __set__(self, inst, value):
|
||||||
if isinstance(value, str):
|
|
||||||
raise ValueError('should be callable')
|
|
||||||
inst.__options_values__[self.name] = self.type(value) if self.type else value
|
|
||||||
|
|
||||||
def clean(self, value):
|
|
||||||
if not hasattr(value, '__call__'):
|
if not hasattr(value, '__call__'):
|
||||||
raise ValueError('{} value must be callable.'.format(type(self).__name__))
|
raise TypeError('Option of type {!r} is expecting a callable value, got {!r} object (which is not).'.format(
|
||||||
return value
|
type(self).__name__, type(value).__name__))
|
||||||
|
inst._options_values[self.name] = self.type(value) if self.type else value
|
||||||
|
|
||||||
|
def __call__(self, *args, **kwargs):
|
||||||
|
# only here to trick IDEs into thinking this is callable.
|
||||||
|
raise NotImplementedError('You cannot call the descriptor')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -74,8 +74,7 @@ class ContextCurrifier:
|
|||||||
def __init__(self, wrapped, *initial_context):
|
def __init__(self, wrapped, *initial_context):
|
||||||
self.wrapped = wrapped
|
self.wrapped = wrapped
|
||||||
self.context = tuple(initial_context)
|
self.context = tuple(initial_context)
|
||||||
self._stack = []
|
self._stack, self._stack_values = None, None
|
||||||
self._stack_values = []
|
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
yield from self.wrapped
|
yield from self.wrapped
|
||||||
@ -86,8 +85,10 @@ class ContextCurrifier:
|
|||||||
return self.wrapped(*self.context, *args, **kwargs)
|
return self.wrapped(*self.context, *args, **kwargs)
|
||||||
|
|
||||||
def setup(self, *context):
|
def setup(self, *context):
|
||||||
if len(self._stack):
|
if self._stack is not None:
|
||||||
raise RuntimeError('Cannot setup context currification twice.')
|
raise RuntimeError('Cannot setup context currification twice.')
|
||||||
|
|
||||||
|
self._stack, self._stack_values = list(), list()
|
||||||
for processor in resolve_processors(self.wrapped):
|
for processor in resolve_processors(self.wrapped):
|
||||||
_processed = processor(self.wrapped, *context, *self.context)
|
_processed = processor(self.wrapped, *context, *self.context)
|
||||||
_append_to_context = next(_processed)
|
_append_to_context = next(_processed)
|
||||||
@ -97,7 +98,7 @@ class ContextCurrifier:
|
|||||||
self._stack.append(_processed)
|
self._stack.append(_processed)
|
||||||
|
|
||||||
def teardown(self):
|
def teardown(self):
|
||||||
while len(self._stack):
|
while self._stack:
|
||||||
processor = self._stack.pop()
|
processor = self._stack.pop()
|
||||||
try:
|
try:
|
||||||
# todo yield from ? how to ?
|
# todo yield from ? how to ?
|
||||||
@ -108,6 +109,7 @@ class ContextCurrifier:
|
|||||||
else:
|
else:
|
||||||
# No error ? We should have had StopIteration ...
|
# No error ? We should have had StopIteration ...
|
||||||
raise RuntimeError('Context processors should not yield more than once.')
|
raise RuntimeError('Context processors should not yield more than once.')
|
||||||
|
self._stack, self._stack_values = None, None
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def as_contextmanager(self, *context):
|
def as_contextmanager(self, *context):
|
||||||
|
|||||||
@ -53,7 +53,7 @@ class Service(Option):
|
|||||||
super().__init__(str, required=False, default=name)
|
super().__init__(str, required=False, default=name)
|
||||||
|
|
||||||
def __set__(self, inst, value):
|
def __set__(self, inst, value):
|
||||||
inst.__options_values__[self.name] = validate_service_name(value)
|
inst._options_values[self.name] = validate_service_name(value)
|
||||||
|
|
||||||
def resolve(self, inst, services):
|
def resolve(self, inst, services):
|
||||||
try:
|
try:
|
||||||
@ -75,7 +75,7 @@ class Container(dict):
|
|||||||
|
|
||||||
def args_for(self, mixed):
|
def args_for(self, mixed):
|
||||||
try:
|
try:
|
||||||
options = mixed.__options__
|
options = dict(mixed.__options__)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
options = {}
|
options = {}
|
||||||
|
|
||||||
|
|||||||
@ -13,13 +13,13 @@ def path_str(path):
|
|||||||
|
|
||||||
|
|
||||||
class OpenDataSoftAPI(Configurable):
|
class OpenDataSoftAPI(Configurable):
|
||||||
dataset = Option(str, required=True)
|
dataset = Option(str, positional=True)
|
||||||
endpoint = Option(str, default='{scheme}://{netloc}{path}')
|
endpoint = Option(str, default='{scheme}://{netloc}{path}')
|
||||||
scheme = Option(str, default='https')
|
scheme = Option(str, default='https')
|
||||||
netloc = Option(str, default='data.opendatasoft.com')
|
netloc = Option(str, default='data.opendatasoft.com')
|
||||||
path = Option(path_str, default='/api/records/1.0/search/')
|
path = Option(path_str, default='/api/records/1.0/search/')
|
||||||
rows = Option(int, default=500)
|
rows = Option(int, default=500)
|
||||||
limit = Option(int, default=None)
|
limit = Option(int, required=False)
|
||||||
timezone = Option(str, default='Europe/Paris')
|
timezone = Option(str, default='Europe/Paris')
|
||||||
kwargs = Option(dict, default=dict)
|
kwargs = Option(dict, default=dict)
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,8 @@
|
|||||||
from bonobo.nodes.io import __all__ as _all_io
|
|
||||||
from bonobo.nodes.io import *
|
|
||||||
|
|
||||||
from bonobo.nodes.basics import __all__ as _all_basics
|
|
||||||
from bonobo.nodes.basics import *
|
from bonobo.nodes.basics import *
|
||||||
|
from bonobo.nodes.basics import __all__ as _all_basics
|
||||||
from bonobo.nodes.filter import Filter
|
from bonobo.nodes.filter import Filter
|
||||||
|
from bonobo.nodes.io import *
|
||||||
|
from bonobo.nodes.io import __all__ as _all_io
|
||||||
|
from bonobo.nodes.throttle import RateLimited
|
||||||
|
|
||||||
__all__ = _all_basics + _all_io + ['Filter']
|
__all__ = _all_basics + _all_io + ['Filter', 'RateLimited']
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
import functools
|
import functools
|
||||||
from pprint import pprint as _pprint
|
|
||||||
|
|
||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
|
|
||||||
from bonobo import settings
|
from bonobo import settings
|
||||||
from bonobo.config import Configurable, Option
|
from bonobo.config import Configurable, Option
|
||||||
from bonobo.config.processors import ContextProcessor
|
from bonobo.config.processors import ContextProcessor
|
||||||
|
from bonobo.constants import NOT_MODIFIED
|
||||||
from bonobo.structs.bags import Bag
|
from bonobo.structs.bags import Bag
|
||||||
|
from bonobo.util.compat import deprecated
|
||||||
from bonobo.util.objects import ValueHolder
|
from bonobo.util.objects import ValueHolder
|
||||||
from bonobo.util.term import CLEAR_EOL
|
from bonobo.util.term import CLEAR_EOL
|
||||||
from bonobo.constants import NOT_MODIFIED
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'identity',
|
'identity',
|
||||||
@ -87,8 +87,12 @@ class PrettyPrinter(Configurable):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
pprint = PrettyPrinter()
|
_pprint = PrettyPrinter()
|
||||||
pprint.__name__ = 'pprint'
|
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def pprint(*args, **kwargs):
|
||||||
|
return _pprint(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def PrettyPrint(title_keys=('title', 'name', 'id'), print_values=True, sort=True):
|
def PrettyPrint(title_keys=('title', 'name', 'id'), print_values=True, sort=True):
|
||||||
|
|||||||
@ -26,7 +26,7 @@ class CsvHandler(FileHandler):
|
|||||||
"""
|
"""
|
||||||
delimiter = Option(str, default=';')
|
delimiter = Option(str, default=';')
|
||||||
quotechar = Option(str, default='"')
|
quotechar = Option(str, default='"')
|
||||||
headers = Option(tuple)
|
headers = Option(tuple, required=False)
|
||||||
|
|
||||||
|
|
||||||
class CsvReader(IOFormatEnabled, FileReader, CsvHandler):
|
class CsvReader(IOFormatEnabled, FileReader, CsvHandler):
|
||||||
|
|||||||
@ -17,7 +17,7 @@ class PickleHandler(FileHandler):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
item_names = Option(tuple)
|
item_names = Option(tuple, required=False)
|
||||||
|
|
||||||
|
|
||||||
class PickleReader(IOFormatEnabled, FileReader, PickleHandler):
|
class PickleReader(IOFormatEnabled, FileReader, PickleHandler):
|
||||||
|
|||||||
55
bonobo/nodes/throttle.py
Normal file
55
bonobo/nodes/throttle.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from bonobo.config import Configurable, ContextProcessor, Method, Option
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimitBucket(threading.Thread):
|
||||||
|
daemon = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stopped(self):
|
||||||
|
return self._stop_event.is_set()
|
||||||
|
|
||||||
|
def __init__(self, initial=1, period=1, amount=1):
|
||||||
|
super(RateLimitBucket, self).__init__()
|
||||||
|
self.semaphore = threading.BoundedSemaphore(initial)
|
||||||
|
self.amount = amount
|
||||||
|
self.period = period
|
||||||
|
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._stop_event.set()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while not self.stopped:
|
||||||
|
time.sleep(self.period)
|
||||||
|
for _ in range(self.amount):
|
||||||
|
self.semaphore.release()
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
return self.semaphore.acquire()
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimited(Configurable):
|
||||||
|
handler = Method()
|
||||||
|
|
||||||
|
initial = Option(int, positional=True, default=1)
|
||||||
|
period = Option(int, positional=True, default=1)
|
||||||
|
amount = Option(int, positional=True, default=1)
|
||||||
|
|
||||||
|
@ContextProcessor
|
||||||
|
def bucket(self, context):
|
||||||
|
print(context)
|
||||||
|
bucket = RateLimitBucket(self.initial, self.amount, self.period)
|
||||||
|
bucket.start()
|
||||||
|
print(bucket)
|
||||||
|
yield bucket
|
||||||
|
bucket.stop()
|
||||||
|
bucket.join()
|
||||||
|
|
||||||
|
def call(self, bucket, *args, **kwargs):
|
||||||
|
print(bucket, args, kwargs)
|
||||||
|
bucket.wait()
|
||||||
|
return self.handler(*args, **kwargs)
|
||||||
@ -27,7 +27,7 @@ class Setting:
|
|||||||
self.validator = None
|
self.validator = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Setting {}={!r}>'.format(self.name, self.value)
|
return '<Setting {}={!r}>'.format(self.name, self.get())
|
||||||
|
|
||||||
def set(self, value):
|
def set(self, value):
|
||||||
if self.validator and not self.validator(value):
|
if self.validator and not self.validator(value):
|
||||||
|
|||||||
6
bonobo/util/collections.py
Normal file
6
bonobo/util/collections.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import bisect
|
||||||
|
|
||||||
|
|
||||||
|
class sortedlist(list):
|
||||||
|
def insort(self, x):
|
||||||
|
bisect.insort(self, x)
|
||||||
114
bonobo/util/inspect.py
Normal file
114
bonobo/util/inspect.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
|
||||||
|
def isconfigurabletype(mixed):
|
||||||
|
"""
|
||||||
|
Check if the given argument is an instance of :class:`bonobo.config.ConfigurableMeta`, meaning it has all the
|
||||||
|
plumbery necessary to build :class:`bonobo.config.Configurable`-like instances.
|
||||||
|
|
||||||
|
:param mixed:
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
from bonobo.config.configurables import ConfigurableMeta
|
||||||
|
return isinstance(mixed, ConfigurableMeta)
|
||||||
|
|
||||||
|
|
||||||
|
def isconfigurable(mixed):
|
||||||
|
"""
|
||||||
|
Check if the given argument is an instance of :class:`bonobo.config.Configurable`.
|
||||||
|
|
||||||
|
:param mixed:
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
from bonobo.config.configurables import Configurable
|
||||||
|
return isinstance(mixed, Configurable)
|
||||||
|
|
||||||
|
|
||||||
|
def isoption(mixed):
|
||||||
|
"""
|
||||||
|
Check if the given argument is an instance of :class:`bonobo.config.Option`.
|
||||||
|
|
||||||
|
:param mixed:
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
|
||||||
|
from bonobo.config.options import Option
|
||||||
|
return isinstance(mixed, Option)
|
||||||
|
|
||||||
|
|
||||||
|
def ismethod(mixed):
|
||||||
|
"""
|
||||||
|
Check if the given argument is an instance of :class:`bonobo.config.Method`.
|
||||||
|
|
||||||
|
:param mixed:
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
from bonobo.config.options import Method
|
||||||
|
return isinstance(mixed, Method)
|
||||||
|
|
||||||
|
|
||||||
|
def iscontextprocessor(x):
|
||||||
|
"""
|
||||||
|
Check if the given argument is an instance of :class:`bonobo.config.ContextProcessor`.
|
||||||
|
|
||||||
|
:param mixed:
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
from bonobo.config.processors import ContextProcessor
|
||||||
|
return isinstance(x, ContextProcessor)
|
||||||
|
|
||||||
|
|
||||||
|
def istype(mixed):
|
||||||
|
"""
|
||||||
|
Check if the given argument is a type object.
|
||||||
|
|
||||||
|
:param mixed:
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
return isinstance(mixed, type)
|
||||||
|
|
||||||
|
|
||||||
|
ConfigurableInspection = namedtuple('ConfigurableInspection',
|
||||||
|
[
|
||||||
|
'type',
|
||||||
|
'instance',
|
||||||
|
'options',
|
||||||
|
'processors',
|
||||||
|
'partial',
|
||||||
|
])
|
||||||
|
|
||||||
|
ConfigurableInspection.__enter__ = lambda self: self
|
||||||
|
ConfigurableInspection.__exit__ = lambda *exc_details: None
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_node(mixed, *, _partial=None):
|
||||||
|
"""
|
||||||
|
If the given argument is somehow a :class:`bonobo.config.Configurable` object (either a subclass, an instance, or
|
||||||
|
a partially configured instance), then it will return a :class:`ConfigurableInspection` namedtuple, used to inspect
|
||||||
|
the configurable metadata (options). If you want to get the option values, you don't need this, it is only usefull
|
||||||
|
to perform introspection on a configurable.
|
||||||
|
|
||||||
|
If it's not looking like a configurable, it will raise a :class:`TypeError`.
|
||||||
|
|
||||||
|
:param mixed:
|
||||||
|
:return: ConfigurableInspection
|
||||||
|
|
||||||
|
:raise: TypeError
|
||||||
|
"""
|
||||||
|
if isconfigurabletype(mixed):
|
||||||
|
inst, typ = None, mixed
|
||||||
|
elif isconfigurable(mixed):
|
||||||
|
inst, typ = mixed, type(mixed)
|
||||||
|
elif hasattr(mixed, 'func'):
|
||||||
|
return inspect_node(mixed.func, _partial=(mixed.args, mixed.keywords))
|
||||||
|
else:
|
||||||
|
raise TypeError(
|
||||||
|
'Not a Configurable, nor a Configurable instance and not even a partially configured Configurable. Check your inputs.')
|
||||||
|
|
||||||
|
return ConfigurableInspection(
|
||||||
|
typ,
|
||||||
|
inst,
|
||||||
|
list(typ.__options__),
|
||||||
|
list(typ.__processors__),
|
||||||
|
_partial,
|
||||||
|
)
|
||||||
@ -2,12 +2,17 @@ import pytest
|
|||||||
|
|
||||||
from bonobo.config.configurables import Configurable
|
from bonobo.config.configurables import Configurable
|
||||||
from bonobo.config.options import Option
|
from bonobo.config.options import Option
|
||||||
|
from bonobo.util.inspect import inspect_node
|
||||||
|
|
||||||
|
|
||||||
|
class NoOptConfigurable(Configurable):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class MyConfigurable(Configurable):
|
class MyConfigurable(Configurable):
|
||||||
required_str = Option(str, required=True)
|
required_str = Option(str)
|
||||||
default_str = Option(str, default='foo')
|
default_str = Option(str, default='foo')
|
||||||
integer = Option(int)
|
integer = Option(int, required=False)
|
||||||
|
|
||||||
|
|
||||||
class MyHarderConfigurable(MyConfigurable):
|
class MyHarderConfigurable(MyConfigurable):
|
||||||
@ -25,14 +30,20 @@ class MyConfigurableUsingPositionalOptions(MyConfigurable):
|
|||||||
|
|
||||||
|
|
||||||
def test_missing_required_option_error():
|
def test_missing_required_option_error():
|
||||||
|
with inspect_node(MyConfigurable()) as ni:
|
||||||
|
assert ni.partial
|
||||||
|
|
||||||
with pytest.raises(TypeError) as exc:
|
with pytest.raises(TypeError) as exc:
|
||||||
MyConfigurable()
|
MyConfigurable(_final=True)
|
||||||
assert exc.match('missing 1 required option:')
|
assert exc.match('missing 1 required option:')
|
||||||
|
|
||||||
|
|
||||||
def test_missing_required_options_error():
|
def test_missing_required_options_error():
|
||||||
|
with inspect_node(MyHarderConfigurable()) as ni:
|
||||||
|
assert ni.partial
|
||||||
|
|
||||||
with pytest.raises(TypeError) as exc:
|
with pytest.raises(TypeError) as exc:
|
||||||
MyHarderConfigurable()
|
MyHarderConfigurable(_final=True)
|
||||||
assert exc.match('missing 2 required options:')
|
assert exc.match('missing 2 required options:')
|
||||||
|
|
||||||
|
|
||||||
@ -50,6 +61,10 @@ def test_extraneous_options_error():
|
|||||||
|
|
||||||
def test_defaults():
|
def test_defaults():
|
||||||
o = MyConfigurable(required_str='hello')
|
o = MyConfigurable(required_str='hello')
|
||||||
|
|
||||||
|
with inspect_node(o) as ni:
|
||||||
|
assert not ni.partial
|
||||||
|
|
||||||
assert o.required_str == 'hello'
|
assert o.required_str == 'hello'
|
||||||
assert o.default_str == 'foo'
|
assert o.default_str == 'foo'
|
||||||
assert o.integer == None
|
assert o.integer == None
|
||||||
@ -57,6 +72,10 @@ def test_defaults():
|
|||||||
|
|
||||||
def test_str_type_factory():
|
def test_str_type_factory():
|
||||||
o = MyConfigurable(required_str=42)
|
o = MyConfigurable(required_str=42)
|
||||||
|
|
||||||
|
with inspect_node(o) as ni:
|
||||||
|
assert not ni.partial
|
||||||
|
|
||||||
assert o.required_str == '42'
|
assert o.required_str == '42'
|
||||||
assert o.default_str == 'foo'
|
assert o.default_str == 'foo'
|
||||||
assert o.integer == None
|
assert o.integer == None
|
||||||
@ -64,6 +83,10 @@ def test_str_type_factory():
|
|||||||
|
|
||||||
def test_int_type_factory():
|
def test_int_type_factory():
|
||||||
o = MyConfigurable(required_str='yo', default_str='bar', integer='42')
|
o = MyConfigurable(required_str='yo', default_str='bar', integer='42')
|
||||||
|
|
||||||
|
with inspect_node(o) as ni:
|
||||||
|
assert not ni.partial
|
||||||
|
|
||||||
assert o.required_str == 'yo'
|
assert o.required_str == 'yo'
|
||||||
assert o.default_str == 'bar'
|
assert o.default_str == 'bar'
|
||||||
assert o.integer == 42
|
assert o.integer == 42
|
||||||
@ -71,6 +94,10 @@ def test_int_type_factory():
|
|||||||
|
|
||||||
def test_bool_type_factory():
|
def test_bool_type_factory():
|
||||||
o = MyHarderConfigurable(required_str='yes', also_required='True')
|
o = MyHarderConfigurable(required_str='yes', also_required='True')
|
||||||
|
|
||||||
|
with inspect_node(o) as ni:
|
||||||
|
assert not ni.partial
|
||||||
|
|
||||||
assert o.required_str == 'yes'
|
assert o.required_str == 'yes'
|
||||||
assert o.default_str == 'foo'
|
assert o.default_str == 'foo'
|
||||||
assert o.integer == None
|
assert o.integer == None
|
||||||
@ -79,6 +106,10 @@ def test_bool_type_factory():
|
|||||||
|
|
||||||
def test_option_resolution_order():
|
def test_option_resolution_order():
|
||||||
o = MyBetterConfigurable()
|
o = MyBetterConfigurable()
|
||||||
|
|
||||||
|
with inspect_node(o) as ni:
|
||||||
|
assert not ni.partial
|
||||||
|
|
||||||
assert o.required_str == 'kaboom'
|
assert o.required_str == 'kaboom'
|
||||||
assert o.default_str == 'foo'
|
assert o.default_str == 'foo'
|
||||||
assert o.integer == None
|
assert o.integer == None
|
||||||
@ -86,3 +117,21 @@ def test_option_resolution_order():
|
|||||||
|
|
||||||
def test_option_positional():
|
def test_option_positional():
|
||||||
o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello')
|
o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello')
|
||||||
|
|
||||||
|
with inspect_node(o) as ni:
|
||||||
|
assert not ni.partial
|
||||||
|
|
||||||
|
assert o.first == '1'
|
||||||
|
assert o.second == '2'
|
||||||
|
assert o.third == '3'
|
||||||
|
assert o.required_str == 'hello'
|
||||||
|
assert o.default_str == 'foo'
|
||||||
|
assert o.integer is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_opt_configurable():
|
||||||
|
o = NoOptConfigurable()
|
||||||
|
|
||||||
|
with inspect_node(o) as ni:
|
||||||
|
assert not ni.partial
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import pytest
|
|
||||||
|
|
||||||
from bonobo.config import Configurable, Method, Option
|
from bonobo.config import Configurable, Method, Option
|
||||||
from bonobo.errors import ConfigurationError
|
from bonobo.util.inspect import inspect_node
|
||||||
|
|
||||||
|
|
||||||
class MethodBasedConfigurable(Configurable):
|
class MethodBasedConfigurable(Configurable):
|
||||||
@ -13,22 +11,56 @@ class MethodBasedConfigurable(Configurable):
|
|||||||
self.handler(*args, **kwargs)
|
self.handler(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def test_one_wrapper_only():
|
def test_multiple_wrapper_suppored():
|
||||||
with pytest.raises(ConfigurationError):
|
|
||||||
|
|
||||||
class TwoMethods(Configurable):
|
class TwoMethods(Configurable):
|
||||||
h1 = Method()
|
h1 = Method(required=True)
|
||||||
h2 = Method()
|
h2 = Method(required=True)
|
||||||
|
|
||||||
|
with inspect_node(TwoMethods) as ci:
|
||||||
|
assert ci.type == TwoMethods
|
||||||
|
assert not ci.instance
|
||||||
|
assert len(ci.options) == 2
|
||||||
|
assert not len(ci.processors)
|
||||||
|
assert not ci.partial
|
||||||
|
|
||||||
|
@TwoMethods
|
||||||
|
def OneMethod():
|
||||||
|
pass
|
||||||
|
|
||||||
|
with inspect_node(OneMethod) as ci:
|
||||||
|
assert ci.type == TwoMethods
|
||||||
|
assert not ci.instance
|
||||||
|
assert len(ci.options) == 2
|
||||||
|
assert not len(ci.processors)
|
||||||
|
assert ci.partial
|
||||||
|
|
||||||
|
@OneMethod
|
||||||
|
def transformation():
|
||||||
|
pass
|
||||||
|
|
||||||
|
with inspect_node(transformation) as ci:
|
||||||
|
assert ci.type == TwoMethods
|
||||||
|
assert ci.instance
|
||||||
|
assert len(ci.options) == 2
|
||||||
|
assert not len(ci.processors)
|
||||||
|
assert not ci.partial
|
||||||
|
|
||||||
|
|
||||||
def test_define_with_decorator():
|
def test_define_with_decorator():
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
@MethodBasedConfigurable
|
def my_handler(*args, **kwargs):
|
||||||
def Concrete(self, *args, **kwargs):
|
calls.append((args, kwargs,))
|
||||||
calls.append((args, kwargs, ))
|
|
||||||
|
Concrete = MethodBasedConfigurable(my_handler)
|
||||||
|
|
||||||
assert callable(Concrete.handler)
|
assert callable(Concrete.handler)
|
||||||
|
assert Concrete.handler == my_handler
|
||||||
|
|
||||||
|
with inspect_node(Concrete) as ci:
|
||||||
|
assert ci.type == MethodBasedConfigurable
|
||||||
|
assert ci.partial
|
||||||
|
|
||||||
t = Concrete('foo', bar='baz')
|
t = Concrete('foo', bar='baz')
|
||||||
|
|
||||||
assert callable(t.handler)
|
assert callable(t.handler)
|
||||||
@ -37,13 +69,29 @@ def test_define_with_decorator():
|
|||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_late_binding_method_decoration():
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
@MethodBasedConfigurable(foo='foo')
|
||||||
|
def Concrete(*args, **kwargs):
|
||||||
|
calls.append((args, kwargs,))
|
||||||
|
|
||||||
|
assert callable(Concrete.handler)
|
||||||
|
t = Concrete(bar='baz')
|
||||||
|
|
||||||
|
assert callable(t.handler)
|
||||||
|
assert len(calls) == 0
|
||||||
|
t()
|
||||||
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_define_with_argument():
|
def test_define_with_argument():
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
def concrete_handler(*args, **kwargs):
|
def concrete_handler(*args, **kwargs):
|
||||||
calls.append((args, kwargs, ))
|
calls.append((args, kwargs,))
|
||||||
|
|
||||||
t = MethodBasedConfigurable('foo', bar='baz', handler=concrete_handler)
|
t = MethodBasedConfigurable(concrete_handler, 'foo', bar='baz')
|
||||||
assert callable(t.handler)
|
assert callable(t.handler)
|
||||||
assert len(calls) == 0
|
assert len(calls) == 0
|
||||||
t()
|
t()
|
||||||
@ -55,7 +103,7 @@ def test_define_with_inheritance():
|
|||||||
|
|
||||||
class Inheriting(MethodBasedConfigurable):
|
class Inheriting(MethodBasedConfigurable):
|
||||||
def handler(self, *args, **kwargs):
|
def handler(self, *args, **kwargs):
|
||||||
calls.append((args, kwargs, ))
|
calls.append((args, kwargs,))
|
||||||
|
|
||||||
t = Inheriting('foo', bar='baz')
|
t = Inheriting('foo', bar='baz')
|
||||||
assert callable(t.handler)
|
assert callable(t.handler)
|
||||||
@ -71,8 +119,8 @@ def test_inheritance_then_decorate():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@Inheriting
|
@Inheriting
|
||||||
def Concrete(self, *args, **kwargs):
|
def Concrete(*args, **kwargs):
|
||||||
calls.append((args, kwargs, ))
|
calls.append((args, kwargs,))
|
||||||
|
|
||||||
assert callable(Concrete.handler)
|
assert callable(Concrete.handler)
|
||||||
t = Concrete('foo', bar='baz')
|
t = Concrete('foo', bar='baz')
|
||||||
|
|||||||
66
tests/config/test_methods_partial.py
Normal file
66
tests/config/test_methods_partial.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from bonobo.config import Configurable, ContextProcessor, Method, Option
|
||||||
|
from bonobo.util.inspect import inspect_node
|
||||||
|
|
||||||
|
|
||||||
|
class Bobby(Configurable):
|
||||||
|
handler = Method()
|
||||||
|
handler2 = Method()
|
||||||
|
foo = Option(positional=True)
|
||||||
|
bar = Option(required=False)
|
||||||
|
|
||||||
|
@ContextProcessor
|
||||||
|
def think(self, context):
|
||||||
|
yield 'different'
|
||||||
|
|
||||||
|
def call(self, think, *args, **kwargs):
|
||||||
|
self.handler('1', *args, **kwargs)
|
||||||
|
self.handler2('2', *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_partial():
|
||||||
|
C = Bobby
|
||||||
|
|
||||||
|
# inspect the configurable class
|
||||||
|
with inspect_node(C) as ci:
|
||||||
|
assert ci.type == Bobby
|
||||||
|
assert not ci.instance
|
||||||
|
assert len(ci.options) == 4
|
||||||
|
assert len(ci.processors) == 1
|
||||||
|
assert not ci.partial
|
||||||
|
|
||||||
|
# instanciate a partial instance ...
|
||||||
|
f1 = MagicMock()
|
||||||
|
C = C(f1)
|
||||||
|
|
||||||
|
with inspect_node(C) as ci:
|
||||||
|
assert ci.type == Bobby
|
||||||
|
assert not ci.instance
|
||||||
|
assert len(ci.options) == 4
|
||||||
|
assert len(ci.processors) == 1
|
||||||
|
assert ci.partial
|
||||||
|
assert ci.partial[0] == (f1,)
|
||||||
|
assert not len(ci.partial[1])
|
||||||
|
|
||||||
|
# instanciate a more complete partial instance ...
|
||||||
|
f2 = MagicMock()
|
||||||
|
C = C(f2)
|
||||||
|
|
||||||
|
with inspect_node(C) as ci:
|
||||||
|
assert ci.type == Bobby
|
||||||
|
assert not ci.instance
|
||||||
|
assert len(ci.options) == 4
|
||||||
|
assert len(ci.processors) == 1
|
||||||
|
assert ci.partial
|
||||||
|
assert ci.partial[0] == (f1, f2,)
|
||||||
|
assert not len(ci.partial[1])
|
||||||
|
|
||||||
|
c = C('foo')
|
||||||
|
|
||||||
|
with inspect_node(c) as ci:
|
||||||
|
assert ci.type == Bobby
|
||||||
|
assert ci.instance
|
||||||
|
assert len(ci.options) == 4
|
||||||
|
assert len(ci.processors) == 1
|
||||||
|
assert not ci.partial
|
||||||
@ -5,6 +5,7 @@ import pytest
|
|||||||
import bonobo
|
import bonobo
|
||||||
from bonobo.config.processors import ContextCurrifier
|
from bonobo.config.processors import ContextCurrifier
|
||||||
from bonobo.constants import NOT_MODIFIED
|
from bonobo.constants import NOT_MODIFIED
|
||||||
|
from bonobo.util.inspect import inspect_node
|
||||||
|
|
||||||
|
|
||||||
def test_count():
|
def test_count():
|
||||||
|
|||||||
Reference in New Issue
Block a user