2
\$\begingroup\$

I want my Python decorators to have optional arguments and not be called when not necessary.

The accepted answer in here doesn't accept named arguments, and I don't want to add boilerplate code inside decorators, so I came up with an alternative decorator:

import inspect

def decorator_defaults(**defined_defaults):
    def decorator(f):
        args_names = inspect.getargspec(f)[0]

        def wrapper(*new_args, **new_kwargs):
            defaults = dict(defined_defaults, **new_kwargs)
            if len(new_args) == 0:
                return f(**defaults)
            elif len(new_args) == 1 and callable(new_args[0]):
                return f(**defaults)(new_args[0])
            else:
                too_many_args = False
                if len(new_args) > len(args_names):
                    too_many_args = True
                else:
                    for i in range(len(new_args)):
                        arg = new_args[i]
                        arg_name = args_names[i]
                        defaults[arg_name] = arg
                if len(defaults) > len(args_names):
                    too_many_args = True
                if not too_many_args:
                    final_defaults = []
                    for name in args_names:
                        final_defaults.append(defaults[name])
                    return f(*final_defaults)
                if too_many_args:
                    raise TypeError("{0}() takes {1} argument(s) "
                                    "but {2} were given".
                                    format(f.__name__,
                                           len(args_names),
                                           len(defaults)))
        return wrapper
    return decorator

Two sample decorators:

from functools import wraps

@decorator_defaults(start_val="-=[", end_val="]=-")
def my_text_decorator(start_val, end_val):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            return "".join([f.__name__, ' ', start_val,
                            f(*args, **kwargs), end_val])
        return wrapper
    return decorator


@decorator_defaults(end_val="]=-")
def my_text_decorator2(start_val, end_val):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            return "".join([f.__name__, ' ', start_val,
                            f(*args, **kwargs), end_val])
        return wrapper
    return decorator

And usage of sample text decorators:

@my_text_decorator
def func1a(value):
    return value


@my_text_decorator()
def func2a(value):
    return value


@my_text_decorator2("-=[")
def func2b(value):
    return value


@my_text_decorator(end_val=" ...")
def func3a(value):
    return value


@my_text_decorator2("-=[", end_val=" ...")
def func3b(value):
    return value


@my_text_decorator("|> ", " <|")
def func4a(value):
    return value


@my_text_decorator2("|> ", " <|")
def func4b(value):
    return value


@my_text_decorator(end_val=" ...", start_val="|> ")
def func5a(value):
    return value


@my_text_decorator2("|> ", end_val=" ...")
def func5b(value):
    return value


print(func1a('My sample text'))  # func1a -=[My sample text]=-
print(func2a('My sample text'))  # func2a -=[My sample text]=-
print(func2b('My sample text'))  # func2b -=[My sample text]=-
print(func3a('My sample text'))  # func3a -=[My sample text ...
print(func3b('My sample text'))  # func3b -=[My sample text ...
print(func4a('My sample text'))  # func4a |> My sample text <|
print(func4b('My sample text'))  # func4b |> My sample text <|
print(func5a('My sample text'))  # func5a |> My sample text ...
print(func5b('My sample text'))  # func5b |> My sample text ...

decorator_defaults works, but I believe it could be written better. I'm not that experienced in Python, so I would like to hear some ideas/comments on how to improve it.

\$\endgroup\$
5
\$\begingroup\$

The code looks rather complicated. Instead of trying to understand it, I'd just like to point to NickC's answer to the linked SO question.

If I add **kwargs to his optional_arg_decorator like this...

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

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

            return real_decorator

    return wrapped_decorator    

...and adapt your decorators like this, I'm getting the same output from the test cases.

from functools import wraps

@optional_arg_decorator
def my_text_decorator(f, start_val="-=[", end_val="]=-"):
    @wraps(f)
    def wrapper(*args, **kwargs):
        return "".join([f.__name__, ' ', start_val,
                        f(*args, **kwargs), end_val])
    return wrapper

@optional_arg_decorator
def my_text_decorator2(f, start_val, end_val="]=-"):
    @wraps(f)
    def wrapper(*args, **kwargs):
        return "".join([f.__name__, ' ', start_val,
                        f(*args, **kwargs), end_val])
    return wrapper
\$\endgroup\$
  • \$\begingroup\$ The wrapt.decorator is like @wraps but supports python 2.7 as well. \$\endgroup\$ – James Draper Aug 11 '17 at 16:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.