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.
# Edit Projectfile and run “make update” (or “medikit update”) to regenerate.

View File

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

View File

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

View File

@ -1,12 +1,10 @@
import logging
import sys
from contextlib import contextmanager
from logging import ERROR
from mondrian import term
from bonobo.util import deprecated
from bonobo.util.objects import Wrapper, get_name
from mondrian import term
@contextmanager
@ -14,7 +12,7 @@ def recoverable(error_handler):
try:
yield
except Exception as exc: # pylint: disable=broad-except
error_handler(*sys.exc_info(), level=ERROR)
error_handler(*sys.exc_info(), level=logging.ERROR)
@contextmanager
@ -22,8 +20,8 @@ def unrecoverable(error_handler):
try:
yield
except Exception as exc: # pylint: disable=broad-except
error_handler(*sys.exc_info(), level=ERROR)
raise # raise unrecoverableerror from x ?
error_handler(*sys.exc_info(), level=logging.ERROR)
raise # raise unrecoverableerror from exc ?
class Lifecycle:
@ -55,12 +53,14 @@ class Lifecycle:
@property
def should_loop(self):
# TODO XXX started/stopped?
return not any((self.defunct, self.killed))
return self.alive and not any((self.defunct, self.killed))
@property
def status(self):
"""One character status for this node. """
"""
One character status for this node.
"""
if self._defunct:
return '!'
if not self.started:
@ -97,9 +97,6 @@ class Lifecycle:
self._stopped = True
if self._stopped: # Stopping twice has no effect
return
def kill(self):
if not self.started:
raise RuntimeError('Cannot kill an unstarted context.')
@ -134,3 +131,12 @@ class BaseContext(Lifecycle, Wrapper):
Lifecycle.__init__(self)
Wrapper.__init__(self, wrapped)
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 queue import Empty
from time import sleep
from bonobo.config import create_container
from bonobo.constants import BEGIN, END
from bonobo.errors import InactiveReadableError
from bonobo.execution import events
from bonobo.execution.contexts.base import BaseContext
from bonobo.execution.contexts.node import NodeExecutionContext
from bonobo.execution.contexts.plugin import PluginExecutionContext
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
PluginExecutionContextType = PluginExecutionContext
@ -17,17 +27,24 @@ class GraphExecutionContext:
@property
def started(self):
if not len(self.nodes):
return super(GraphExecutionContext, self).started
return any(node.started for node in self.nodes)
@property
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)
@property
def alive(self):
if not len(self.nodes):
return super(GraphExecutionContext, self).alive
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.graph = 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))
def start(self, starter=None):
super(GraphExecutionContext, self).start()
self.register_plugins()
self.dispatch(events.START)
self.tick(pause=False)
@ -89,13 +108,21 @@ class GraphExecutionContext:
if pause:
sleep(self.TICK_PERIOD)
def kill(self):
self.dispatch(events.KILL)
for node_context in self.nodes:
node_context.kill()
self.tick()
def loop(self):
nodes = set(node for node in self.nodes if node.should_loop)
while self.should_loop and len(nodes):
self.tick(pause=False)
for node in list(nodes):
try:
node.step()
except Empty:
continue
except InactiveReadableError:
nodes.discard(node)
def stop(self, stopper=None):
super(GraphExecutionContext, self).stop()
self.dispatch(events.STOP)
for node_context in self.nodes:
if stopper is None:
@ -106,6 +133,14 @@ class GraphExecutionContext:
self.dispatch(events.STOPPED)
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):
for plugin_context in self.plugins:
plugin_context.register()
@ -113,3 +148,11 @@ class GraphExecutionContext:
def unregister_plugins(self):
for plugin_context in self.plugins:
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):
"""
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):
"""
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()
self._stack = ContextCurrifier(self.wrapped, *initial.args, **initial.kwargs)
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
# the hard trouble of understanding that by himself.
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)
except Exception:
@ -102,9 +123,16 @@ class NodeExecutionContext(BaseContext, WithStatistics):
self.step()
except InactiveReadableError:
break
logger.debug('Node loop ends for {!r}.'.format(self))
def step(self):
try:
self._step()
except InactiveReadableError:
raise
except Empty:
sleep(TICK_PERIOD) # XXX: How do we determine this constant?
continue
except (
NotImplementedError,
UnrecoverableError,
@ -115,9 +143,7 @@ class NodeExecutionContext(BaseContext, WithStatistics):
except BaseException:
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.
@ -163,7 +189,7 @@ class NodeExecutionContext(BaseContext, WithStatistics):
if self._stack:
try:
self._stack.teardown()
except:
except Exception as exc:
self.fatal(sys.exc_info())
super().stop()
@ -231,12 +257,11 @@ class NodeExecutionContext(BaseContext, WithStatistics):
:param mixed value: message
"""
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)
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):
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.
"""
input_bag = self.input.get()
input_bag = self.input.get(timeout=0)
# Store or check input type
if self._input_type is None:
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(
'Input type changed between calls to {!r}.\nGot {!r} which is not of type {!r}.'.format(
self.wrapped, input_bag, self._input_type
)
)
'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
if self._input_length is None:

View File

@ -29,7 +29,7 @@ class ExecutorStrategy(Strategy):
with self.create_executor() as executor:
try:
context.start(self.get_starter(executor, futures))
except:
except Exception:
logger.critical('Exception caught while starting execution context.', exc_info=sys.exc_info())
while context.alive:
@ -53,14 +53,14 @@ class ExecutorStrategy(Strategy):
try:
with node:
node.loop()
except:
except Exception:
logging.getLogger(__name__).critical(
'Critical error in threadpool node starter.', exc_info=sys.exc_info()
)
try:
futures.append(executor.submit(_runner))
except:
except Exception:
logging.getLogger(__name__).critical('futures.append', exc_info=sys.exc_info())
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
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.constants import NOT_MODIFIED
from bonobo.nodes.io.base import FileHandler
from bonobo.nodes.io.file import FileReader, FileWriter
from bonobo.util import ensure_tuple
from bonobo.util.bags import BagType
class CsvHandler(FileHandler):

View File

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

View File

@ -89,6 +89,7 @@ class Registry:
default_registry = Registry()
def create_reader(name, *args, format=None, registry=default_registry, **kwargs):
"""
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)
def create_writer(name, *args, format=None, registry=default_registry, **kwargs):
"""
Create a writer instance, guessing its factory using filename (and eventually format).

View File

@ -52,7 +52,7 @@ class Setting:
def set(self, value):
value = self.formatter(value) if self.formatter else 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
def set_if_true(self, value):

View File

@ -7,7 +7,23 @@ class sortedlist(list):
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.
Otherwise, not changed.
@ -16,6 +32,8 @@ def ensure_tuple(tuple_or_mixed, *, cls=tuple):
:return: tuple
"""
if cls is None:
cls = tuple
if isinstance(tuple_or_mixed, cls):
return tuple_or_mixed

View File

@ -57,7 +57,6 @@ def get_argument_parser(parser=None):
:return:
"""
if parser is None:
import argparse
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

View File

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

View File

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

View File

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

View File

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

View File

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