2

Note: I know that decorators with optional argument contain three nested function. But optional argument here is function itself. Please go through the complete post before you mark this as duplicate. I already tried all the tricks for decorators with optional argument, but I could not found any that takes function as argument.

I am having a decorator for wrapping error:

def wrap_error(func):
    from functools import wraps

    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except:
            import sys

            exc_msg = traceback.format_exception(*sys.exc_info())
            raise MyCustomError(exc_msg)

    return wrapper

If some function raises any exception, it wraps the error. This wrapper is used like:

@wrap_error
def foo():
    ...

Now I want to modify this wrapper with additional callback function which will be optional. And I want this wrapper to be used as:

@wrap_error
def foo():
    ...

@wrap_error(callback)
def foo():
    ...

I know how to write decorators with optional arguments (in case passed argument is not function, based on isfunction(func) check within wrapper). But I am not sure how to handle this case.

Note: I can not use @wrap_error() instead of @wrap_error. This wrapper is used in multiple number of packages, and it is not possible to update the change in all

Here is the blocker: Consider the wrapper as:

@wrap_error(callback)               --->       foo = wrap_error(callback)(foo)
def foo():
    ...

So, by the time wrap_error(foo) is executed, we do not know whether there will be any callback function for execution after that or not (in case we use just @wrap_error instead of @wrap_error(callback)).

If there is no (callback), wrapping function within wrap_error will return func(*args. **kwargs) so that I can raise exception. Else we have to return func so that it is called at next step, and if func() raises the exception, we call callback() in except block.

Moinuddin Quadri
  • 46,825
  • 13
  • 96
  • 126
  • 1
    Decorators that take arguments are different; they have three layers of `def`, not two. You could try to write something that either returns a decorator or decorated function, depending on how it's called, but this would be very difficult to implement; how can you tell the difference between being called with a `callback` and with the function to be wrapped? Both are just callable objects. – jonrsharpe Nov 27 '14 at 09:41
  • Have you thought about passing either a function (to be wrapped) or string (name of callback)? You would have to have some mapping of the valid callbacks, maybe some method of registration, but that would allow you to use it as you want. – jonrsharpe Nov 27 '14 at 09:50
  • @jonrsharpe it is possible to do, though; Django does it in its [template tag decorators](https://github.com/django/django/blob/master/django/template/base.py#L1065). – Daniel Roseman Nov 27 '14 at 09:52
  • Ah, except I see your point about confusion between the wrapped func and the callback. Ignore me. – Daniel Roseman Nov 27 '14 at 09:53
  • Question updated. I know that decorators with optional argument contain three nested function. But optional argument here is **function** itself. Please go thorough the complete post before you mark this as duplicate. I already tried all the tricks for decorators with optional argument, but I could not found any that takes **function** as argument. – Moinuddin Quadri Nov 27 '14 at 09:59
  • @jonrsharpe: "how can you tell the difference between being called with a callback and with the function to be wrapped?" That is the issue which I am facing. Trying to find some workaround for this. – Moinuddin Quadri Nov 27 '14 at 10:03
  • Looks like you can't: http://stackoverflow.com/questions/17119154/python-decorator-optional-argument - so the easiest way around it is probably to make two decorators: `wrap_error` and `wrap_error_callback`, or to use keyword arguments, or multiple arguments. – Pieter Witvoet Nov 27 '14 at 10:31
  • You can, but it involves tagging the callback so the decorator can tell the difference. In my answer I used a decorator to tag the callback, so you still have to write two decorators. I think just having two decorators with different names is cleaner. – Mark Tolonen Nov 28 '14 at 05:10

2 Answers2

2

To summarise the problem before attempting to answer it, you want a decorator that works correctly in both of the following contexts:

@decorator  # case 1
def some_func(...):
    ...

@decorator(some_callback)  # case 2
def some_func(...):
    ...

or, to unroll the @ syntax to clarify things:

some_func = decorator(some_func)  # case 1

some_func = decorator(some_callback)(some_func)  # case 2

The tricky issue here, as I see it, is that it's very hard for decorator to tell the difference between some_func and some_callback (and therefore between cases 1 and 2); both are (presumably) just callable objects.


One potential solution is to provide named arguments:

# imports at top of file, not in function definitions
from functools import wraps
import sys

def decorator(func=None, callback=None):
    # Case 1
    if func is not None:
        @wraps(func)
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)  # or whatever
        return wrapper
    # Case 2
    elif callback is not None: 
        def deco(f):
            @wraps(f)
            def wrapper(*args, **kwargs):
                return callback(f(*args, **kwargs))  # or whatever
            return wrapper
        return deco

This makes case 2 look slightly different:

@decorator(callback=some_callback)
def some_func(...):
    ...

But otherwise does what you want. Note that the option you say you can't use

@decorator()
def some_func(...):
    ...

