wip: refactoring context to share base.
This commit is contained in:
@ -99,9 +99,6 @@ class Lifecycle:
|
|||||||
|
|
||||||
self._stopped = True
|
self._stopped = True
|
||||||
|
|
||||||
if self._stopped: # Stopping twice has no effect
|
|
||||||
return
|
|
||||||
|
|
||||||
def kill(self):
|
def kill(self):
|
||||||
if not self.started:
|
if not self.started:
|
||||||
raise RuntimeError('Cannot kill an unstarted context.')
|
raise RuntimeError('Cannot kill an unstarted context.')
|
||||||
@ -136,3 +133,12 @@ class BaseContext(Lifecycle, Wrapper):
|
|||||||
Lifecycle.__init__(self)
|
Lifecycle.__init__(self)
|
||||||
Wrapper.__init__(self, wrapped)
|
Wrapper.__init__(self, wrapped)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
||||||
|
@property
|
||||||
|
def xstatus(self):
|
||||||
|
"""
|
||||||
|
UNIX-like exit status, only coherent if the context has stopped.
|
||||||
|
"""
|
||||||
|
if self._defunct:
|
||||||
|
return 70
|
||||||
|
return 0
|
||||||
|
|||||||
@ -4,12 +4,17 @@ from time import sleep
|
|||||||
from bonobo.config import create_container
|
from bonobo.config import create_container
|
||||||
from bonobo.constants import BEGIN, END
|
from bonobo.constants import BEGIN, END
|
||||||
from bonobo.execution import events
|
from bonobo.execution import events
|
||||||
|
from bonobo.execution.contexts.base import BaseContext
|
||||||
from bonobo.execution.contexts.node import NodeExecutionContext
|
from bonobo.execution.contexts.node import NodeExecutionContext
|
||||||
from bonobo.execution.contexts.plugin import PluginExecutionContext
|
from bonobo.execution.contexts.plugin import PluginExecutionContext
|
||||||
from whistle import EventDispatcher
|
from whistle import EventDispatcher
|
||||||
|
|
||||||
|
|
||||||
class GraphExecutionContext:
|
class GraphExecutionContext(BaseContext):
|
||||||
|
"""
|
||||||
|
Stores the actual state of a graph execution, and manages its lifecycle.
|
||||||
|
|
||||||
|
"""
|
||||||
NodeExecutionContextType = NodeExecutionContext
|
NodeExecutionContextType = NodeExecutionContext
|
||||||
PluginExecutionContextType = PluginExecutionContext
|
PluginExecutionContextType = PluginExecutionContext
|
||||||
|
|
||||||
@ -17,17 +22,24 @@ class GraphExecutionContext:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def started(self):
|
def started(self):
|
||||||
|
if not len(self.nodes):
|
||||||
|
return super(GraphExecutionContext, self).started
|
||||||
return any(node.started for node in self.nodes)
|
return any(node.started for node in self.nodes)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stopped(self):
|
def stopped(self):
|
||||||
|
if not len(self.nodes):
|
||||||
|
return super(GraphExecutionContext, self).stopped
|
||||||
return all(node.started and node.stopped for node in self.nodes)
|
return all(node.started and node.stopped for node in self.nodes)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def alive(self):
|
def alive(self):
|
||||||
|
if not len(self.nodes):
|
||||||
|
return super(GraphExecutionContext, self).alive
|
||||||
return any(node.alive for node in self.nodes)
|
return any(node.alive for node in self.nodes)
|
||||||
|
|
||||||
def __init__(self, graph, plugins=None, services=None, dispatcher=None):
|
def __init__(self, graph, *, plugins=None, services=None, dispatcher=None):
|
||||||
|
super(GraphExecutionContext, self).__init__(graph)
|
||||||
self.dispatcher = dispatcher or EventDispatcher()
|
self.dispatcher = dispatcher or EventDispatcher()
|
||||||
self.graph = graph
|
self.graph = graph
|
||||||
self.nodes = [self.create_node_execution_context_for(node) for node in self.graph]
|
self.nodes = [self.create_node_execution_context_for(node) for node in self.graph]
|
||||||
@ -74,6 +86,8 @@ class GraphExecutionContext:
|
|||||||
self.dispatcher.dispatch(name, events.ExecutionEvent(self))
|
self.dispatcher.dispatch(name, events.ExecutionEvent(self))
|
||||||
|
|
||||||
def start(self, starter=None):
|
def start(self, starter=None):
|
||||||
|
super(GraphExecutionContext, self).start()
|
||||||
|
|
||||||
self.register_plugins()
|
self.register_plugins()
|
||||||
self.dispatch(events.START)
|
self.dispatch(events.START)
|
||||||
self.tick(pause=False)
|
self.tick(pause=False)
|
||||||
@ -89,13 +103,16 @@ class GraphExecutionContext:
|
|||||||
if pause:
|
if pause:
|
||||||
sleep(self.TICK_PERIOD)
|
sleep(self.TICK_PERIOD)
|
||||||
|
|
||||||
def kill(self):
|
def loop(self):
|
||||||
self.dispatch(events.KILL)
|
while self.should_loop:
|
||||||
for node_context in self.nodes:
|
self.tick()
|
||||||
node_context.kill()
|
for node in self.nodes:
|
||||||
self.tick()
|
if node.should_loop:
|
||||||
|
node.step()
|
||||||
|
|
||||||
def stop(self, stopper=None):
|
def stop(self, stopper=None):
|
||||||
|
super(GraphExecutionContext, self).stop()
|
||||||
|
|
||||||
self.dispatch(events.STOP)
|
self.dispatch(events.STOP)
|
||||||
for node_context in self.nodes:
|
for node_context in self.nodes:
|
||||||
if stopper is None:
|
if stopper is None:
|
||||||
@ -106,6 +123,14 @@ class GraphExecutionContext:
|
|||||||
self.dispatch(events.STOPPED)
|
self.dispatch(events.STOPPED)
|
||||||
self.unregister_plugins()
|
self.unregister_plugins()
|
||||||
|
|
||||||
|
def kill(self):
|
||||||
|
super(GraphExecutionContext, self).kill()
|
||||||
|
|
||||||
|
self.dispatch(events.KILL)
|
||||||
|
for node_context in self.nodes:
|
||||||
|
node_context.kill()
|
||||||
|
self.tick()
|
||||||
|
|
||||||
def register_plugins(self):
|
def register_plugins(self):
|
||||||
for plugin_context in self.plugins:
|
for plugin_context in self.plugins:
|
||||||
plugin_context.register()
|
plugin_context.register()
|
||||||
|
|||||||
@ -21,6 +21,15 @@ UnboundArguments = namedtuple('UnboundArguments', ['args', 'kwargs'])
|
|||||||
|
|
||||||
|
|
||||||
class NodeExecutionContext(BaseContext, WithStatistics):
|
class NodeExecutionContext(BaseContext, WithStatistics):
|
||||||
|
"""
|
||||||
|
Stores the actual context of a node, within a given graph execution, accessed as `self.parent`.
|
||||||
|
|
||||||
|
A special case exist, mostly for testing purpose, where there is no parent context.
|
||||||
|
|
||||||
|
Can be used as a context manager, also very convenient for testing nodes that requires some external context (like
|
||||||
|
a service implementation, or a value holder).
|
||||||
|
|
||||||
|
"""
|
||||||
def __init__(self, wrapped, *, parent=None, services=None, _input=None, _outputs=None):
|
def __init__(self, wrapped, *, parent=None, services=None, _input=None, _outputs=None):
|
||||||
"""
|
"""
|
||||||
Node execution context has the responsibility fo storing the state of a transformation during its execution.
|
Node execution context has the responsibility fo storing the state of a transformation during its execution.
|
||||||
@ -170,7 +179,7 @@ class NodeExecutionContext(BaseContext, WithStatistics):
|
|||||||
if self._stack:
|
if self._stack:
|
||||||
try:
|
try:
|
||||||
self._stack.teardown()
|
self._stack.teardown()
|
||||||
except:
|
except Exception as exc:
|
||||||
self.fatal(sys.exc_info())
|
self.fatal(sys.exc_info())
|
||||||
|
|
||||||
super().stop()
|
super().stop()
|
||||||
|
|||||||
59
tests/execution/contexts/test_execution_contexts_graph.py
Normal file
59
tests/execution/contexts/test_execution_contexts_graph.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from bonobo import Graph
|
||||||
|
from bonobo.execution.contexts import GraphExecutionContext
|
||||||
|
|
||||||
|
|
||||||
|
def raise_an_error(*args, **kwargs):
|
||||||
|
raise Exception('Careful, man, there\'s a beverage here!')
|
||||||
|
|
||||||
|
|
||||||
|
def raise_an_unrecoverrable_error(*args, **kwargs):
|
||||||
|
raise Exception('You are entering a world of pain!')
|
||||||
|
|
||||||
|
|
||||||
|
def test_lifecycle_of_empty_graph():
|
||||||
|
graph = Graph()
|
||||||
|
with GraphExecutionContext(graph) as context:
|
||||||
|
assert context.started
|
||||||
|
assert context.alive
|
||||||
|
assert not context.stopped
|
||||||
|
assert context.started
|
||||||
|
assert not context.alive
|
||||||
|
assert context.stopped
|
||||||
|
assert not context.xstatus
|
||||||
|
|
||||||
|
|
||||||
|
def test_lifecycle_of_nonempty_graph():
|
||||||
|
graph = Graph([1, 2, 3], print)
|
||||||
|
with GraphExecutionContext(graph) as context:
|
||||||
|
assert context.started
|
||||||
|
assert context.alive
|
||||||
|
assert not context.stopped
|
||||||
|
assert context.started
|
||||||
|
assert not context.alive
|
||||||
|
assert context.stopped
|
||||||
|
assert not context.xstatus
|
||||||
|
|
||||||
|
|
||||||
|
def test_lifecycle_of_graph_with_recoverable_error():
|
||||||
|
graph = Graph([1, 2, 3], raise_an_error, print)
|
||||||
|
with GraphExecutionContext(graph) as context:
|
||||||
|
assert context.started
|
||||||
|
assert context.alive
|
||||||
|
assert not context.stopped
|
||||||
|
assert context.started
|
||||||
|
assert not context.alive
|
||||||
|
assert context.stopped
|
||||||
|
assert not context.xstatus
|
||||||
|
|
||||||
|
|
||||||
|
def test_lifecycle_of_graph_with_unrecoverable_error():
|
||||||
|
graph = Graph([1, 2, 3], raise_an_unrecoverrable_error, print)
|
||||||
|
with GraphExecutionContext(graph) as context:
|
||||||
|
assert context.started
|
||||||
|
assert context.alive
|
||||||
|
assert not context.stopped
|
||||||
|
context.loop()
|
||||||
|
assert context.started
|
||||||
|
assert not context.alive
|
||||||
|
assert context.stopped
|
||||||
|
assert not context.xstatus
|
||||||
Reference in New Issue
Block a user