[config] Implements "Exclusive" context processor allowing to ask for an exclusive usage of a service while in a transformation.
This commit is contained in:
88
tests/config/test_configurables.py
Normal file
88
tests/config/test_configurables.py
Normal file
@ -0,0 +1,88 @@
|
||||
import pytest
|
||||
|
||||
from bonobo.config.configurables import Configurable
|
||||
from bonobo.config.options import Option
|
||||
|
||||
|
||||
class MyConfigurable(Configurable):
|
||||
required_str = Option(str, required=True)
|
||||
default_str = Option(str, default='foo')
|
||||
integer = Option(int)
|
||||
|
||||
|
||||
class MyHarderConfigurable(MyConfigurable):
|
||||
also_required = Option(bool, required=True)
|
||||
|
||||
|
||||
class MyBetterConfigurable(MyConfigurable):
|
||||
required_str = Option(str, required=False, default='kaboom')
|
||||
|
||||
|
||||
class MyConfigurableUsingPositionalOptions(MyConfigurable):
|
||||
first = Option(str, required=True, positional=True)
|
||||
second = Option(str, required=True, positional=True)
|
||||
third = Option(str, required=False, positional=True)
|
||||
|
||||
|
||||
def test_missing_required_option_error():
|
||||
with pytest.raises(TypeError) as exc:
|
||||
MyConfigurable()
|
||||
assert exc.match('missing 1 required option:')
|
||||
|
||||
|
||||
def test_missing_required_options_error():
|
||||
with pytest.raises(TypeError) as exc:
|
||||
MyHarderConfigurable()
|
||||
assert exc.match('missing 2 required options:')
|
||||
|
||||
|
||||
def test_extraneous_option_error():
|
||||
with pytest.raises(TypeError) as exc:
|
||||
MyConfigurable(required_str='foo', hello='world')
|
||||
assert exc.match('got 1 unexpected option:')
|
||||
|
||||
|
||||
def test_extraneous_options_error():
|
||||
with pytest.raises(TypeError) as exc:
|
||||
MyConfigurable(required_str='foo', hello='world', acme='corp')
|
||||
assert exc.match('got 2 unexpected options:')
|
||||
|
||||
|
||||
def test_defaults():
|
||||
o = MyConfigurable(required_str='hello')
|
||||
assert o.required_str == 'hello'
|
||||
assert o.default_str == 'foo'
|
||||
assert o.integer == None
|
||||
|
||||
|
||||
def test_str_type_factory():
|
||||
o = MyConfigurable(required_str=42)
|
||||
assert o.required_str == '42'
|
||||
assert o.default_str == 'foo'
|
||||
assert o.integer == None
|
||||
|
||||
|
||||
def test_int_type_factory():
|
||||
o = MyConfigurable(required_str='yo', default_str='bar', integer='42')
|
||||
assert o.required_str == 'yo'
|
||||
assert o.default_str == 'bar'
|
||||
assert o.integer == 42
|
||||
|
||||
|
||||
def test_bool_type_factory():
|
||||
o = MyHarderConfigurable(required_str='yes', also_required='True')
|
||||
assert o.required_str == 'yes'
|
||||
assert o.default_str == 'foo'
|
||||
assert o.integer == None
|
||||
assert o.also_required == True
|
||||
|
||||
|
||||
def test_option_resolution_order():
|
||||
o = MyBetterConfigurable()
|
||||
assert o.required_str == 'kaboom'
|
||||
assert o.default_str == 'foo'
|
||||
assert o.integer == None
|
||||
|
||||
|
||||
def test_option_positional():
|
||||
o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello')
|
||||
82
tests/config/test_methods.py
Normal file
82
tests/config/test_methods.py
Normal file
@ -0,0 +1,82 @@
|
||||
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, ))
|
||||
|
||||
assert callable(Concrete.handler)
|
||||
t = Concrete('foo', bar='baz')
|
||||
|
||||
assert callable(t.handler)
|
||||
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 callable(t.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 callable(t.handler)
|
||||
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, ))
|
||||
|
||||
assert callable(Concrete.handler)
|
||||
t = Concrete('foo', bar='baz')
|
||||
assert callable(t.handler)
|
||||
assert len(calls) == 0
|
||||
t()
|
||||
assert len(calls) == 1
|
||||
63
tests/config/test_processors.py
Normal file
63
tests/config/test_processors.py
Normal file
@ -0,0 +1,63 @@
|
||||
from operator import attrgetter
|
||||
|
||||
from bonobo.config import Configurable
|
||||
from bonobo.config.processors import ContextProcessor, resolve_processors, ContextCurrifier
|
||||
|
||||
|
||||
class CP1(Configurable):
|
||||
@ContextProcessor
|
||||
def c(self):
|
||||
yield
|
||||
|
||||
@ContextProcessor
|
||||
def a(self):
|
||||
yield 'this is A'
|
||||
|
||||
@ContextProcessor
|
||||
def b(self, a):
|
||||
yield a.upper()[:-1] + 'b'
|
||||
|
||||
def __call__(self, a, b):
|
||||
return a, b
|
||||
|
||||
|
||||
class CP2(CP1):
|
||||
@ContextProcessor
|
||||
def f(self):
|
||||
pass
|
||||
|
||||
@ContextProcessor
|
||||
def e(self):
|
||||
pass
|
||||
|
||||
@ContextProcessor
|
||||
def d(self):
|
||||
pass
|
||||
|
||||
|
||||
class CP3(CP2):
|
||||
@ContextProcessor
|
||||
def c(self):
|
||||
pass
|
||||
|
||||
@ContextProcessor
|
||||
def b(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_all_processors_names(cls):
|
||||
return list(map(attrgetter('__name__'), resolve_processors(cls)))
|
||||
|
||||
|
||||
def test_inheritance_and_ordering():
|
||||
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(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()
|
||||
96
tests/config/test_services.py
Normal file
96
tests/config/test_services.py
Normal file
@ -0,0 +1,96 @@
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from bonobo.config import Configurable, Container, Exclusive, Service
|
||||
from bonobo.config.services import validate_service_name
|
||||
|
||||
|
||||
class PrinterInterface():
|
||||
def print(self, *args):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class ConcretePrinter(PrinterInterface):
|
||||
def __init__(self, prefix):
|
||||
self.prefix = prefix
|
||||
|
||||
def print(self, *args):
|
||||
return ';'.join((self.prefix, *args))
|
||||
|
||||
|
||||
SERVICES = Container(
|
||||
printer0=ConcretePrinter(prefix='0'),
|
||||
printer1=ConcretePrinter(prefix='1'),
|
||||
)
|
||||
|
||||
|
||||
class MyServiceDependantConfigurable(Configurable):
|
||||
printer = Service(
|
||||
PrinterInterface,
|
||||
)
|
||||
|
||||
def __call__(self, printer: PrinterInterface, *args):
|
||||
return printer.print(*args)
|
||||
|
||||
|
||||
def test_service_name_validator():
|
||||
assert validate_service_name('foo') == 'foo'
|
||||
assert validate_service_name('foo.bar') == 'foo.bar'
|
||||
assert validate_service_name('Foo') == 'Foo'
|
||||
assert validate_service_name('Foo.Bar') == 'Foo.Bar'
|
||||
assert validate_service_name('Foo.a0') == 'Foo.a0'
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
validate_service_name('foo.0')
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
validate_service_name('0.foo')
|
||||
|
||||
|
||||
def test_service_dependency():
|
||||
o = MyServiceDependantConfigurable(printer='printer0')
|
||||
|
||||
assert o(SERVICES.get('printer0'), 'foo', 'bar') == '0;foo;bar'
|
||||
assert o(SERVICES.get('printer1'), 'bar', 'baz') == '1;bar;baz'
|
||||
assert o(*SERVICES.args_for(o), 'foo', 'bar') == '0;foo;bar'
|
||||
|
||||
|
||||
def test_service_dependency_unavailable():
|
||||
o = MyServiceDependantConfigurable(printer='printer2')
|
||||
with pytest.raises(KeyError):
|
||||
SERVICES.args_for(o)
|
||||
|
||||
|
||||
class VCR:
|
||||
def __init__(self):
|
||||
self.tape = []
|
||||
|
||||
def append(self, x):
|
||||
return self.tape.append(x)
|
||||
|
||||
|
||||
def test_exclusive():
|
||||
vcr = VCR()
|
||||
vcr.append('hello')
|
||||
|
||||
def record(prefix, vcr=vcr):
|
||||
with Exclusive(vcr):
|
||||
for i in range(5):
|
||||
vcr.append(' '.join((prefix, str(i))))
|
||||
time.sleep(0.05)
|
||||
|
||||
threads = [threading.Thread(target=record, args=(str(i), )) for i in range(5)]
|
||||
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
time.sleep(0.01) # this is not good practice, how to test this without sleeping ?? XXX
|
||||
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
assert vcr.tape == [
|
||||
'hello', '0 0', '0 1', '0 2', '0 3', '0 4', '1 0', '1 1', '1 2', '1 3', '1 4', '2 0', '2 1', '2 2', '2 3',
|
||||
'2 4', '3 0', '3 1', '3 2', '3 3', '3 4', '4 0', '4 1', '4 2', '4 3', '4 4'
|
||||
]
|
||||
Reference in New Issue
Block a user