[config] Implements "Exclusive" context processor allowing to ask for an exclusive usage of a service while in a transformation.

This commit is contained in:
Romain Dorgueil
2017-05-25 11:14:49 +02:00
parent 8abd40cd73
commit 71c47a762b
9 changed files with 139 additions and 61 deletions

View File

@ -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

View File

@ -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',
] ]

View File

@ -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
View File

0
tests/__init__.py Normal file
View File

View 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')

View 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'
]