Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@ install:
- pip install .
- pip install -r dev-requirements.txt
- pip install sphinx sphinx_rtd_theme
# black isn't installing on pypy3, so just skip it
- 'if [ "$TRAVIS_PYTHON_VERSION" != "pypy3" ]; then pip install black; fi'
script:
- flake8
- py.test
- 'if [ "$TRAVIS_PYTHON_VERSION" != "pypy3" ]; then black --check .; fi'
- pytest
- make doc

notifications:
Expand Down
18 changes: 9 additions & 9 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-

extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
"sphinx.ext.autodoc",
"sphinx.ext.viewcode",
]

# include __init__ docstrings in class docstrings
autoclass_content = 'both'
autoclass_content = "both"

source_suffix = '.rst'
master_doc = 'index'
project = u'Effect'
copyright = u'2015, Christopher Armstrong'
version = release = '0.12.0+'
source_suffix = ".rst"
master_doc = "index"
project = u"Effect"
copyright = u"2015, Christopher Armstrong"
version = release = "0.12.0+"

html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
45 changes: 30 additions & 15 deletions effect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,42 @@
"""

from ._base import Effect, perform, NoPerformerFoundError, catch, raise_
from ._sync import (
NotSynchronousError,
sync_perform,
sync_performer)
from ._sync import NotSynchronousError, sync_perform, sync_performer
from ._intents import (
Delay, perform_delay_with_sleep,
ParallelEffects, parallel, parallel_all_errors, FirstError,
Constant, Error, Func,
base_dispatcher)
Delay,
perform_delay_with_sleep,
ParallelEffects,
parallel,
parallel_all_errors,
FirstError,
Constant,
Error,
Func,
base_dispatcher,
)
from ._dispatcher import ComposedDispatcher, TypeDispatcher


__all__ = [
# Order here affects the order that these things show up in the API docs.
"Effect", "sync_perform", "sync_performer",
"Effect",
"sync_perform",
"sync_performer",
"base_dispatcher",
"TypeDispatcher", "ComposedDispatcher",
"Delay", "perform_delay_with_sleep",
"ParallelEffects", "parallel", "parallel_all_errors",
"Constant", "Error", "Func",
"catch", "raise_",
"NoPerformerFoundError", "NotSynchronousError", "perform",
"TypeDispatcher",
"ComposedDispatcher",
"Delay",
"perform_delay_with_sleep",
"ParallelEffects",
"parallel",
"parallel_all_errors",
"Constant",
"Error",
"Func",
"catch",
"raise_",
"NoPerformerFoundError",
"NotSynchronousError",
"perform",
"FirstError",
]
15 changes: 9 additions & 6 deletions effect/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ def on(self, success=None, error=None):
If a callback returns an :obj:`Effect`, the result of that
:obj:`Effect` will be passed to the next callback.
"""
return Effect(self.intent,
callbacks=self.callbacks + [(success, error)])
return Effect(self.intent, callbacks=self.callbacks + [(success, error)])


class _Box(object):
"""
An object into which an effect dispatcher can place a result.
"""

def __init__(self, cont):
"""
:param callable cont: Called with (bool is_error, result)
Expand Down Expand Up @@ -119,13 +119,14 @@ def perform(dispatcher, effect):
``box.succeed(result)`` or ``box.fail(exc)``, where ``exc`` is
an exception. Decorators like :func:`sync_performer` simply abstract this away.
"""

def _run_callbacks(bouncer, chain, result):
is_error, value = result

if type(value) is Effect:
bouncer.bounce(
_perform,
Effect(value.intent, callbacks=value.callbacks + chain))
_perform, Effect(value.intent, callbacks=value.callbacks + chain)
)
return

if not chain:
Expand All @@ -146,8 +147,8 @@ def _perform(bouncer, effect):
performer(
dispatcher,
effect.intent,
_Box(partial(bouncer.bounce,
_run_callbacks, effect.callbacks)))
_Box(partial(bouncer.bounce, _run_callbacks, effect.callbacks)),
)
except Exception as e:
_run_callbacks(bouncer, effect.callbacks, (True, e))

