Skip to content

[BACKWARDS INCOMPATIBLE] Use exception instances instead of exc_info tuples #82

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Nov 24, 2019
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
15 changes: 5 additions & 10 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,16 @@ sudo: false

language: python
python:
- "2.7"
- "3.4"
- "3.5"
- "pypy"
matrix:
include:
- python: 3.7
dist: xenial
sudo: true
- "3.6"
- "3.7"
- "3.8"
- "pypy3"
install:
- pip install .
- pip install -r dev-requirements.txt
- pip install sphinx sphinx_rtd_theme
script:
- make lint
- flake8
- py.test
- make doc

Expand Down
3 changes: 0 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
lint:
flake8 --ignore=E131,E301,E302,E731,W503,E701,E704,E722 --max-line-length=100 effect/

build-dist:
rm -rf dist
python setup.py sdist bdist_wheel
Expand Down
3 changes: 1 addition & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ the effects (that is, IO or state manipulation) in your code. Documentation is
available at https://effect.readthedocs.org/, and its PyPI page is
https://pypi.python.org/pypi/effect.

It `supports`_ Python 2.7, 3.4 and 3.5 as well as PyPy.
It `supports`_ 3.6 and above.

.. _`supports`: https://travis-ci.org/python-effect/effect

Expand Down Expand Up @@ -44,7 +44,6 @@ A very quick example of using Effects:

.. code:: python

from __future__ import print_function
from effect import sync_perform, sync_performer, Effect, TypeDispatcher

class ReadLine(object):
Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Effect
Effect is a library for helping you write purely functional code by
isolating the effects (that is, IO or state manipulation) in your code.

It supports both Python 2.6 and up, and 3.4 and up, as well as PyPy.
It supports both Python 3.6 and up, as well as PyPy.

It lives on PyPI at https://pypi.python.org/pypi/effect and GitHub at
https://github.com/python-effect/effect.
Expand Down
2 changes: 1 addition & 1 deletion docs/source/intro.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ the ``on`` method:
def greet():
return get_user_name().on(
success=lambda r: Effect(Print("Hello,", r)),
error=lambda exc_info: Effect(Print("There was an error!", exc_info[1])))
error=lambda exc: Effect(Print("There was an error!", exc)))


(Here we assume another intent, ``Print``, which shows some text to the user.)
Expand Down
2 changes: 0 additions & 2 deletions effect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
See https://effect.readthedocs.org/ for documentation.
"""

from __future__ import absolute_import

from ._base import Effect, perform, NoPerformerFoundError, catch, raise_
from ._sync import (
NotSynchronousError,
Expand Down
52 changes: 20 additions & 32 deletions effect/_base.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
# -*- test-case-name: effect.test_base -*-
from __future__ import print_function, absolute_import

import sys

from functools import partial

import attr

import six

from ._continuation import trampoline


Expand All @@ -34,8 +28,7 @@ def on(self, success=None, error=None):
The result of the Effect will be passed to the first callback. Any
callbacks added afterwards will receive the result of the previous
callback. Normal return values are passed on to the next ``success``
callback, and exceptions are passed to the next ``error`` callback
as a ``sys.exc_info()`` tuple.
callback, and exceptions are passed to the next ``error`` callback.

If a callback returns an :obj:`Effect`, the result of that
:obj:`Effect` will be passed to the next callback.
Expand All @@ -62,7 +55,7 @@ def succeed(self, result):

def fail(self, result):
"""
Indicate that the effect has failed. result must be an exc_info tuple.
Indicate that the effect has failed. result must be an exception.
"""
self._cont((True, result))

Expand All @@ -71,13 +64,13 @@ def guard(f, *args, **kwargs):
"""
Run a function.

