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

functools.wraps inferred signature breaks with non-standard wrapper arguments #3939

Closed
Dzeri96 opened this issue Sep 13, 2022 · 2 comments
Closed
Labels
as designed Not a bug, working as intended

Comments

@Dzeri96
Copy link

Dzeri96 commented Sep 13, 2022

Describe the bug
It seems like the inferred signature of functions wrapped with @functools.wraps depends on the signature of the wrapper itself.

To be more specific, there are cases where the wrapper function should take a self argument, like so:

def wrapper(self, *args, **kwargs):
    pass

In this case, the signature of the wrapped function is inferred as (*args: Unknown, **kwargs: Unknown) -> Unknown.
If however, the wrapper takes only *args and **kwargs, the signature is inferred properly.

To Reproduce
Refer to the attached code.

Expected behavior
It is perhaps unclear what exactly should happen here, but if we run the same code in the python shell, and inspect its signature with the inspect.signature module method, the original signature is returned.
I therefore suggest this behavior is maintained in the typechecker.

Screenshots or Code
Version with self in wrapper:
image

import functools
from typing import Callable

def test_decorator(
        func: Callable
    ):
        @functools.wraps(func)
        def wrapper(self, *args, **kwargs):
            # Do something with self here
            ret_val = func(self, *args, **kwargs)
            # Do some cleanup
            return ret_val

        return wrapper

class TestClass:

    @test_decorator
    def test_func(self, arg1: int):
        pass

TestClass().test_func()

Version without self in wrapper:
image

import functools
from typing import Callable

def test_decorator(
        func: Callable
    ):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            # Do something with self here
            self = None
            ret_val = func(self, *args, **kwargs)
            # Do some cleanup
            return ret_val

        return wrapper

class TestClass:

    @test_decorator
    def test_func(self, arg1: int):
        pass

TestClass().test_func()

VS Code extension or command-line
Pylance: v2022.1.3

Additional context
It is perhaps also worth it to think about other custom arguments.
For example, what happens if the wrapper's signature clearly doesn't match the wrapped function's?

@erictraut
Copy link
Collaborator

There is no type inference involved with functools.wraps. Pyright is simply applying the standard typing rules for call expression evaluation. The problem here is that the type definition for wraps in the typeshed functools.pyi stub is wrong. It should use a ParamSpec, which will retain additional information about the signature, including the fact that it's an instance method that contains a self parameter. I've submitted a PR to typeshed to fix it, but the PR has yet to be accepted because mypy does not work well with the updated definition. Until that PR is accepted, I don't think there's much we can do about this in pyright.

@erictraut erictraut added the as designed Not a bug, working as intended label Sep 13, 2022
@Dzeri96
Copy link
Author

Dzeri96 commented Sep 13, 2022

I see, thanks for the clarification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
as designed Not a bug, working as intended
Projects
None yet
Development

No branches or pull requests

2 participants