Skip to content
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

ENH: Adds excepts #262

Merged
merged 7 commits into from
Jan 2, 2016
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
112 changes: 111 additions & 1 deletion toolz/functoolz.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from functools import reduce, partial
import inspect
import operator
from operator import attrgetter
from textwrap import dedent
import sys


__all__ = ('identity', 'thread_first', 'thread_last', 'memoize', 'compose',
'pipe', 'complement', 'juxt', 'do', 'curry', 'flip')
'pipe', 'complement', 'juxt', 'do', 'curry', 'flip', 'excepts')


def identity(x):
Expand Down Expand Up @@ -568,3 +570,111 @@ def flip(func, a, b):
[1, 2, 3]
"""
return func(b, a)


def return_none(exc):
"""Returns None.
"""
return None


class _ExceptsDoc(object):
"""A descriptor that allows us to get the docstring for both the
`excepts` class and generate a custom docstring for the instances of
excepts.

Parameters
----------
class_doc : str
The docstring for the excepts class.
"""
def __init__(self, class_doc):
self._class_doc = class_doc

def __get__(self, instance, owner):
if instance is None:
return self._class_doc

exc = instance.exc
try:
if isinstance(exc, tuple):
exc_name = '(%s)' % ', '.join(
map(attrgetter('__name__'), exc),
)
else:
exc_name = exc.__name__

return dedent(
"""\
A wrapper around {inst.f.__name__!r} that will except:
{exc}
and handle any exceptions with {inst.handler.__name__!r}.

Docs for {inst.f.__name__!r}:
{inst.f.__doc__}

Docs for {inst.handler.__name__!r}:
{inst.handler.__doc__}
"""
).format(
inst=instance,
exc=exc_name,
)
except AttributeError:
return self._class_doc


class excepts(object):
"""A wrapper around a function to catch exceptions and
dispatch to a handler.

This is like a functional try/except block, in the same way that
ifexprs are functional if/else blocks.

Examples
--------
>>> excepting = excepts(
... ValueError,
... lambda a: [1, 2].index(a),
... lambda _: -1,
... )
>>> excepting(1)
0
>>> excepting(3)
-1

Multiple exceptions and default except clause.
>>> excepting = excepts((IndexError, KeyError), lambda a: a[0])
>>> excepting([])
>>> excepting([1])
1
>>> excepting({})
>>> excepting({0: 1})
1
"""
# override the docstring above with a descritor that can return
# an instance-specific docstring
__doc__ = _ExceptsDoc(__doc__)

def __init__(self, exc, f, handler=return_none):
self.exc = exc
self.f = f
self.handler = handler

def __call__(self, *args, **kwargs):
try:
return self.f(*args, **kwargs)
except self.exc as e:
return self.handler(e)

@property
def __name__(self):
exc = self.exc
try:
if isinstance(exc, tuple):
exc_name = '_or_'.join(map(attrgetter('__name__'), exc))
else:
exc_name = exc.__name__
return '%s_excepting_%s' % (self.f.__name__, exc_name)
except AttributeError:
return 'excepting'
63 changes: 62 additions & 1 deletion toolz/tests/test_functoolz.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


from toolz.functoolz import (thread_first, thread_last, memoize, curry,
compose, pipe, complement, do, juxt, flip)
compose, pipe, complement, do, juxt, flip, excepts)
from toolz.functoolz import _num_required_args
from operator import add, mul, itemgetter
from toolz.utils import raises
Expand Down Expand Up @@ -508,3 +508,64 @@ def f(a, b):
return a, b

assert flip(f, 'a', 'b') == ('b', 'a')


def test_excepts():
# These are descriptors, make sure this works correctly.
assert excepts.__name__ == 'excepts'
assert excepts.__doc__.startswith(
'A wrapper around a function to catch exceptions and\n'
' dispatch to a handler.\n'
)

def idx(a):
"""idx docstring
"""
return [1, 2].index(a)

def handler(e):
"""handler docstring
"""
assert isinstance(e, ValueError)
return -1

excepting = excepts(ValueError, idx, handler)
assert excepting(1) == 0
assert excepting(2) == 1
assert excepting(3) == -1

assert excepting.__name__ == 'idx_excepting_ValueError'
assert 'idx docstring' in excepting.__doc__
assert 'ValueError' in excepting.__doc__
assert 'handler docstring' in excepting.__doc__

def getzero(a):
"""getzero docstring
"""
return a[0]

excepting = excepts((IndexError, KeyError), getzero)
assert excepting([]) is None
assert excepting([1]) == 1
assert excepting({}) is None
assert excepting({0: 1}) == 1

assert excepting.__name__ == 'getzero_excepting_IndexError_or_KeyError'
assert 'getzero docstring' in excepting.__doc__
assert 'return_none' in excepting.__doc__
assert 'Returns None' in excepting.__doc__

def raise_(a):
"""A function that raises an instance of the exception type given.
"""
raise a()

excepting = excepts((ValueError, KeyError), raise_)
assert excepting(ValueError) is None
assert excepting(KeyError) is None
assert raises(TypeError, lambda: excepting(TypeError))
assert raises(NotImplementedError, lambda: excepting(NotImplementedError))

excepting = excepts(object(), object(), object())
assert excepting.__name__ == 'excepting'
assert excepting.__doc__ == excepts.__doc__
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to test that we only pass on the provided Exception type. A small test that raises a different kind of error than the provided type would be useful.