[config] Refactoring of configurables, allowing partially configured objects.
Configurables did not allow more than one "method" option, and mixed scenarios (options+methods+...) were sometimes flaky, forcing the user to know what order was the right one. Now, all options work the same, sharing the same "order" namespace. Backward incompatible change: Options are now required by default, unless a default is provided. Also adds a few candies for debugging/testing, found in the bonobo.util.inspect module.
This commit is contained in:
@ -2,12 +2,17 @@ import pytest
|
||||
|
||||
from bonobo.config.configurables import Configurable
|
||||
from bonobo.config.options import Option
|
||||
from bonobo.util.inspect import inspect_node
|
||||
|
||||
|
||||
class NoOptConfigurable(Configurable):
|
||||
pass
|
||||
|
||||
|
||||
class MyConfigurable(Configurable):
|
||||
required_str = Option(str, required=True)
|
||||
required_str = Option(str)
|
||||
default_str = Option(str, default='foo')
|
||||
integer = Option(int)
|
||||
integer = Option(int, required=False)
|
||||
|
||||
|
||||
class MyHarderConfigurable(MyConfigurable):
|
||||
@ -25,14 +30,20 @@ class MyConfigurableUsingPositionalOptions(MyConfigurable):
|
||||
|
||||
|
||||
def test_missing_required_option_error():
|
||||
with inspect_node(MyConfigurable()) as ni:
|
||||
assert ni.partial
|
||||
|
||||
with pytest.raises(TypeError) as exc:
|
||||
MyConfigurable()
|
||||
MyConfigurable(_final=True)
|
||||
assert exc.match('missing 1 required option:')
|
||||
|
||||
|
||||
def test_missing_required_options_error():
|
||||
with inspect_node(MyHarderConfigurable()) as ni:
|
||||
assert ni.partial
|
||||
|
||||
with pytest.raises(TypeError) as exc:
|
||||
MyHarderConfigurable()
|
||||
MyHarderConfigurable(_final=True)
|
||||
assert exc.match('missing 2 required options:')
|
||||
|
||||
|
||||
@ -50,6 +61,10 @@ def test_extraneous_options_error():
|
||||
|
||||
def test_defaults():
|
||||
o = MyConfigurable(required_str='hello')
|
||||
|
||||
with inspect_node(o) as ni:
|
||||
assert not ni.partial
|
||||
|
||||
assert o.required_str == 'hello'
|
||||
assert o.default_str == 'foo'
|
||||
assert o.integer == None
|
||||
@ -57,6 +72,10 @@ def test_defaults():
|
||||
|
||||
def test_str_type_factory():
|
||||
o = MyConfigurable(required_str=42)
|
||||
|
||||
with inspect_node(o) as ni:
|
||||
assert not ni.partial
|
||||
|
||||
assert o.required_str == '42'
|
||||
assert o.default_str == 'foo'
|
||||
assert o.integer == None
|
||||
@ -64,6 +83,10 @@ def test_str_type_factory():
|
||||
|
||||
def test_int_type_factory():
|
||||
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.default_str == 'bar'
|
||||
assert o.integer == 42
|
||||
@ -71,6 +94,10 @@ def test_int_type_factory():
|
||||
|
||||
def test_bool_type_factory():
|
||||
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.default_str == 'foo'
|
||||
assert o.integer == None
|
||||
@ -79,6 +106,10 @@ def test_bool_type_factory():
|
||||
|
||||
def test_option_resolution_order():
|
||||
o = MyBetterConfigurable()
|
||||
|
||||
with inspect_node(o) as ni:
|
||||
assert not ni.partial
|
||||
|
||||
assert o.required_str == 'kaboom'
|
||||
assert o.default_str == 'foo'
|
||||
assert o.integer == None
|
||||
@ -86,3 +117,21 @@ def test_option_resolution_order():
|
||||
|
||||
def test_option_positional():
|
||||
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.errors import ConfigurationError
|
||||
from bonobo.util.inspect import inspect_node
|
||||
|
||||
|
||||
class MethodBasedConfigurable(Configurable):
|
||||
@ -13,22 +11,56 @@ class MethodBasedConfigurable(Configurable):
|
||||
self.handler(*args, **kwargs)
|
||||
|
||||
|
||||
def test_one_wrapper_only():
|
||||
with pytest.raises(ConfigurationError):
|
||||
def test_multiple_wrapper_suppored():
|
||||
class TwoMethods(Configurable):
|
||||
h1 = Method(required=True)
|
||||
h2 = Method(required=True)
|
||||
|
||||
class TwoMethods(Configurable):
|
||||
h1 = Method()
|
||||
h2 = Method()
|
||||
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():
|
||||
calls = []
|
||||
|
||||
@MethodBasedConfigurable
|
||||
def Concrete(self, *args, **kwargs):
|
||||
calls.append((args, kwargs, ))
|
||||
def my_handler(*args, **kwargs):
|
||||
calls.append((args, kwargs,))
|
||||
|
||||
Concrete = MethodBasedConfigurable(my_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')
|
||||
|
||||
assert callable(t.handler)
|
||||
@ -37,13 +69,29 @@ def test_define_with_decorator():
|
||||
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():
|
||||
calls = []
|
||||
|
||||
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 len(calls) == 0
|
||||
t()
|
||||
@ -55,7 +103,7 @@ def test_define_with_inheritance():
|
||||
|
||||
class Inheriting(MethodBasedConfigurable):
|
||||
def handler(self, *args, **kwargs):
|
||||
calls.append((args, kwargs, ))
|
||||
calls.append((args, kwargs,))
|
||||
|
||||
t = Inheriting('foo', bar='baz')
|
||||
assert callable(t.handler)
|
||||
@ -71,8 +119,8 @@ def test_inheritance_then_decorate():
|
||||
pass
|
||||
|
||||
@Inheriting
|
||||
def Concrete(self, *args, **kwargs):
|
||||
calls.append((args, kwargs, ))
|
||||
def Concrete(*args, **kwargs):
|
||||
calls.append((args, kwargs,))
|
||||
|
||||
assert callable(Concrete.handler)
|
||||
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
|
||||
from bonobo.config.processors import ContextCurrifier
|
||||
from bonobo.constants import NOT_MODIFIED
|
||||
from bonobo.util.inspect import inspect_node
|
||||
|
||||
|
||||
def test_count():
|
||||
|
||||
Reference in New Issue
Block a user