Return (is_error, result), where is_error is a boolean indicating whether
it raised an exception. In that case result will be ``sys.exc_info()``.
Return (is_error, result), where ``is_error`` is a boolean indicating whether
it raised an exception. In that case, ``result`` will be an exception.
"""
try:
return (False, f(*args, **kwargs))
except:
return (True, sys.exc_info())
except Exception as e:
return (True, e)


class NoPerformerFoundError(Exception):
Expand Down Expand Up @@ -110,7 +103,7 @@ def perform(dispatcher, effect):
or return another Effect, which will be recursively performed, such that
the result of the returned Effect becomes the result passed to the next
callback. In the case of exceptions, the next error-callback will be called
with a ``sys.exc_info()``-style tuple.
with the exception instance.

:returns: None

Expand All @@ -123,9 +116,8 @@ def perform(dispatcher, effect):
passed three arguments, not two: the dispatcher, the intent, and a
"box". The box is an object that lets the performer provide the result,
optionally asynchronously. To provide the result, use
``box.succeed(result)`` or ``box.fail(exc_info)``, where ``exc_info`` is
a ``sys.exc_info()``-style tuple. Decorators like :func:`sync_performer`
simply abstract this away.
``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
Expand Down Expand Up @@ -156,8 +148,7 @@ def _perform(bouncer, effect):
effect.intent,
_Box(partial(bouncer.bounce,
_run_callbacks, effect.callbacks)))
except:
e = sys.exc_info()
except Exception as e:
_run_callbacks(bouncer, effect.callbacks, (True, e))

trampoline(_perform, effect)
Expand All @@ -168,27 +159,24 @@ def catch(exc_type, callable):
A helper for handling errors of a specific type::

eff.on(error=catch(SpecificException,
lambda exc_info: "got an error!"))
lambda exc: "got an error!"))

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(exc_info):
if isinstance(exc_info[1], exc_type):
return callable(exc_info)
six.reraise(*exc_info)
def catcher(error):
if isinstance(error, exc_type):
return callable(error)
raise error
return catcher


def raise_(exception, tb=None):
"""Simple convenience function to allow raising exceptions from lambdas.

This is slightly more convenient than ``six.reraise`` because it takes an
exception instance instead of needing the type separate from the instance.
def raise_(exception):
"""Simple convenience function to allow raising exceptions as an expression,
useful in lambdas.

:param exception: An exception *instance* (not an exception type).

- ``raise_(exc)`` is the same as ``raise exc``.
- ``raise_(exc, tb)`` is the same as ``raise type(exc), exc, tb``.
``raise_(exc)`` is the same as ``raise exc``.
"""
six.reraise(type(exception), exception, tb)
raise exception
2 changes: 0 additions & 2 deletions effect/_dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

import attr

from six.moves import filter


@attr.s
class TypeDispatcher(object):
Expand Down
9 changes: 3 additions & 6 deletions effect/_intents.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
performed, sunch as :class:`Func`, :class:`Error`, and :class:`Constant`.
"""


from __future__ import print_function, absolute_import

import time

import attr
Expand Down Expand Up @@ -77,7 +74,7 @@ def parallel_all_errors(effects):
:param effects: Effects which should be performed in parallel.
:return: An Effect that results in a list of ``(is_error, result)`` tuples,
where ``is_error`` is True if the child effect raised an exception, in
which case ``result`` will be an exc_info tuple. If ``is_error`` is
which case ``result`` will be the exception. If ``is_error`` is
False, then ``result`` will just be the result as provided by the child
effect.
"""
Expand All @@ -93,12 +90,12 @@ class FirstError(Exception):
One of the effects in a :obj:`ParallelEffects` resulted in an error. This
represents the first such error that occurred.
"""
exc_info = attr.ib()
exception = attr.ib()
index = attr.ib()

def __str__(self):
return '(index=%s) %s: %s' % (
self.index, self.exc_info[0].__name__, self.exc_info[1])
self.index, type(self.exception).__name__, self.exception)


@attr.s
Expand Down
9 changes: 3 additions & 6 deletions effect/_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
Tools for dealing with Effects synchronously.
"""