Expand All @@ -164,10 +165,12 @@ def catch(exc_type, callable):
If any exception other than a ``SpecificException`` is thrown, it will be
ignored by this handler and propogate further down the chain of callbacks.
"""

def catcher(error):
if isinstance(error, exc_type):
return callable(error)
raise error

return catcher


Expand Down
3 changes: 2 additions & 1 deletion effect/_continuation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def bounce(self, func, *args, **kwargs):
if self.work is not None:
raise RuntimeError(
"Already specified work %r, refusing to set to (%r %r %r)"
% (self.work, func, args, kwargs))
% (self.work, func, args, kwargs)
)
self.work = (func, args, kwargs)
if self._asynchronous:
trampoline(func, *args, **kwargs)
Expand Down
1 change: 1 addition & 0 deletions effect/_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class TypeDispatcher(object):

:param mapping: mapping of intent type to performer
"""

mapping = attr.ib()

def __call__(self, intent):
Expand Down
27 changes: 17 additions & 10 deletions effect/_intents.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,10 @@ def parallel_all_errors(effects):
False, then ``result`` will just be the result as provided by the child
effect.
"""
effects = [effect.on(success=lambda r: (False, r),
error=lambda e: (True, e))
for effect in effects]
effects = [
effect.on(success=lambda r: (False, r), error=lambda e: (True, e))
for effect in effects
]
return Effect(ParallelEffects(list(effects)))


Expand All @@ -90,12 +91,16 @@ class FirstError(Exception):
One of the effects in a :obj:`ParallelEffects` resulted in an error. This
represents the first such error that occurred.
"""

exception = attr.ib()
index = attr.ib()

def __str__(self):
return '(index=%s) %s: %s' % (
self.index, type(self.exception).__name__, self.exception)
return "(index=%s) %s: %s" % (
self.index,
type(self.exception).__name__,
self.exception,
)


@attr.s
Expand All @@ -108,6 +113,7 @@ class Delay(object):

:param float delay: The number of seconds to delay.
"""

delay = attr.ib()


Expand All @@ -124,6 +130,7 @@ class Constant(object):

:param result: The object which the Effect will result in.
"""

result = attr.ib()


Expand All @@ -140,6 +147,7 @@ class Error(object):

:param BaseException exception: Exception instance to raise.
"""

exception = attr.ib()


Expand Down Expand Up @@ -173,6 +181,7 @@ class Func(object):
:param args: Positional arguments to pass to the function.
:param kwargs: Keyword arguments to pass to the function.
"""

func = attr.ib()
args = attr.ib()
kwargs = attr.ib()
Expand All @@ -189,8 +198,6 @@ def perform_func(dispatcher, intent):
return intent.func(*intent.args, **intent.kwargs)


base_dispatcher = TypeDispatcher({
Constant: perform_constant,
Error: perform_error,
Func: perform_func,
})
base_dispatcher = TypeDispatcher(
{Constant: perform_constant, Error: perform_error, Func: perform_func}
)
5 changes: 3 additions & 2 deletions effect/_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ def sync_perform(dispatcher, effect):
elif errors:
raise errors[0]
else:
raise NotSynchronousError("Performing %r was not synchronous!"
% (effect,))
raise NotSynchronousError("Performing %r was not synchronous!" % (effect,))


