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

How do I create a "signature-changing" decorator with decorator.py? #71

Closed
mrmachine opened this issue Jul 16, 2019 · 4 comments
Closed

Comments

@mrmachine
Copy link

The docs (https://github.com/micheles/decorator/blob/master/docs/documentation.md#decoratordecorator) say:

The decorator function can be used as a signature-changing decorator, just like classmethod and staticmethod.

But then goes on to say:

But classmethod and staticmethod return generic objects which are not callable. Instead, decorator returns signature-preserving decorators (i.e. functions with a single argument).

And the following trivial example doesn't appear to change the signature at all?

What I want to do is create a decorator that will add a few optional keyword arguments to the decorated function. At first I thought I should be able to do something like:

@decorator.decorator
def mydec(func, new_kw=None, another_new_kw=None, *args, **kwargs):
    if new_kw:
        # do something
    return func(*args, **kwargs)

But this returns a decorator factory (also useful, but not for me in this case).

Then, I thought I could do it with decorate, like this:

def _mydec(func, new_kw=None, another_new_kw=None, *args, **kwargs):
    if new_kw:
        # do something else when the decorated function is executed
    return func(*args, **kwargs)

def mydec(func):
    newfunc = decorator.decorate(func, _mydec)
    # do something else with func when the decorator is created
    return newfunc

But this doesn't work. The decorated function still only accepts the original keyword arguments.

I guess the two methods are functionally equivalent, but decorator.decorate allows to do something with the original function at the time the decorator is created (vs at the time runtime, with decorator.decorator.

I also found #55 and #58 which seem to imply that signature-changing decorators are not supported at all and go against the entire philosophy of decorator.py.

But this seems to be a direct contradiction of the quoted docs:

The decorator function can be used as a signature-changing decorator, just like classmethod and staticmethod.

Can I use decorator.py to create signature-changing decorators, with new required or optional args or keyword args?

@micheles
Copy link
Owner

micheles commented Jul 16, 2019

Yes, you can and you need to use the FunctionMaker class to do that. But there no examples in the docs for that. The function decorator.decorator is signature-changing because it takes a function with arguments func, *args, **kw and returns a function with a single argument func, so the documentation is correct.

@mrmachine
Copy link
Author

I see now. It IS (can acts as) a signature changing decorator. It cannot CREATE signature changing decorators. Now the docs make sense.

I'd love for that distinction to be made a little clearer in the docs, and to see a new easy to use function equivalent to decorator that makes signature changing decorators.

While looking into this I discovered wrapt which does support and document the creation of signature changing decorators (called "adaptors"), so I'll probably switch to that, although I prefer the decorator syntax.

@micheles
Copy link
Owner

You may try this project https://github.com/smarie/python-makefun which is based on the decorator module.

@smarie
Copy link
Contributor

smarie commented Jul 22, 2019

Thanks @micheles for the redirection, yes I think that makefun would be appropriate for this use case. It was entirely inspired by decoratorand relies on the same trick that decorator uses, but it focuses solely on the function wrapping aspect so that it is de-coupled from the decorator creation problem.

Here is how you would create your decorator using makefun:

from inspect import signature, Parameter
from makefun import wraps, add_signature_parameters

def mydec(func):
    """
    A decorator to add 2 keyword arguments to the decorated function.

    :param func:
    :return:
    """
    # add parameters to the wrapped function signature
    func_sig = signature(func)
    new_kw_param = Parameter('new_kw', kind=Parameter.POSITIONAL_OR_KEYWORD, default=None)
    another_new_kw_param = Parameter('another_new_kw', kind=Parameter.POSITIONAL_OR_KEYWORD, default=None)
    new_sig = add_signature_parameters(func_sig, last=(new_kw_param, another_new_kw_param))

    # create the new function with modified signature
    @wraps(func, new_sig=new_sig)
    def new_func(*args, **kwargs):
        new_kw = kwargs.pop('new_kw')
        another_new_kw = kwargs.pop('another_new_kw')
        if new_kw:
            print(new_kw)

        return func(*args, **kwargs)

    # return the new function
    return new_func


# ------ Test
@mydec
def foo(a):
    return a + 1

assert foo(1) == 2
assert foo(1, new_kw='bar') == 2  # prints 'bar'
print(help(foo))                  # new signature

If you then wish to make the decorator creation step more easy, you can use decopatch. This is particularly relevant if you wish your decorator to have arguments, including optional ones. This is how you can do it :

from inspect import signature, Parameter
from decopatch import function_decorator, DECORATED
from makefun import wraps, add_signature_parameters

@function_decorator
def mydec(func=DECORATED, msg_to_print="hello"):
    """
    A decorator to add 2 keyword arguments to the decorated function.

    :param msg_to_print: the message to print
    :return:
    """
    # add parameters to the wrapped function signature
    func_sig = signature(func)
    new_kw_param = Parameter('new_kw', kind=Parameter.POSITIONAL_OR_KEYWORD, default=None)
    another_new_kw_param = Parameter('another_new_kw', kind=Parameter.POSITIONAL_OR_KEYWORD, default=None)
    new_sig = add_signature_parameters(func_sig, last=(new_kw_param, another_new_kw_param))

    # create the new function with modified signature
    @wraps(func, new_sig=new_sig)
    def new_func(*args, **kwargs):
        new_kw = kwargs.pop('new_kw')
        another_new_kw = kwargs.pop('another_new_kw')
        if new_kw:
            print("%s %s" % (msg_to_print, new_kw))

        return func(*args, **kwargs)

    # return the new function
    return new_func

# ------ Test
@mydec          # no parenthesis ! default behaviour
def foo(a):
    return a + 1

assert foo(1) == 2
assert foo(1, new_kw='bar') == 2  # prints 'hello bar'

@mydec(msg_to_print="oops")
def foo2(a):
    return a + 2

assert foo2(1) == 3
assert foo2(1, new_kw='bar') == 3  # prints 'oops bar'

Note: the helper function add_signature_parameters is still a bit ugly but there are open tickets about this. Feel free to comment or propose other ideas: smarie/python-makefun#41 smarie/python-makefun#42 smarie/python-makefun#43

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants