Skip to content

Commit

Permalink
Adds method based options, limited to one, to allow nodes based on a …
Browse files Browse the repository at this point in the history
…specific method (think filter, join, etc)...

Reimplementation of "Filter", with (rather simple) code from rdc.etl.
  • Loading branch information
hartym committed May 20, 2017
1 parent d5cfa02 commit cf0b982
Show file tree
Hide file tree
Showing 10 changed files with 181 additions and 8 deletions.
3 changes: 2 additions & 1 deletion bonobo/config/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from bonobo.config.configurables import Configurable
from bonobo.config.options import Option
from bonobo.config.options import Option, Method
from bonobo.config.processors import ContextProcessor
from bonobo.config.services import Container, Service

Expand All @@ -8,5 +8,6 @@
'Container',
'ContextProcessor',
'Option',
'Method',
'Service',
]
18 changes: 17 additions & 1 deletion bonobo/config/configurables.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from bonobo.config.options import Method, Option
from bonobo.config.processors import ContextProcessor
from bonobo.config.options import Option
from bonobo.errors import ConfigurationError

__all__ = [
'Configurable',
Expand All @@ -17,13 +18,18 @@ def __init__(cls, what, bases=None, dict=None):
cls.__options__ = {}
cls.__positional_options__ = []
cls.__processors__ = []
cls.__wrappable__ = None

for typ in cls.__mro__:
for name, value in typ.__dict__.items():
if isinstance(value, Option):
if isinstance(value, ContextProcessor):
cls.__processors__.append(value)
else:
if isinstance(value, Method):
if cls.__wrappable__:
raise ConfigurationError('Cannot define more than one "Method" option in a configurable. That may change in the future.')
cls.__wrappable__ = name
if not value.name:
value.name = name
if not name in cls.__options__:
Expand All @@ -43,6 +49,13 @@ class Configurable(metaclass=ConfigurableMeta):
"""

def __new__(cls, *args, **kwargs):
if cls.__wrappable__ and len(args) == 1 and hasattr(args[0], '__call__'):
wrapped, args = args[0], args[1:]
return type(wrapped.__name__, (cls, ), {cls.__wrappable__: wrapped})

return super().__new__(cls)

def __init__(self, *args, **kwargs):
super().__init__()

Expand Down Expand Up @@ -90,3 +103,6 @@ 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.
"""
return self.call(*args, **kwargs)

def call(self, *args, **kwargs):
raise NotImplementedError('Not implemented.')
23 changes: 22 additions & 1 deletion bonobo/config/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,34 @@ def __init__(self, type=None, *, required=False, positional=False, default=None)
self._creation_counter = Option._creation_counter
Option._creation_counter += 1

def __get__(self, inst, typ):
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 get_default(self):
return self.default() if callable(self.default) else self.default

def clean(self, value):
return self.type(value) if self.type else value


class Method(Option):
def __init__(self):
super().__init__(None, required=False, positional=True)

def __get__(self, inst, typ):
if not self.name in inst.__options_values__:
inst.__options_values__[self.name] = self.get_default()
inst.__options_values__[self.name] = getattr(inst, self.name)
return inst.__options_values__[self.name]

def __set__(self, inst, value):
inst.__options_values__[self.name] = self.type(value) if self.type else value

def clean(self, value):
if not hasattr(value, '__call__'):
raise ValueError('{} value must be callable.'.format(type(self).__name__))
return value
4 changes: 4 additions & 0 deletions bonobo/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,7 @@ def __init__(self, inst, message):

class ProhibitedOperationError(RuntimeError):
pass


class ConfigurationError(Exception):
pass
21 changes: 21 additions & 0 deletions bonobo/examples/utils/filter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import bonobo

from bonobo.filter import Filter


class OddOnlyFilter(Filter):
def filter(self, i):
return i % 2


@Filter
def MultiplesOfThreeOnlyFilter(self, i):
return not (i % 3)


graph = bonobo.Graph(
lambda: tuple(range(50)),
OddOnlyFilter(),
MultiplesOfThreeOnlyFilter(),
print,
)
6 changes: 1 addition & 5 deletions bonobo/execution/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,7 @@ def __init__(self, graph, plugins=None, services=None):
self.services = Container(services) if services else Container()

for i, node_context in enumerate(self):
try:
node_context.outputs = [self[j].input for j in self.graph.outputs_of(i)]
except KeyError:
continue

node_context.outputs = [self[j].input for j in self.graph.outputs_of(i)]
node_context.input.on_begin = partial(node_context.send, BEGIN, _control=True)
node_context.input.on_end = partial(node_context.send, END, _control=True)
node_context.input.on_finalize = partial(node_context.stop)
Expand Down
28 changes: 28 additions & 0 deletions bonobo/filter/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from bonobo.constants import NOT_MODIFIED

from bonobo.config import Configurable, Method


class Filter(Configurable):
"""Filter out hashes from the stream depending on the :attr:`filter` callable return value, when called with the
current hash as parameter.
Can be used as a decorator on a filter callable.
.. attribute:: filter
A callable used to filter lines.
If the callable returns a true-ish value, the input will be passed unmodified to the next items.
Otherwise, it'll be burnt.
"""

filter = Method()

def call(self, *args, **kwargs):
if self.filter(*args, **kwargs):
return NOT_MODIFIED


Empty file added pytest.ini
Empty file.
73 changes: 73 additions & 0 deletions tests/test_config_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import pytest

from bonobo.config import Configurable, Method, Option
from bonobo.errors import ConfigurationError


class MethodBasedConfigurable(Configurable):
handler = Method()
foo = Option(positional=True)
bar = Option()

def call(self, *args, **kwargs):
self.handler(*args, **kwargs)


def test_one_wrapper_only():
with pytest.raises(ConfigurationError):
class TwoMethods(Configurable):
h1 = Method()
h2 = Method()


def test_define_with_decorator():
calls = []

@MethodBasedConfigurable
def Concrete(self, *args, **kwargs):
calls.append((args, kwargs,))

t = Concrete('foo', bar='baz')
assert len(calls) == 0
t()
assert len(calls) == 1


def test_define_with_argument():
calls = []

def concrete_handler(*args, **kwargs):
calls.append((args, kwargs,))

t = MethodBasedConfigurable('foo', bar='baz', handler=concrete_handler)
assert len(calls) == 0
t()
assert len(calls) == 1

def test_define_with_inheritance():
calls = []

class Inheriting(MethodBasedConfigurable):
def handler(self, *args, **kwargs):
calls.append((args, kwargs,))

t = Inheriting('foo', bar='baz')
assert len(calls) == 0
t()
assert len(calls) == 1


def test_inheritance_then_decorate():
calls = []

class Inheriting(MethodBasedConfigurable):
pass

@Inheriting
def Concrete(self, *args, **kwargs):
calls.append((args, kwargs,))

t = Concrete('foo', bar='baz')
assert len(calls) == 0
t()
assert len(calls) == 1
13 changes: 13 additions & 0 deletions tests/test_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,24 @@ def test_simple_execution_context():
assert ctx[i].wrapped is node

assert not ctx.alive
assert not ctx.started
assert not ctx.stopped

ctx.recv(BEGIN, Bag(), END)

assert not ctx.alive
assert not ctx.started
assert not ctx.stopped

ctx.start()

assert ctx.alive
assert ctx.started
assert not ctx.stopped

ctx.stop()

assert not ctx.alive
assert ctx.started
assert ctx.stopped

0 comments on commit cf0b982

Please sign in to comment.