Starting to work on #47 a.k.a the context processor mess. This is a first implementation removing all the uggly function calls, but further work must work on less things called "context", as it is bad for readability. Concerns better separated now.

This commit is contained in:
Romain Dorgueil
2017-05-01 18:27:27 +02:00
parent 32bf13c77d
commit 4154d7e3d8
12 changed files with 152 additions and 75 deletions

View File

@ -3,7 +3,7 @@ from pprint import pprint as _pprint
from colorama import Fore, Style from colorama import Fore, Style
from bonobo.config.processors import contextual from bonobo.config.processors import ContextProcessor
from bonobo.structs.bags import Bag from bonobo.structs.bags import Bag
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
@ -49,12 +49,11 @@ def Tee(f):
return wrapped return wrapped
@contextual
def count(counter, *args, **kwargs): def count(counter, *args, **kwargs):
counter += 1 counter += 1
@count.add_context_processor @ContextProcessor.decorate(count)
def _count_counter(self, context): def _count_counter(self, context):
counter = ValueHolder(0) counter = ValueHolder(0)
yield counter yield counter

View File

@ -1,3 +1,4 @@
from bonobo.config.processors import ContextProcessor
from bonobo.config.options import Option from bonobo.config.options import Option
__all__ = [ __all__ = [
@ -15,16 +16,24 @@ class ConfigurableMeta(type):
super().__init__(what, bases, dict) super().__init__(what, bases, dict)
cls.__options__ = {} cls.__options__ = {}
cls.__positional_options__ = [] cls.__positional_options__ = []
cls.__processors__ = []
for typ in cls.__mro__: for typ in cls.__mro__:
for name, value in typ.__dict__.items(): for name, value in typ.__dict__.items():
if isinstance(value, Option): if isinstance(value, Option):
if not value.name: if isinstance(value, ContextProcessor):
value.name = name cls.__processors__.append(value)
if not name in cls.__options__: else:
cls.__options__[name] = value if not value.name:
if value.positional: value.name = name
cls.__positional_options__.append(name) if not name in cls.__options__:
cls.__options__[name] = value
if value.positional:
cls.__positional_options__.append(name)
# This can be done before, more efficiently. Not so bad neither as this is only done at type() creation time
# (aka class Xxx(...) time) and there should not be hundreds of processors. Still not very elegant.
cls.__processors__ = sorted(cls.__processors__, key=lambda v: v._creation_counter)
class Configurable(metaclass=ConfigurableMeta): class Configurable(metaclass=ConfigurableMeta):

View File

@ -2,24 +2,23 @@ import functools
import types import types
from bonobo.util.compat import deprecated_alias from bonobo.util.compat import deprecated_alias, deprecated
from bonobo.config.options import Option
from bonobo.util.iterators import ensure_tuple
_CONTEXT_PROCESSORS_ATTR = '__processors__' _CONTEXT_PROCESSORS_ATTR = '__processors__'
class ContextProcessor: class ContextProcessor(Option):
_creation_counter = 0
@property @property
def __name__(self): def __name__(self):
return self.func.__name__ return self.func.__name__
def __init__(self, func): def __init__(self, func):
self.func = func self.func = func
super(ContextProcessor, self).__init__(required=False, default=self.__name__)
# This hack is necessary for python3.5 self.name = self.__name__
self._creation_counter = ContextProcessor._creation_counter
ContextProcessor._creation_counter += 1
def __repr__(self): def __repr__(self):
return repr(self.func).replace('<function', '<{}'.format(type(self).__name__)) return repr(self.func).replace('<function', '<{}'.format(type(self).__name__))
@ -27,11 +26,63 @@ class ContextProcessor:
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs) return self.func(*args, **kwargs)
@classmethod
def decorate(cls, cls_or_func):
try:
cls_or_func.__processors__
except AttributeError:
cls_or_func.__processors__ = []
def decorator(processor, cls_or_func=cls_or_func):
cls_or_func.__processors__.append(cls(processor))
return cls_or_func
return decorator
class ContextCurrifier:
"""
This is a helper to resolve processors.
"""
def __init__(self, wrapped, *initial_context):
self.wrapped = wrapped
self.context = tuple(initial_context)
self._stack = []
def setup(self, *context):
if len(self._stack):
raise RuntimeError('Cannot setup context currification twice.')
for processor in resolve_processors(self.wrapped):
_processed = processor(self.wrapped, *context, *self.context)
_append_to_context = next(_processed)
if _append_to_context is not None:
self.context += ensure_tuple(_append_to_context)
self._stack.append(_processed)
def __call__(self, *args, **kwargs):
return self.wrapped(*self.context, *args, **kwargs)
def teardown(self):
while len(self._stack):
processor = self._stack.pop()
try:
# todo yield from ? how to ?
next(processor)
except StopIteration as exc:
# This is normal, and wanted.
pass
else:
# No error ? We should have had StopIteration ...
raise RuntimeError('Context processors should not yield more than once.')
@deprecated
def add_context_processor(cls_or_func, context_processor): def add_context_processor(cls_or_func, context_processor):
getattr(cls_or_func, _CONTEXT_PROCESSORS_ATTR).append(context_processor) getattr(cls_or_func, _CONTEXT_PROCESSORS_ATTR).append(context_processor)
@deprecated
def contextual(cls_or_func): def contextual(cls_or_func):
""" """
Make sure an element has the context processors collection. Make sure an element has the context processors collection.
@ -62,11 +113,15 @@ def contextual(cls_or_func):
def resolve_processors(mixed): def resolve_processors(mixed):
if isinstance(mixed, types.FunctionType): try:
yield from getattr(mixed, _CONTEXT_PROCESSORS_ATTR, ()) yield from mixed.__processors__
except AttributeError:
# old code, deprecated usage
if isinstance(mixed, types.FunctionType):
yield from getattr(mixed, _CONTEXT_PROCESSORS_ATTR, ())
for cls in reversed((mixed if isinstance(mixed, type) else type(mixed)).__mro__): for cls in reversed((mixed if isinstance(mixed, type) else type(mixed)).__mro__):
yield from cls.__dict__.get(_CONTEXT_PROCESSORS_ATTR, ()) yield from cls.__dict__.get(_CONTEXT_PROCESSORS_ATTR, ())
return () return ()

View File

@ -2,7 +2,7 @@ import traceback
from time import sleep from time import sleep
from bonobo.config import Container from bonobo.config import Container
from bonobo.config.processors import resolve_processors from bonobo.config.processors import resolve_processors, ContextCurrifier
from bonobo.util.errors import print_error from bonobo.util.errors import print_error
from bonobo.util.iterators import ensure_tuple from bonobo.util.iterators import ensure_tuple
from bonobo.util.objects import Wrapper from bonobo.util.objects import Wrapper
@ -36,31 +36,19 @@ class LoopingExecutionContext(Wrapper):
else: else:
self.services = None self.services = None
self._started, self._stopped, self._context, self._stack = False, False, None, [] self._started, self._stopped, self._stack = False, False, None
def start(self): def start(self):
assert self.state == (False, assert self.state == (False,
False), ('{}.start() can only be called on a new node.').format(type(self).__name__) False), ('{}.start() can only be called on a new node.').format(type(self).__name__)
assert self._context is None
self._started = True self._started = True
self._stack = ContextCurrifier(self.wrapped, *self._get_initial_context())
if self.parent: try:
self._context = self.parent.services.args_for(self.wrapped) self._stack.setup(self)
elif self.services: except Exception as exc: # pylint: disable=broad-except
self._context = self.services.args_for(self.wrapped) self.handle_error(exc, traceback.format_exc())
else: raise
self._context = ()
for processor in resolve_processors(self.wrapped):
try:
_processed = processor(self.wrapped, self, *self._context)
_append_to_context = next(_processed)
if _append_to_context is not None:
self._context += ensure_tuple(_append_to_context)
except Exception as exc: # pylint: disable=broad-except
self.handle_error(exc, traceback.format_exc())
raise
self._stack.append(_processed)
def loop(self): def loop(self):
"""Generic loop. A bit boring. """ """Generic loop. A bit boring. """
@ -78,21 +66,18 @@ class LoopingExecutionContext(Wrapper):
return return
self._stopped = True self._stopped = True
if self._context is not None: try:
while len(self._stack): self._stack.teardown()
processor = self._stack.pop() except Exception as exc: # pylint: disable=broad-except
try: self.handle_error(exc, traceback.format_exc())
# todo yield from ? how to ? raise
next(processor)
except StopIteration as exc:
# This is normal, and wanted.
pass
except Exception as exc: # pylint: disable=broad-except
self.handle_error(exc, traceback.format_exc())
raise
else:
# No error ? We should have had StopIteration ...
raise RuntimeError('Context processors should not yield more than once.')
def handle_error(self, exc, trace): def handle_error(self, exc, trace):
return print_error(exc, trace, context=self.wrapped) return print_error(exc, trace, context=self.wrapped)
def _get_initial_context(self):
if self.parent:
return self.parent.services.args_for(self.wrapped)
if self.services:
return self.services.args_for(self.wrapped)
return ()

View File

@ -93,7 +93,7 @@ class NodeExecutionContext(WithStatistics, LoopingExecutionContext):
input_bag = self.get() input_bag = self.get()
# todo add timer # todo add timer
self.handle_results(input_bag, input_bag.apply(self.wrapped, *self._context)) self.handle_results(input_bag, input_bag.apply(self._stack))
def push(self, bag): def push(self, bag):
# MAKE THIS PUBLIC API FOR CONTEXT PROCESSORS !!! # MAKE THIS PUBLIC API FOR CONTEXT PROCESSORS !!!

View File

@ -13,7 +13,6 @@ def path_str(path):
return path if path.startswith('/') else '/' + path return path if path.startswith('/') else '/' + path
@contextual
class OpenDataSoftAPI(Configurable): class OpenDataSoftAPI(Configurable):
dataset = Option(str, required=True) dataset = Option(str, required=True)
endpoint = Option(str, default='{scheme}://{netloc}{path}') endpoint = Option(str, default='{scheme}://{netloc}{path}')

View File

@ -27,7 +27,6 @@ class CsvHandler(FileHandler):
headers = Option(tuple) headers = Option(tuple)
@contextual
class CsvReader(CsvHandler, FileReader): class CsvReader(CsvHandler, FileReader):
""" """
Reads a CSV and yield the values as dicts. Reads a CSV and yield the values as dicts.
@ -60,7 +59,6 @@ class CsvReader(CsvHandler, FileReader):
yield dict(zip(headers.value, row)) yield dict(zip(headers.value, row))
@contextual
class CsvWriter(CsvHandler, FileWriter): class CsvWriter(CsvHandler, FileWriter):
@ContextProcessor @ContextProcessor
def writer(self, context, fs, file, lineno): def writer(self, context, fs, file, lineno):

View File

@ -9,7 +9,6 @@ __all__ = [
] ]
@contextual
class FileHandler(Configurable): class FileHandler(Configurable):
"""Abstract component factory for file-related components. """Abstract component factory for file-related components.
@ -75,7 +74,6 @@ class FileReader(Reader):
yield line.rstrip(self.eol) yield line.rstrip(self.eol)
@contextual
class FileWriter(Writer): class FileWriter(Writer):
"""Component factory for file or file-like writers. """Component factory for file or file-like writers.