import six
import sys

from ._base import perform
from ._utils import wraps

Expand All @@ -31,7 +28,7 @@ def sync_perform(dispatcher, effect):
if successes:
return successes[0]
elif errors:
six.reraise(*errors[0])
raise errors[0]
else:
raise NotSynchronousError("Performing %r was not synchronous!"
% (effect,))
Expand Down Expand Up @@ -70,6 +67,6 @@ def sync_wrapper(*args, **kwargs):
pass_args = args[:-1]
try:
box.succeed(f(*pass_args, **kwargs))
except:
box.fail(sys.exc_info())
except Exception as e:
box.fail(e)
return sync_wrapper
31 changes: 17 additions & 14 deletions effect/_test_utils.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""Another sad little utility module."""

import sys
import traceback

import attr

from testtools.matchers import Equals
from testtools.matchers import Equals, Mismatch


@attr.s
Expand All @@ -20,35 +19,39 @@ def describe(self):
+ ''.join(self.got_tb)
+ "\nbut it doesn't.")

@attr.s
class MatchesException(object):
expected = attr.ib()

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))
if other.args != self.expected.args:
return Mismatch('{} has different arguments: {}.'.format(
other.args, self.expected.args))


@attr.s
class MatchesReraisedExcInfo(object):

expected = attr.ib()

def match(self, actual):
valcheck = Equals(self.expected[1]).match(actual[1])
valcheck = Equals(self.expected.args).match(actual.args)
if valcheck is not None:
return valcheck
typecheck = Equals(self.expected[0]).match(actual[0])
typecheck = Equals(type(self.expected)).match(type(actual))
if typecheck is not None:
return typecheck
expected = traceback.format_exception(*self.expected)
new = traceback.format_exception(*actual)
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):]
if not tail_equals(expected[1:], new[1:]):
return ReraisedTracebackMismatch(expected_tb=expected,
got_tb=new)


def get_exc_info(exception):
"""Get an exc_info tuple based on an exception instance."""
try:
raise exception
except:
return sys.exc_info()


def raise_(e):
"""Raise an exception instance. Exists so you can raise in a lambda."""
raise e
2 changes: 0 additions & 2 deletions effect/async.py

This file was deleted.

9 changes: 2 additions & 7 deletions effect/do.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
See :func:`do`.
"""


from __future__ import print_function

import sys
import types

from . import Effect, Func
Expand Down Expand Up @@ -95,7 +91,7 @@ def foo():
def _do(result, generator, is_error):
try:
if is_error:
val = generator.throw(*result)
val = generator.throw(result)
else:
val = generator.send(result)
except StopIteration as stop:
Expand All @@ -104,8 +100,7 @@ def _do(result, generator, is_error):
# case where some other code is raising StopIteration up through this
# generator, in which case we shouldn't really treat it like a function
# return -- it could quite easily hide bugs.
tb = sys.exc_info()[2]
if tb.tb_next:
if stop.__traceback__.tb_next:
raise
else:
# Python 3 allows you to use `return val` in a generator, which
Expand Down
5 changes: 2 additions & 3 deletions effect/fold.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ class FoldError(Exception):
Raised when one of the Effects passed to :func:`fold_effect` fails.

:ivar accumulator: The data accumulated so far, before the failing Effect.
:ivar wrapped_exception: The exc_info tuple representing the original
exception raised by the failing Effect.
:ivar wrapped_exception: The original exception raised by the failing Effect.
"""
def __init__(self, accumulator, wrapped_exception):
self.accumulator = accumulator
self.wrapped_exception = wrapped_exception

def __str__(self):
tb_lines = traceback.format_exception(*self.wrapped_exception)
tb_lines = traceback.TracebackException.from_exception(self.wrapped_exception).format()
tb = ''.join(tb_lines)
st = (
"<FoldError after accumulating %r> Original traceback follows:\n%s"
Expand Down
Loading