[config] Implements "Exclusive" context processor allowing to ask for an exclusive usage of a service while in a transformation.
This commit is contained in:
@ -1,5 +1,3 @@
|
|||||||
import warnings
|
|
||||||
|
|
||||||
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, \
|
||||||
PrettyPrint, Tee, count, identity, noop, pprint
|
PrettyPrint, Tee, count, identity, noop, pprint
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
from bonobo.config.configurables import Configurable
|
from bonobo.config.configurables import Configurable
|
||||||
from bonobo.config.options import Option, Method
|
from bonobo.config.options import Option, Method
|
||||||
from bonobo.config.processors import ContextProcessor
|
from bonobo.config.processors import ContextProcessor
|
||||||
from bonobo.config.services import Container, Service
|
from bonobo.config.services import Container, Service, Exclusive
|
||||||
|
|
||||||
|
# bonobo.config public programming interface
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'Configurable',
|
'Configurable',
|
||||||
'Container',
|
'Container',
|
||||||
'ContextProcessor',
|
'ContextProcessor',
|
||||||
'Option',
|
'Exclusive',
|
||||||
'Method',
|
'Method',
|
||||||
|
'Option',
|
||||||
'Service',
|
'Service',
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
|
import threading
|
||||||
import types
|
import types
|
||||||
|
from contextlib import ContextDecorator
|
||||||
|
|
||||||
from bonobo.config.options import Option
|
from bonobo.config.options import Option
|
||||||
from bonobo.errors import MissingServiceImplementationError
|
from bonobo.errors import MissingServiceImplementationError
|
||||||
@ -87,3 +89,40 @@ class Container(dict):
|
|||||||
if isinstance(value, types.LambdaType):
|
if isinstance(value, types.LambdaType):
|
||||||
value = value(self)
|
value = value(self)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class Exclusive(ContextDecorator):
|
||||||
|
"""
|
||||||
|
Decorator and context manager used to require exclusive usage of an object, most probably a service. It's usefull
|
||||||
|
for example if call order matters on a service implementation (think of an http api that requires a nonce or version
|
||||||
|
parameter ...).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
>>> def handler(some_service):
|
||||||
|
... with Exclusive(some_service):
|
||||||
|
... some_service.call_1()
|
||||||
|
... some_service.call_2()
|
||||||
|
... some_service.call_3()
|
||||||
|
|
||||||
|
This will ensure that nobody else is using the same service while in the "with" block, using a lock primitive to
|
||||||
|
ensure that.
|
||||||
|
|
||||||
|
"""
|
||||||
|
_locks = {}
|
||||||
|
|
||||||
|
def __init__(self, wrapped):
|
||||||
|
self._wrapped = wrapped
|
||||||
|
|
||||||
|
def get_lock(self):
|
||||||
|
_id = id(self._wrapped)
|
||||||
|
if not _id in Exclusive._locks:
|
||||||
|
Exclusive._locks[_id] = threading.RLock()
|
||||||
|
return Exclusive._locks[_id]
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.get_lock().acquire()
|
||||||
|
return self._wrapped
|
||||||
|
|
||||||
|
def __exit__(self, *exc):
|
||||||
|
self.get_lock().release()
|
||||||
|
|||||||
0
config/__init__.py
Normal file
0
config/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
@ -2,7 +2,6 @@ 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.config.services import Container, Service, validate_service_name
|
|
||||||
|
|
||||||
|
|
||||||
class MyConfigurable(Configurable):
|
class MyConfigurable(Configurable):
|
||||||
@ -25,28 +24,6 @@ class MyConfigurableUsingPositionalOptions(MyConfigurable):
|
|||||||
third = Option(str, required=False, positional=True)
|
third = Option(str, required=False, positional=True)
|
||||||
|
|
||||||
|
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
class MyServiceDependantConfigurable(Configurable):
|
|
||||||
printer = Service(
|
|
||||||
PrinterInterface,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __call__(self, printer: PrinterInterface, *args):
|
|
||||||
return printer.print(*args)
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_required_option_error():
|
def test_missing_required_option_error():
|
||||||
with pytest.raises(TypeError) as exc:
|
with pytest.raises(TypeError) as exc:
|
||||||
MyConfigurable()
|
MyConfigurable()
|
||||||
@ -107,39 +84,5 @@ def test_option_resolution_order():
|
|||||||
assert o.integer == None
|
assert o.integer == None
|
||||||
|
|
||||||
|
|
||||||
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')
|
|
||||||
|
|
||||||
|
|
||||||
SERVICES = Container(
|
|
||||||
printer0=ConcretePrinter(prefix='0'),
|
|
||||||
printer1=ConcretePrinter(prefix='1'),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def test_option_positional():
|
def test_option_positional():
|
||||||
o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello')
|
o = MyConfigurableUsingPositionalOptions('1', '2', '3', required_str='hello')
|
||||||
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