Merge pull request #275 from hartym/better_errors
Proposed changes about errors and casts.
This commit is contained in:
2
Makefile
2
Makefile
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import inspect
|
|
||||||
import pprint
|
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
import types
|
import types
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,10 +87,21 @@ 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):
|
||||||
|
try:
|
||||||
|
self.wrapped = self.wrapped(_final=True)
|
||||||
|
except Exception as exc:
|
||||||
# Not normal to have a partially configured object here, so let's warn the user instead of having get into
|
# 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.
|
# the hard trouble of understanding that by himself.
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'Configurables should be instanciated before execution starts.\nGot {!r}.\n'.format(self.wrapped)
|
'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:
|
||||||
@ -102,9 +123,16 @@ class NodeExecutionContext(BaseContext, WithStatistics):
|
|||||||
self.step()
|
self.step()
|
||||||
except InactiveReadableError:
|
except InactiveReadableError:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
logger.debug('Node loop ends for {!r}.'.format(self))
|
||||||
|
|
||||||
|
def step(self):
|
||||||
|
try:
|
||||||
|
self._step()
|
||||||
|
except InactiveReadableError:
|
||||||
|
raise
|
||||||
except Empty:
|
except Empty:
|
||||||
sleep(TICK_PERIOD) # XXX: How do we determine this constant?
|
sleep(TICK_PERIOD) # XXX: How do we determine this constant?
|
||||||
continue
|
|
||||||
except (
|
except (
|
||||||
NotImplementedError,
|
NotImplementedError,
|
||||||
UnrecoverableError,
|
UnrecoverableError,
|
||||||
@ -115,9 +143,7 @@ class NodeExecutionContext(BaseContext, WithStatistics):
|
|||||||
except BaseException:
|
except BaseException:
|
||||||
self.fatal(sys.exc_info()) # exit loop
|
self.fatal(sys.exc_info()) # exit loop
|
||||||
|
|
||||||
logger.debug('Node loop ends for {!r}.'.format(self))
|
def _step(self):
|
||||||
|
|
||||||
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):
|
||||||
|
message = ensure_tuple(message, cls=self._input_type, length=self._input_length)
|
||||||
|
if self._input_length is None:
|
||||||
|
self._input_length = len(message)
|
||||||
self.input.put(message)
|
self.input.put(message)
|
||||||
elif self._input_type:
|
|
||||||
self.input.put(ensure_tuple(message, cls=self._input_type))
|
|
||||||
else:
|
|
||||||
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:
|
||||||
|
try:
|
||||||
|
if self._input_type == tuple:
|
||||||
|
input_bag = self._input_type(input_bag)
|
||||||
|
else:
|
||||||
|
input_bag = self._input_type(*input_bag)
|
||||||
|
except Exception as exc:
|
||||||
raise UnrecoverableTypeError(
|
raise UnrecoverableTypeError(
|
||||||
'Input type changed between calls to {!r}.\nGot {!r} which is not of type {!r}.'.format(
|
'Input type changed to incompatible type between calls to {!r}.\nGot {!r} which is not of type {!r}.'.
|
||||||
self.wrapped, input_bag, self._input_type
|
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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
16
bonobo/nodes/aggregation.py
Normal file
16
bonobo/nodes/aggregation.py
Normal 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))
|
||||||
@ -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):
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
from bonobo.plugins import Plugin
|
from bonobo.plugins import Plugin
|
||||||
from raven import Client
|
|
||||||
|
|
||||||
|
|
||||||
class SentryPlugin(Plugin):
|
class SentryPlugin(Plugin):
|
||||||
|
|||||||
@ -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).
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
2
setup.py
2
setup.py
@ -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.
|
||||||
|
|
||||||
|
|||||||
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.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
|
||||||
Reference in New Issue
Block a user