GrahamDumpleton / wrapt

A Python module for decorators, wrappers and monkey patching.
BSD 2-Clause "Simplified" License
2.04k stars 230 forks source link

Can we propagate the name from a coroutine function to the resulting coroutine? #216

Open Tinche opened 2 years ago

Tinche commented 2 years ago

Hello!

I've come up against this while working on an asyncio job library. Maybe it's best if I paste a code example.

from wrapt import decorator

@decorator
async def dec(wrapped, instance, args, kwargs):
    return await wrapped(*args, **kwargs)

@dec
async def coroutine_func():
    pass

coroutine_func.__name__  # is 'coroutine_func' as expected
coroutine_func().__name__  # is 'dec' instead of 'coroutine_func'

Can anything be done about this in wrapt, or is this too lost a case?

GrahamDumpleton commented 2 years ago

What do you get for:

type(coroutine_func())
GrahamDumpleton commented 2 years ago

Am inclined to think this is a shortcoming in Python implementation of coroutines in that it doesn't cope well with decorated functions, or certainly not ones where the decorated function is actual using a decorator which is a descriptor.

If you use older asyncio.coroutine decorator you get:

>>> @asyncio.coroutine
... @dec
... def my_func(): pass
...
>>> my_func().__name__
'my_func'
>>> type(my_func())
<class 'generator'>

Although if switch that around you get:

>>> @dec
... @asyncio.coroutine
... def my_func(): pass
...
>>> my_func().__name__
'dec'
>>> type(my_func())
<class 'coroutine'>

showing wrong name, with result being a different type as well, which is strange.

So right now not really sure. I can't really work out how the async keyword magic happens.

It might be interesting to do some test with decorator coroutines where decorator uses normal function closure and functools.wraps. If it works with that probably shows that a decorator implemented as a descriptor is the issue.

Tinche commented 2 years ago

Yep, looks like functools.wraps will do the trick.

from functools import wraps

def dec(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)

    return wrapper

@dec
async def coroutine_func():
    pass

print(coroutine_func.__name__)  # is 'coroutine_func' as expected
print(coroutine_func().__name__)  # is 'coroutine_func' as expected

prints outs:

coroutine_func
coroutine_func

(and a warning that's not relevant)

Should we open up a CPython issue? Could you do it since I think you have a much better grasp of what's going on?