Merge pull request #275 from hartym/better_errors

Proposed changes about errors and casts.
This commit is contained in:
Romain Dorgueil
2018-07-28 13:45:24 +01:00
committed by GitHub
23 changed files with 277 additions and 111 deletions

View File

@ -1,4 +1,4 @@
# Generated by Medikit 0.6.3 on 2018-06-11. # Generated by Medikit 0.6.3 on 2018-07-22.
# All changes will be overriden. # All changes will be overriden.
# Edit Projectfile and run “make update” (or “medikit update”) to regenerate. # Edit Projectfile and run “make update” (or “medikit update”) to regenerate.

View File

@ -72,7 +72,7 @@ class ConfigurableMeta(type):
try: try:
import _functools import _functools
except: except ImportError:
import functools import functools
PartiallyConfigured = functools.partial PartiallyConfigured = functools.partial
@ -141,15 +141,14 @@ class Configurable(metaclass=ConfigurableMeta):
break # option orders make all positional options first, job done. break # option orders make all positional options first, job done.
if not isoption(getattr(cls, name)): if not isoption(getattr(cls, name)):
missing.remove(name) missing.discard(name)
continue continue
if len(args) <= position: if len(args) <= position:
break # no more positional arguments given. break # no more positional arguments given.
position += 1 position += 1
if name in missing: missing.discard(name)
missing.remove(name)
# complain if there is more options than possible. # complain if there is more options than possible.
extraneous = set(kwargs.keys()) - (set(next(zip(*options))) if len(options) else set()) extraneous = set(kwargs.keys()) - (set(next(zip(*options))) if len(options) else set())

View File

@ -1,5 +1,3 @@
import inspect
import pprint
import re import re
import threading import threading
import types import types

View File

