microsoft / pylance-release

Documentation and issues for Pylance
Creative Commons Attribution 4.0 International
1.72k stars 769 forks source link

Display functools.wraps data in tooltips #467

Closed janosh closed 4 years ago

janosh commented 4 years ago

@functools.wraps(func) conveys information like function name and doc string from a bare to the decorated function. In this example, it seems like Pylance doesn't pick up on this information:

from functools import wraps
from typing import Callable

def interruptable(orig_func: Callable = None, handler: Callable = None):
    """Allows to gracefully abort calls to decorated functions with ctrl + c."""

    def wrapper(func):
        @wraps(func)
        def wrapped_function(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except KeyboardInterrupt:
                handler() if handler else print(
                    f"\nDetected KeyboardInterrupt: Aborting call to {func.__name__}"
                )

        return wrapped_function

    if orig_func:
        return wrapper(orig_func)

    return wrapper

@interruptable
def test_fn():
    """a test function"""
    print("ran test")

# with @wraps(func)
test_fn() # > ran test
print(test_fn.__name__) # > test_fn
print(test_fn.__doc__) # > a test function

# without @wraps(func)
test_fn() # > ran test
print(test_fn.__name__) # > wrapped_function
print(test_fn.__doc__) # > None
Tooltip with decorator Tooltip without decorator
decorated-1 decorated-2 bare

Anything Pylance could do to display the information conveyed by functools.wraps?

erictraut commented 4 years ago

The problem here is the way that wraps is defined in the typeshed stubs. It returns the type _AnyCallable which completely obscures the type of the function that it's wrapping.

@jakebailey, do you happen to know why functools.wraps is defined in this way in typeshed? Most decorators are annotated with type variables so they retain information about the function they are decoratoring. Perhaps we could convince the typeshed maintainers to change it.

jakebailey commented 4 years ago

Typeshed doesn't like adding types which cause false negatives, opting for Any, or in this case the equivalent of a very generalized Callable.

My guess had been that they were waiting for ParamSpec.

judej commented 4 years ago

Duplicate of #442

jakebailey commented 4 years ago

Also #125.

janosh commented 4 years ago

Thanks for the linked issues. I'm still unclear if this is a problem that will be fixed on Pylance's side or if typeshed would have to do something about this (if they choose to).

erictraut commented 4 years ago

There's really nothing we can do in the Pylance/Pyright side. The type checker is correctly applying the type information provided in the stub file.

janosh commented 4 years ago

@erictraut Ah, I was confused by the enhancement label.

In #125, you mentioned:

Since @wraps appears to be the source of many of these problems, maybe we should submit a change to typeshed to improve its signature so it uses a TypeVar. I don't know enough about the semantics of @wraps to say whether that's feasible. If it's mutating the original function (adding or removing parameters), then it's possible to describe that mutation using a TypeVar.

Happy to open a PR with typeshed to discuss this. Looks that would be the first step towards a solution.

jakebailey commented 4 years ago

The only special casing that would be "appropriate" would be when we're doing hovers/completions and need a docstring, to recognize a function decorated with wraps and go find its docstring, but that's a side issue compared to the typing problem (covered a bit in #442).

jackblk commented 4 years ago

Thanks @jakebailey, I came from this comment: https://github.com/microsoft/pylance-release/issues/49#issuecomment-706340352

As from above comments, I understand that Pylance cannot do anything. I'm using decoration from another module (allure) so I cannot redefine TypeVar or it.

Can I do anything to make it display temporarily? I work a lot in allure/pytest module, which they have a lot of decorations so it's hard to work without docstring :(.

erictraut commented 4 years ago

The correct fix is to properly annotate the decorator. You mentioned that you're using a decorator from another module. You could temporarily modify that module locally. It would be great if you'd then contribute the change back to the maintainer of the library so others benefit from it as well.