View File

@ -21,7 +21,6 @@ class JsonReader(JsonHandler, FileReader):
yield line yield line
@contextual
class JsonWriter(JsonHandler, FileWriter): class JsonWriter(JsonHandler, FileWriter):
@ContextProcessor @ContextProcessor
def envelope(self, context, fs, file, lineno): def envelope(self, context, fs, file, lineno):

27
tests/test_basics.py Normal file
View File

@ -0,0 +1,27 @@
import pprint
from unittest.mock import MagicMock
import bonobo
import pytest
from bonobo.config.processors import ContextCurrifier
def test_count():
with pytest.raises(TypeError):
bonobo.count()
context = MagicMock()
currified = ContextCurrifier(bonobo.count)
currified.setup(context)
for i in range(42):
currified()
currified.teardown()
context.send.assert_called_once()
bag = context.send.call_args[0][0]
assert isinstance(bag, bonobo.Bag)
assert 0 == len(bag.kwargs)
assert 1 == len(bag.args)
assert bag.args[0] == 42

View File

@ -1,24 +1,26 @@
from operator import attrgetter from operator import attrgetter
from bonobo.config.processors import ContextProcessor, contextual, resolve_processors from bonobo.config import Configurable
from bonobo.config.processors import ContextProcessor, resolve_processors, ContextCurrifier
@contextual class CP1(Configurable):
class CP1:
@ContextProcessor @ContextProcessor
def c(self): def c(self):
pass yield
@ContextProcessor @ContextProcessor
def a(self): def a(self):
pass yield 'this is A'
@ContextProcessor @ContextProcessor
def b(self): def b(self, a):
pass yield a.upper()[:-1] + 'b'
def __call__(self, a, b):
return a, b
@contextual
class CP2(CP1): class CP2(CP1):
@ContextProcessor @ContextProcessor
def f(self): def f(self):
@ -33,7 +35,6 @@ class CP2(CP1):
pass pass
@contextual
class CP3(CP2): class CP3(CP2):
@ContextProcessor @ContextProcessor
def c(self): def c(self):
@ -52,3 +53,11 @@ def test_inheritance_and_ordering():
assert get_all_processors_names(CP1) == ['c', 'a', 'b'] assert get_all_processors_names(CP1) == ['c', 'a', 'b']
assert get_all_processors_names(CP2) == ['c', 'a', 'b', 'f', 'e', 'd'] assert get_all_processors_names(CP2) == ['c', 'a', 'b', 'f', 'e', 'd']
assert get_all_processors_names(CP3) == ['c', 'a', 'b', 'f', 'e', 'd', 'c', 'b'] assert get_all_processors_names(CP3) == ['c', 'a', 'b', 'f', 'e', 'd', 'c', 'b']
def test_setup_teardown():
o = CP1()
stack = ContextCurrifier(o)
stack.setup()
assert o(*stack.context) == ('this is A', 'THIS IS b')
stack.teardown()

View File

@ -1,4 +1,4 @@
from bonobo.config.processors import contextual from bonobo.config.processors import ContextProcessor
from bonobo.constants import BEGIN, END from bonobo.constants import BEGIN, END
from bonobo.execution.graph import GraphExecutionContext from bonobo.execution.graph import GraphExecutionContext
from bonobo.strategies import NaiveStrategy from bonobo.strategies import NaiveStrategy
@ -13,12 +13,11 @@ def square(i: int) -> int:
return i**2 return i**2
@contextual
def push_result(results, i: int): def push_result(results, i: int):
results.append(i) results.append(i)
@push_result.__processors__.append @ContextProcessor.decorate(push_result)
def results(f, context): def results(f, context):
results = [] results = []
yield results yield results