Making decorators with optional arguments

33,808

Solution 1

Glenn - I had to do it then. I guess I'm glad that there is not a "magic" way to do it. I hate those.

So, here's my own answer (method names different than above, but same concept):

from functools import wraps

def register_gw_method(method_or_name):
    """Cool!"""
    def decorator(method):
        if callable(method_or_name):
            method.gw_method = method.__name__
        else:
            method.gw_method = method_or_name
        @wraps(method)
        def wrapper(*args, **kwargs):
            method(*args, **kwargs)
        return wrapper
    if callable(method_or_name):
        return decorator(method_or_name)
    return decorator

Example usage (both versions work the same):

@register_gw_method
def my_function():
    print('hi...')

@register_gw_method('say_hi')
def my_function():
    print('hi...')

Solution 2

The cleanest way I know of for doing this is the following:

import functools


def decorator(original_function=None, optional_argument1=None, optional_argument2=None, ...):

    def _decorate(function):

        @functools.wraps(function)
        def wrapped_function(*args, **kwargs):
            ...

        return wrapped_function

    if original_function:
        return _decorate(original_function)

    return _decorate

Explanation

When the decorator is called with no optional arguments like this:

@decorator
def function ...

The function is passed as the first argument and decorate returns the decorated function, as expected.

If the decorator is called with one or more optional arguments like this:

@decorator(optional_argument1='some value')
def function ....

Then decorator is called with the function argument with value None, so a function that decorates is returned, as expected.

Python 3

Note that the decorator signature above may be improved with Python 3-specific *, syntax to enforce safe use of keyword arguments. Simply replace the signature of the outermost function with:

def decorator(original_function=None, *, optional_argument1=None, optional_argument2=None, ...):

Solution 3

Through the help of the answers here and elsewhere and a bunch of trial and error I've found that there is actually a far easier and generic way to make decorators take optional arguments. It does check the args it was called with but there isn't any other way to do it.

The key is to decorate your decorator.

Generic decorator decorator code

Here is the decorator decorator (this code is generic and can be used by anyone who needs an optional arg decorator):

def optional_arg_decorator(fn):
    def wrapped_decorator(*args):
        if len(args) == 1 and callable(args[0]):
            return fn(args[0])

        else:
            def real_decorator(decoratee):
                return fn(decoratee, *args)

            return real_decorator

    return wrapped_decorator

Usage

Using it is as easy as:

  1. Create a decorator like normal.
  2. After the first target function argument, add your optional arguments.
  3. Decorate the decorator with optional_arg_decorator

Example:

@optional_arg_decorator
def example_decorator_with_args(fn, optional_arg = 'Default Value'):
    ...
    return fn

Test cases

For your use case:

So for your case, to save an attribute on the function with the passed-in method name or the __name__ if None:

@optional_arg_decorator
def register_method(fn, method_name = None):
    fn.gw_method = method_name or fn.__name__
    return fn

Add decorated methods

Now you have a decorator that is usable with or without args:

@register_method('Custom Name')
def custom_name():
    pass

@register_method
def default_name():
    pass

assert custom_name.gw_method == 'Custom Name'
assert default_name.gw_method == 'default_name'

print 'Test passes :)'

Solution 4

How about

from functools import wraps, partial

def foo_register(method=None, string=None):
    if not callable(method):
        return partial(foo_register, string=method)
    method.gw_method = string or method.__name__
    @wraps(method)
    def wrapper(*args, **kwargs):
        method(*args, **kwargs)
    return wrapper

Solution 5

Enhanced Generic Decorator Decorator Code

Here's my adaption of @Nicole's answer with the following enhancements:

  • optional kwargs may be passed to the decorated decorator
  • the decorated decorator may be a bound method
import functools

def optional_arg_decorator(fn):
    @functools.wraps(fn)
    def wrapped_decorator(*args, **kwargs):
        is_bound_method = hasattr(args[0], fn.__name__) if args else False

        if is_bound_method:
            klass = args[0]
            args = args[1:]

        # If no arguments were passed...
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            if is_bound_method:
                return fn(klass, args[0])
            else:
                return fn(args[0])

        else:
            def real_decorator(decoratee):
                if is_bound_method:
                    return fn(klass, decoratee, *args, **kwargs)
                else:
                    return fn(decoratee, *args, **kwargs)
            return real_decorator
    return wrapped_decorator
Share:
33,808
orokusaki
Author by

orokusaki

I'm Michael Angeletti. I am a Python / Django developer, specializing in SaaS applications.

Updated on July 05, 2022