won't work with this, as the decorator expects either func or callback to be supplied (it will return None otherwise, which isn't callable, so you'll get a TypeError).

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • I tried this. but was not able to make this work. Can you please paste the complete code regarding your suggestion? – Moinuddin Quadri Nov 27 '14 at 14:16
  • @MoinuddinQuadri I have updated, but more information than *"not able to make this work"* would be helpful. – jonrsharpe Nov 27 '14 at 14:20
  • @jonrshape: I meant that the code you have written is not working as expected. It is not wrapping the exceptions. Still, thanks for the response. – Moinuddin Quadri Nov 27 '14 at 15:05
  • @MoinuddinQuadri could you be more specific? Errors? Unexpected behaviour? My demonstration is just to show how a decorator would work, you will have to adapt it to your specific needs. – jonrsharpe Nov 27 '14 at 15:06
  • @johnsharpe: Updated you answer with the issue. – Moinuddin Quadri Nov 27 '14 at 15:18
  • @MoinuddinQuadri that is *not* an appropriate use of the edit function. If I try a simple `def callback(): print "Calling back"` your suggested code works fine for me - I see `Calling back` then the traceback from the `raise`. And *please* stop adding `import` inside nested functions - all imports should be at the top of the script. – jonrsharpe Nov 27 '14 at 15:21
  • @johnsharpei: I am using `@decorator(callback=callback)` with function `foo` that raises an error. The code snippet I forwarded is not wrapping up that error when I call `foo()` And I agree, all imports should be at top. It was just test code :) – Moinuddin Quadri Nov 27 '14 at 15:26
  • @MoinuddinQuadri because *you don't ask it to*, in the `if callback is not None` branch. If you want to use `MyCustomError` in both branches, you will have to add it in *both versions of `wrapper`*. – jonrsharpe Nov 27 '14 at 15:27
2

Since it is hard to tell decorator(func) from decorator(callback), make two decorators:

from functools import wraps

class MyCustomError(Exception):
    def __init__(self):
        print('in MyCustomError')

# Common implementation
def wrap(func,cb=None):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except:
            if cb is not None:
                cb()
            raise MyCustomError()
    return wrapper

# No parameters version
def wrap_error(func):
    return wrap(func)

# callback parameter version
def wrap_error_cb(cb):
    def deco(func):
        return wrap(func,cb)
    return deco

@wrap_error
def foo(a,b):
    print('in foo',a,b)
    raise Exception('foo exception')

def callback():
    print('in callback')

@wrap_error_cb(callback)
def bar(a):
    print('in bar',a)
    raise Exception('bar exception')

Check that foo and bar are correctly using functools.wraps:

>>> foo
<function foo at 0x0000000003F00400>
>>> bar
<function bar at 0x0000000003F00598>

Check that the wrapped functions work:

>>> foo(1,2)
in foo 1 2
in MyCustomError
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
  File "C:\test.py", line 16, in wrapper
    raise MyCustomError()
MyCustomError
>>> bar(3)
in bar 3
in callback
in MyCustomError
Traceback (most recent call last):
  File "<interactive input>", line 1, in <module>
  File "C:\test.py", line 16, in wrapper
    raise MyCustomError()
MyCustomError

Updated

Here's a way to do it with the syntax you requested, but I think the above answer is clearer.

from functools import wraps

class MyCustomError(Exception):
    def __init__(self):
        print('in MyCustomError')

# Common implementation
def wrap(func,cb=None):
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except:
            if cb is not None:
                cb()
            raise MyCustomError()
    return wrapper

def wrap_error(func_or_cb):
    # If the function is tagged as a wrap_error_callback
    # return a decorator that returns the wrapped function
    # with a callback.
    if hasattr(func_or_cb,'cb'):
        def deco(func):
            return wrap(func,func_or_cb)
        return deco
    # Otherwise, return a wrapped function without a callback.
    return wrap(func_or_cb)

# decorator to tag callbacks so wrap_error can distinguish them
# from *regular* functions.
def wrap_error_callback(func):
    func.cb = True
    return func

### Examples of use

@wrap_error
def foo(a,b):
    print('in foo',a,b)
    raise Exception('foo exception')

@wrap_error_callback
def callback():
    print('in callback')

@wrap_error(callback)
def bar(a):
    print('in bar',a)
    raise Exception('bar exception')
Mark Tolonen
  • 166,664
  • 26
  • 169
  • 251
  • Is there a way to merge both into one? – Moinuddin Quadri Nov 27 '14 at 12:58
  • It's not really practical. You have two different behaviors, so use two functions. Why make it complicated? @jonrsharpe's provides a way with named parameters, but not your specified syntax. – Mark Tolonen Nov 28 '14 at 00:06
  • I did come up with a way to do it with your syntax. It involves using another decorator to tag the callback function so it can be differentiated from the wrapped function. See updated answer. – Mark Tolonen Nov 28 '14 at 05:00