@ -51,7 +51,7 @@ if __name__ == '__main__':
s3.head_object( s3.head_object(
Bucket='bonobo-examples', Key=s3_path Bucket='bonobo-examples', Key=s3_path
) )
except: except Exception:
s3.upload_file( s3.upload_file(
local_path, local_path,
'bonobo-examples', 'bonobo-examples',

View File

@ -1,12 +1,10 @@
import logging import logging
import sys import sys
from contextlib import contextmanager from contextlib import contextmanager
from logging import ERROR
from mondrian import term
from bonobo.util import deprecated from bonobo.util import deprecated
from bonobo.util.objects import Wrapper, get_name from bonobo.util.objects import Wrapper, get_name
from mondrian import term
@contextmanager @contextmanager
@ -14,7 +12,7 @@ def recoverable(error_handler):
try: try:
yield yield
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
error_handler(*sys.exc_info(), level=ERROR) error_handler(*sys.exc_info(), level=logging.ERROR)
@contextmanager @contextmanager
@ -22,8 +20,8 @@ def unrecoverable(error_handler):
try: try:
yield yield
except Exception as exc: # pylint: disable=broad-except except Exception as exc: # pylint: disable=broad-except
error_handler(*sys.exc_info(), level=ERROR) error_handler(*sys.exc_info(), level=logging.ERROR)
raise # raise unrecoverableerror from x ? raise # raise unrecoverableerror from exc ?
class Lifecycle: class Lifecycle:
@ -55,12 +53,14 @@ class Lifecycle:
@property @property
def should_loop(self): def should_loop(self):
# TODO XXX started/stopped? return self.alive and not any((self.defunct, self.killed))
return not any((self.defunct, self.killed))
@property @property
def status(self): def status(self):
"""One character status for this node. """ """
One character status for this node.
"""
if self._defunct: if self._defunct:
return '!' return '!'
if not self.started: if not self.started:
@ -97,9 +97,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.')
@ -134,3 +131,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

View File

@ -1,15 +1,25 @@
import logging
from functools import partial from functools import partial
from queue import Empty
from time import sleep 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.errors import InactiveReadableError
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
logger = logging.getLogger(__name__)
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 +27,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 +91,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 +108,21 @@ class GraphExecutionContext:
if pause: if pause:
sleep(self.TICK_PERIOD) sleep(self.TICK_PERIOD)
def kill(self): def loop(self):
self.dispatch(events.KILL) nodes = set(node for node in self.nodes if node.should_loop)
for node_context in self.nodes: while self.should_loop and len(nodes):
node_context.kill() self.tick(pause=False)
self.tick() for node in list(nodes):
try:
node.step()
except Empty:
continue
except InactiveReadableError:
nodes.discard(node)
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 +133,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()
@ -113,3 +148,11 @@ class GraphExecutionContext:
def unregister_plugins(self): def unregister_plugins(self):
for plugin_context in self.plugins: for plugin_context in self.plugins:
plugin_context.unregister() plugin_context.unregister()
@property
def xstatus(self):
"""
UNIX-like exit status, only coherent if the context has stopped.
"""
return max(node.xstatus for node in self.nodes) if len(self.nodes) else 0

View File

@ -21,6 +21,16 @@ 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.
@ -77,11 +87,22 @@ class NodeExecutionContext(BaseContext, WithStatistics):
initial = self._get_initial_context() initial = self._get_initial_context()
self._stack = ContextCurrifier(self.wrapped, *initial.args, **initial.kwargs) self._stack = ContextCurrifier(self.wrapped, *initial.args, **initial.kwargs)
if isconfigurabletype(self.wrapped): if isconfigurabletype(self.wrapped):
# Not normal to have a partially configured object here, so let's warn the user instead of having get into try:
# the hard trouble of understanding that by himself. self.wrapped = self.wrapped(_final=True)
raise TypeError( except Exception as exc:
'Configurables should be instanciated before execution starts.\nGot {!r}.\n'.format(self.wrapped) # Not normal to have a partially configured object here, so let's warn the user instead of having get into
) # the hard trouble of understanding that by himself.
raise TypeError(
'Configurables should be instanciated before execution starts.\nGot {!r}.\n'.format(
self.wrapped
)
) from exc
else:
raise TypeError(
'Configurables should be instanciated before execution starts.\nGot {!r}.\n'.format(
self.wrapped
)
)
self._stack.setup(self) self._stack.setup(self)
except Exception: except Exception:
# Set the logging level to the lowest possible, to avoid double log. # Set the logging level to the lowest possible, to avoid double log.
@ -102,22 +123,27 @@ class NodeExecutionContext(BaseContext, WithStatistics):
self.step() self.step()
except InactiveReadableError: except InactiveReadableError:
break break
except Empty:
sleep(TICK_PERIOD) # XXX: How do we determine this constant?
continue
except (
NotImplementedError,
UnrecoverableError,
):
self.fatal(sys.exc_info()) # exit loop
except Exception: # pylint: disable=broad-except
self.error(sys.exc_info()) # does not exit loop
except BaseException:
self.fatal(sys.exc_info()) # exit loop
logger.debug('Node loop ends for {!r}.'.format(self)) logger.debug('Node loop ends for {!r}.'.format(self))
def step(self): def step(self):
try:
self._step()
except InactiveReadableError:
raise
except Empty:
sleep(TICK_PERIOD) # XXX: How do we determine this constant?
except (
NotImplementedError,
UnrecoverableError,
):
self.fatal(sys.exc_info()) # exit loop
except Exception: # pylint: disable=broad-except
self.error(sys.exc_info()) # does not exit loop
except BaseException:
self.fatal(sys.exc_info()) # exit loop
def _step(self):
""" """
A single step in the loop. A single step in the loop.
@ -163,7 +189,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()
@ -231,12 +257,11 @@ class NodeExecutionContext(BaseContext, WithStatistics):
:param mixed value: message :param mixed value: message
""" """
for message in messages: for message in messages:
if isinstance(message, Token): if not isinstance(message, Token):
self.input.put(message) message = ensure_tuple(message, cls=self._input_type, length=self._input_length)
elif self._input_type: if self._input_length is None:
self.input.put(ensure_tuple(message, cls=self._input_type)) self._input_length = len(message)
else: self.input.put(message)
self.input.put(ensure_tuple(message))
def write_sync(self, *messages): def write_sync(self, *messages):
self.write(BEGIN, *messages, END) self.write(BEGIN, *messages, END)
@ -264,17 +289,22 @@ class NodeExecutionContext(BaseContext, WithStatistics):
If Queue raises (like Timeout or Empty), stat won't be changed. If Queue raises (like Timeout or Empty), stat won't be changed.
""" """
input_bag = self.input.get() input_bag = self.input.get(timeout=0)
# Store or check input type # Store or check input type
if self._input_type is None: if self._input_type is None:
self._input_type = type(input_bag) self._input_type = type(input_bag)
elif type(input_bag) is not self._input_type: elif type(input_bag) != self._input_type:
raise UnrecoverableTypeError( try:
'Input type changed between calls to {!r}.\nGot {!r} which is not of type {!r}.'.format( if self._input_type == tuple:
self.wrapped, input_bag, self._input_type input_bag = self._input_type(input_bag)
) else:
) input_bag = self._input_type(*input_bag)
except Exception as exc:
raise UnrecoverableTypeError(
'Input type changed to incompatible type between calls to {!r}.\nGot {!r} which is not of type {!r}.'.
format(self.wrapped, input_bag, self._input_type)
) from exc
# Store or check input length, which is a soft fallback in case we're just using tuples # Store or check input length, which is a soft fallback in case we're just using tuples
if self._input_length is None: if self._input_length is None:

View File

@ -29,7 +29,7 @@ class ExecutorStrategy(Strategy):
with self.create_executor() as executor: with self.create_executor() as executor:
try: try:
context.start(self.get_starter(executor, futures)) context.start(self.get_starter(executor, futures))
except: except Exception:
logger.critical('Exception caught while starting execution context.', exc_info=sys.exc_info()) logger.critical('Exception caught while starting execution context.', exc_info=sys.exc_info())
while context.alive: while context.alive:
@ -53,14 +53,14 @@ class ExecutorStrategy(Strategy):
try: try:
with node: with node:
node.loop() node.loop()
except: except Exception:
logging.getLogger(__name__).critical( logging.getLogger(__name__).critical(
'Critical error in threadpool node starter.', exc_info=sys.exc_info() 'Critical error in threadpool node starter.', exc_info=sys.exc_info()
) )
try: try:
futures.append(executor.submit(_runner)) futures.append(executor.submit(_runner))
except: except Exception:
logging.getLogger(__name__).critical('futures.append', exc_info=sys.exc_info()) logging.getLogger(__name__).critical('futures.append', exc_info=sys.exc_info())
return starter return starter

View File

@ -0,0 +1,16 @@
from bonobo.config import Configurable, Method, Option, ContextProcessor, use_raw_input
from bonobo.util import ValueHolder
class Reduce(Configurable):
function = Method()
initializer = Option(required=False)
@ContextProcessor
def buffer(self, context):
values = yield ValueHolder(self.initializer() if callable(self.initializer) else self.initializer)
context.send(values.get())
@use_raw_input
def __call__(self, values, bag):
values.set(self.function(values.get(), bag))

View File

@ -1,12 +1,11 @@
import csv import csv
from bonobo.config import Option, use_raw_input, use_context from bonobo.config import Option, use_context
from bonobo.config.options import Method, RenamedOption from bonobo.config.options import Method, RenamedOption
from bonobo.constants import NOT_MODIFIED from bonobo.constants import NOT_MODIFIED
from bonobo.nodes.io.base import FileHandler from bonobo.nodes.io.base import FileHandler
from bonobo.nodes.io.file import FileReader, FileWriter from bonobo.nodes.io.file import FileReader, FileWriter
from bonobo.util import ensure_tuple from bonobo.util import ensure_tuple
from bonobo.util.bags import BagType
class CsvHandler(FileHandler): class CsvHandler(FileHandler):

View File

@ -1,5 +1,4 @@
from bonobo.plugins import Plugin from bonobo.plugins import Plugin
from raven import Client
class SentryPlugin(Plugin): class SentryPlugin(Plugin):

View File

@ -89,6 +89,7 @@ class Registry:
default_registry = Registry() default_registry = Registry()
def create_reader(name, *args, format=None, registry=default_registry, **kwargs): def create_reader(name, *args, format=None, registry=default_registry, **kwargs):
""" """
Create a reader instance, guessing its factory using filename (and eventually format). Create a reader instance, guessing its factory using filename (and eventually format).
@ -103,6 +104,7 @@ def create_reader(name, *args, format=None, registry=default_registry, **kwargs)
""" """
return registry.get_reader_factory_for(name, format=format)(name, *args, **kwargs) return registry.get_reader_factory_for(name, format=format)(name, *args, **kwargs)
def create_writer(name, *args, format=None, registry=default_registry, **kwargs): def create_writer(name, *args, format=None, registry=default_registry, **kwargs):
""" """
Create a writer instance, guessing its factory using filename (and eventually format). Create a writer instance, guessing its factory using filename (and eventually format).

View File

@ -52,7 +52,7 @@ class Setting:
def set(self, value): def set(self, value):
value = self.formatter(value) if self.formatter else value value = self.formatter(value) if self.formatter else value
if self.validator and not self.validator(value): if self.validator and not self.validator(value):
raise ValidationError('Invalid value {!r} for setting {}.'.format(value, self.name)) raise ValidationError(self, 'Invalid value {!r} for setting {!r}.'.format(value, self.name))
self.value = value self.value = value
def set_if_true(self, value): def set_if_true(self, value):

View File

@ -7,7 +7,23 @@ class sortedlist(list):
bisect.insort(self, x) bisect.insort(self, x)
def ensure_tuple(tuple_or_mixed, *, cls=tuple): def _with_length_check(f):
@functools.wraps(f)
def _wrapped(*args, length=None, **kwargs):
nonlocal f
result = f(*args, **kwargs)
if length is not None:
if length != len(result):
raise TypeError(
'Length check failed, expected {} fields but got {}: {!r}.'.format(length, len(result), result)
)
return result
return _wrapped
@_with_length_check
def ensure_tuple(tuple_or_mixed, *, cls=None):
""" """
If it's not a tuple, let's make a tuple of one item. If it's not a tuple, let's make a tuple of one item.
Otherwise, not changed. Otherwise, not changed.
@ -16,6 +32,8 @@ def ensure_tuple(tuple_or_mixed, *, cls=tuple):
:return: tuple :return: tuple
""" """
if cls is None:
cls = tuple
if isinstance(tuple_or_mixed, cls): if isinstance(tuple_or_mixed, cls):
return tuple_or_mixed return tuple_or_mixed

View File

@ -57,7 +57,6 @@ def get_argument_parser(parser=None):
:return: :return:
""" """
if parser is None: if parser is None:
import argparse
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
# Store globally to be able to warn the user about the fact he's probably wrong not to pass a parser to # Store globally to be able to warn the user about the fact he's probably wrong not to pass a parser to

View File

@ -1,6 +1,6 @@
-e .[dev] -e .[dev]
-r requirements.txt -r requirements.txt
alabaster==0.7.10 alabaster==0.7.11
arrow==0.12.1 arrow==0.12.1
atomicwrites==1.1.5 atomicwrites==1.1.5
attrs==18.1.0 attrs==18.1.0
@ -13,7 +13,7 @@ cookiecutter==1.5.1
coverage==4.5.1 coverage==4.5.1
docutils==0.14 docutils==0.14
future==0.16.0 future==0.16.0
idna==2.6 idna==2.7
imagesize==1.0.0 imagesize==1.0.0
jinja2-time==0.2.0 jinja2-time==0.2.0
jinja2==2.10 jinja2==2.10
@ -22,20 +22,20 @@ more-itertools==4.2.0
packaging==17.1 packaging==17.1
pluggy==0.6.0 pluggy==0.6.0
poyo==0.4.1 poyo==0.4.1
py==1.5.3 py==1.5.4
pygments==2.2.0 pygments==2.2.0
pyparsing==2.2.0 pyparsing==2.2.0
pytest-cov==2.5.1 pytest-cov==2.5.1
pytest-timeout==1.2.1 pytest-timeout==1.3.0
pytest==3.6.1 pytest==3.6.3
python-dateutil==2.7.3 python-dateutil==2.7.3
pytz==2018.4 pytz==2018.5
requests==2.18.4 requests==2.19.1
six==1.11.0 six==1.11.0
snowballstemmer==1.2.1 snowballstemmer==1.2.1
sphinx-sitemap==0.2 sphinx-sitemap==0.2
sphinx==1.7.5 sphinx==1.7.6
sphinxcontrib-websupport==1.1.0 sphinxcontrib-websupport==1.1.0
urllib3==1.22 urllib3==1.23
whichcraft==0.4.1 whichcraft==0.4.1
yapf==0.22.0 yapf==0.22.0

View File

@ -7,24 +7,23 @@ chardet==3.0.4
colorama==0.3.9 colorama==0.3.9
docker-pycreds==0.3.0 docker-pycreds==0.3.0
docker==2.7.0 docker==2.7.0
fs==2.0.23 fs==2.0.25
graphviz==0.8.3 graphviz==0.8.4
idna==2.6 idna==2.7
jinja2==2.10 jinja2==2.10
markupsafe==1.0 markupsafe==1.0
mondrian==0.7.0 mondrian==0.7.0
packaging==17.1 packaging==17.1
pbr==4.0.4 pbr==4.1.1
psutil==5.4.6 psutil==5.4.6
pyparsing==2.2.0 pyparsing==2.2.0
python-slugify==1.2.5 python-slugify==1.2.5
pytz==2018.4 pytz==2018.5
requests==2.18.4 requests==2.19.1
semantic-version==2.6.0 semantic-version==2.6.0
six==1.11.0 six==1.11.0
stevedore==1.28.0 stevedore==1.29.0
typing==3.6.4
unidecode==1.0.22 unidecode==1.0.22
urllib3==1.22 urllib3==1.23
websocket-client==0.48.0 websocket-client==0.48.0
whistle==1.0.1 whistle==1.0.1

View File

@ -10,7 +10,7 @@ ipykernel==4.8.2
ipython-genutils==0.2.0 ipython-genutils==0.2.0
ipython==6.4.0 ipython==6.4.0
ipywidgets==6.0.1 ipywidgets==6.0.1
jedi==0.12.0 jedi==0.12.1
jinja2==2.10 jinja2==2.10
jsonschema==2.6.0 jsonschema==2.6.0
jupyter-client==5.2.3 jupyter-client==5.2.3
@ -21,23 +21,24 @@ markupsafe==1.0
mistune==0.8.3 mistune==0.8.3
nbconvert==5.3.1 nbconvert==5.3.1
nbformat==4.4.0 nbformat==4.4.0
notebook==5.5.0 notebook==5.6.0
pandocfilters==1.4.2 pandocfilters==1.4.2
parso==0.2.1 parso==0.3.1
pexpect==4.6.0 pexpect==4.6.0
pickleshare==0.7.4 pickleshare==0.7.4
prometheus-client==0.3.0
prompt-toolkit==1.0.15 prompt-toolkit==1.0.15
ptyprocess==0.5.2 ptyprocess==0.6.0
pygments==2.2.0 pygments==2.2.0
python-dateutil==2.7.3 python-dateutil==2.7.3
pyzmq==17.0.0 pyzmq==17.1.0
qtconsole==4.3.1 qtconsole==4.3.1
send2trash==1.5.0 send2trash==1.5.0
simplegeneric==0.8.1 simplegeneric==0.8.1
six==1.11.0 six==1.11.0
terminado==0.8.1 terminado==0.8.1
testpath==0.3.1 testpath==0.3.1
tornado==5.0.2 tornado==5.1
traitlets==4.3.2 traitlets==4.3.2
wcwidth==0.1.7 wcwidth==0.1.7
webencodings==0.5.1 webencodings==0.5.1

View File

@ -5,23 +5,22 @@ bonobo-sqlalchemy==0.6.0
certifi==2018.4.16 certifi==2018.4.16
chardet==3.0.4 chardet==3.0.4
colorama==0.3.9 colorama==0.3.9
fs==2.0.23 fs==2.0.25
graphviz==0.8.3 graphviz==0.8.4
idna==2.6 idna==2.7
jinja2==2.10 jinja2==2.10
markupsafe==1.0 markupsafe==1.0
mondrian==0.7.0 mondrian==0.7.0
packaging==17.1 packaging==17.1
pbr==4.0.4 pbr==4.1.1
psutil==5.4.6 psutil==5.4.6
pyparsing==2.2.0 pyparsing==2.2.0
python-slugify==1.2.5 python-slugify==1.2.5
pytz==2018.4 pytz==2018.5
requests==2.18.4 requests==2.19.1
six==1.11.0 six==1.11.0
sqlalchemy==1.2.8 sqlalchemy==1.2.10
stevedore==1.28.0 stevedore==1.29.0
typing==3.6.4
unidecode==1.0.22 unidecode==1.0.22
urllib3==1.22 urllib3==1.23
whistle==1.0.1 whistle==1.0.1

View File

@ -3,22 +3,21 @@ appdirs==1.4.3
certifi==2018.4.16 certifi==2018.4.16
chardet==3.0.4 chardet==3.0.4
colorama==0.3.9 colorama==0.3.9
fs==2.0.23 fs==2.0.25
graphviz==0.8.3 graphviz==0.8.4
idna==2.6 idna==2.7
jinja2==2.10 jinja2==2.10
markupsafe==1.0 markupsafe==1.0
mondrian==0.7.0 mondrian==0.7.0
packaging==17.1 packaging==17.1
pbr==4.0.4 pbr==4.1.1
psutil==5.4.6 psutil==5.4.6
pyparsing==2.2.0 pyparsing==2.2.0
python-slugify==1.2.5 python-slugify==1.2.5
pytz==2018.4 pytz==2018.5
requests==2.18.4 requests==2.19.1
six==1.11.0 six==1.11.0
stevedore==1.28.0 stevedore==1.29.0
typing==3.6.4
unidecode==1.0.22 unidecode==1.0.22
urllib3==1.22 urllib3==1.23
whistle==1.0.1 whistle==1.0.1

View File

@ -1,4 +1,4 @@
# Generated by Medikit 0.6.3 on 2018-06-11. # Generated by Medikit 0.6.3 on 2018-07-22.
# All changes will be overriden. # All changes will be overriden.
# Edit Projectfile and run “make update” (or “medikit update”) to regenerate. # Edit Projectfile and run “make update” (or “medikit update”) to regenerate.

View File

@ -0,0 +1,59 @@
from bonobo import Graph
from bonobo.constants import EMPTY, BEGIN, END
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 and context.alive and not context.stopped
context.write(BEGIN, EMPTY, END)
context.loop()
assert context.started
assert not context.alive
assert context.stopped
assert not context.xstatus