Comments

  • orokusaki
    orokusaki almost 2 years
    from functools import wraps
    
    def foo_register(method_name=None):
        """Does stuff."""
        def decorator(method):
            if method_name is None:
                method.gw_method = method.__name__
            else:
                method.gw_method = method_name
            @wraps(method)
            def wrapper(*args, **kwargs):
                method(*args, **kwargs)
            return wrapper
        return decorator
    

    Example: The following decorates my_function with foo_register instead of ever making it to decorator.

    @foo_register
    def my_function():
        print('hi...')
    

    Example: The following works as expected.

    @foo_register('say_hi')
    def my_function():
        print('hi...')
    

    If I want it to work correctly in both applications (one using method.__name__ and one passing the name in), I have to check inside of foo_register to see if the first argument is a decorator, and if so, I have to: return decorator(method_name) (instead of return decorator). This sort of "check to see if it's a callable" seems very hackish. Is there a nicer way to create a multi-use decorator like this?

    P.S. I already know that I can require the decorator to be called, but that's not a "solution". I want the API to feel natural. My wife loves decorating, and I don't want to ruin that.

  • Glenn Maynard
    Glenn Maynard over 13 years
    This is a function with radically different behavior depending on its arguments. That's what I mean by magic: it "figures out what you mean" instead of expecting the user to say what he means to begin with.
  • intuited
    intuited over 13 years
    FWIW, the whole concept of decorators is pretty magical. Not like Lucky Charms magical, but magical nonetheless. I think to make the wife really happy, there should be a decorator decorator in this situation that makes a decorator use default arguments if it's invoked with none. Of course this wouldn't work if it's actually passed a callable.
  • orokusaki
    orokusaki over 13 years
    @intuited - When I'm coding a decorator I feel like I'm watching the van/bridge scene from "Inception". I agree they aren't much fun to maintain, but they sure help to make a library more user friendly (ie, ugly implementation detail).
  • orokusaki
    orokusaki over 13 years
    @Aaron - why did you delete your answer? It was a great answer (which I just up-voted and attempted to comment on). (comment was: "@Aaron - 1+ clean and explicit FTW.")
  • aaronasterling
    aaronasterling over 13 years
    @orokusaki. I decided that I don't like splitting it because it's not an orthogonal split. The register method still knows about names and unless I can think of a better way to do it, then what you have is cleaner. Thanks for the upvote though.
  • intuited
    intuited over 13 years
    @orokusaki: I used to find them really confusing, but getting used to the idea of passing and returning functions, mostly just by doing a lot of it, has helped to make them only kind of confusing.
  • flying sheep
    flying sheep over 11 years
    there is no such thing as dead threads on stackoverflow. if the best answer comes too late for the original asker: tough luck. but for others discovering this later through searching, it’s always valuable to answer if your answer is valuable.
  • Simon Weber
    Simon Weber over 11 years
    careful: type is callable. Consider adding and not (type(args[0]) == type and issubclass(args[0], Exception)) to the condition in the case your decorator takes Exceptions as arguments (like this does).
  • Niklas B.
    Niklas B. over 11 years
    @Simon: Feel free to edit accordingly :)
  • Craig Labenz
    Craig Labenz almost 10 years
    If one accepts the requirement of keyword args instead of positional args (which I do), this is by far the best answer.
  • Michael Scott Asato Cuthbert
    Michael Scott Asato Cuthbert almost 9 years
    somehow I totally missed this answer when made my own... it's brilliant.
  • Michael Scott Asato Cuthbert
    Michael Scott Asato Cuthbert almost 9 years
    we should get this into functools -- are you on Python ideas?
  • Michael Scott Asato Cuthbert
    Michael Scott Asato Cuthbert almost 9 years
    (even though the fn-decorator is not exposed in the end. it's probably worth using "@wraps(fn)" before wrapped_decorator, so that things such as doctests will still run on the decorator)
  • Ryne Everett
    Ryne Everett almost 9 years
    I am subscribed to python ideas, but I wonder if this isn't too hacky/fragile for the standard library. For instance, is_bound_method would yield a false positive if the first argument passed to your free function had a property of the same name as the function.
  • Ryne Everett
    Ryne Everett almost 9 years
    @MichaelScottCuthbert Thanks for the doctests tip, I was not aware of that issue.
  • Claudiu
    Claudiu over 8 years
    Ah yes, figured that would work, was about to write this myself.. +1
  • Henry Gomersall
    Henry Gomersall about 8 years
    I concur this should be the answer. It's neat and clear.
  • ForeverWintr
    ForeverWintr almost 8 years
    You can enforce the keyword args only requirement by adding * to your function definition. E.g., def decorator(original_function=None, *, argument1=None, argument2=None, ...):
  • Andras Gyomrey
    Andras Gyomrey almost 7 years
    In case your decorator received a class as a parameter, this solution WON'T work, since callable(args[0]) returns True in both cases. When you're decorating the function AND when you're invoking it.
  • Cecil Curry
    Cecil Curry about 6 years
    @ForeverWintr I've incorporated your astute Python 3-specific recommendation into the original answer. We could even extend this a bit further with a clever Python 2.7 hack, but... it's hard to see the point. Python 2.7 is on the cusp of its well-deserved deathbed.
  • imanzabet
    imanzabet over 3 years
    RyneEverett and @Nicole Can you provide test case or example of your solution? Thanks