def sync_performer(f):
Expand Down Expand Up @@ -61,6 +60,7 @@ def sync_performer(f):
def perform_foo(dispatcher, foo):
return do_side_effect(foo)
"""

@wraps(f)
def sync_wrapper(*args, **kwargs):
box = args[-1]
Expand All @@ -69,4 +69,5 @@ def sync_wrapper(*args, **kwargs):
box.succeed(f(*pass_args, **kwargs))
except Exception as e:
box.fail(e)

return sync_wrapper
29 changes: 17 additions & 12 deletions effect/_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ class ReraisedTracebackMismatch(object):
got_tb = attr.ib()

def describe(self):
return ("The reference traceback:\n"
+ ''.join(self.expected_tb)
+ "\nshould match the tail end of the received traceback:\n"
+ ''.join(self.got_tb)
+ "\nbut it doesn't.")
return (
"The reference traceback:\n"
+ "".join(self.expected_tb)
+ "\nshould match the tail end of the received traceback:\n"
+ "".join(self.got_tb)
+ "\nbut it doesn't."
)


@attr.s
class MatchesException(object):
Expand All @@ -26,10 +29,11 @@ class MatchesException(object):
def match(self, other):
expected_type = type(self.expected)
if type(other) is not expected_type:
return Mismatch('{} is not a {}'.format(other, expected_type))
return Mismatch("{} is not a {}".format(other, expected_type))
if other.args != self.expected.args:
return Mismatch('{} has different arguments: {}.'.format(
other.args, self.expected.args))
return Mismatch(
"{} has different arguments: {}.".format(other.args, self.expected.args)
)


@attr.s
Expand All @@ -44,12 +48,13 @@ def match(self, actual):
typecheck = Equals(type(self.expected)).match(type(actual))
if typecheck is not None:
return typecheck
expected = list(traceback.TracebackException.from_exception(self.expected).format())
expected = list(
traceback.TracebackException.from_exception(self.expected).format()
)
new = list(traceback.TracebackException.from_exception(actual).format())
tail_equals = lambda a, b: a == b[-len(a):]
tail_equals = lambda a, b: a == b[-len(a) :]
if not tail_equals(expected[1:], new[1:]):
return ReraisedTracebackMismatch(expected_tb=expected,
got_tb=new)
return ReraisedTracebackMismatch(expected_tb=expected, got_tb=new)


def raise_(e):
Expand Down
2 changes: 2 additions & 0 deletions effect/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def wraps(original):
This is like :func:`functools.wraps`, except you can wrap non-functions
without blowing up.
"""

def wraps_decorator(wrapper):
try:
wrapper.__name__ = original.__name__
Expand All @@ -19,4 +20,5 @@ def wraps_decorator(wrapper):
except:
pass
return wrapper

return wraps_decorator
22 changes: 13 additions & 9 deletions effect/do.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,23 @@ def foo():
(This decorator is named for Haskell's ``do`` notation, which is similar in
spirit).
"""

@wraps(f)
def do_wrapper(*args, **kwargs):

def doit():
gen = f(*args, **kwargs)
if not isinstance(gen, types.GeneratorType):
raise TypeError(
"%r is not a generator function. It returned %r."
% (f, gen))
"%r is not a generator function. It returned %r." % (f, gen)
)
return _do(None, gen, False)

fname = getattr(f, '__name__', None)
fname = getattr(f, "__name__", None)
if fname is not None:
doit.__name__ = 'do_' + fname
doit.__name__ = "do_" + fname

return Effect(Func(doit))

return do_wrapper


Expand Down Expand Up @@ -108,13 +109,16 @@ def _do(result, generator, is_error):
# set to the return value. So we'll return that value as the
# ultimate result of the effect. Python 2 doesn't have the 'value'
# attribute of StopIteration, so we'll fall back to None.
return getattr(stop, 'value', None)
return getattr(stop, "value", None)
if type(val) is _ReturnSentinel:
return val.result
elif type(val) is Effect:
return val.on(success=lambda r: _do(r, generator, False),
error=lambda e: _do(e, generator, True))
return val.on(
success=lambda r: _do(r, generator, False),
error=lambda e: _do(e, generator, True),
)
else:
raise TypeError(
"@do functions must only yield Effects or results of do_return. "
"Got %r from %r" % (val, generator))
"Got %r from %r" % (val, generator)
)
Loading