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

Improve type hinting for ContextDecorator and AsyncContextDecorator #13403

Closed
ncoghlan opened this issue Jan 17, 2025 · 3 comments · Fixed by #13416
Closed

Improve type hinting for ContextDecorator and AsyncContextDecorator #13403

ncoghlan opened this issue Jan 17, 2025 · 3 comments · Fixed by #13416
Labels
stubs: false positive Type checkers report false errors

Comments

@ncoghlan
Copy link
Contributor

I recently ran into the problem described in https://stackoverflow.com/questions/62703400/python-how-to-type-hint-a-callable-with-wrapped, where MyPy complained when I attempted to access __wrapped__ on a function decorated with a ContextDecorator subclass.

The function signature is handled correctly (as per #4399), but the fact ContextDecorator.__call__ necessarily adds the __wrapped__ attribute to the returned wrapper is lost.

The functools stub does have some machinery for accurately typing functools.wraps itself, but there isn't anything that could be readily used to adjust the ContextDecorator.__call__ signature.

Ideally something like the following would be possible:

    def __call__(self, func: _F) -> _WrappedCallable[_F]: ...

However, I'm not sure how _WrappedCallable could be expressed as a generic type - the corresponding protocol in functools.pyi accepts the call parameters and return type as separate type variables.

@srittau srittau added the stubs: false positive Type checkers report false errors label Jan 19, 2025
@srittau
Copy link
Collaborator

srittau commented Jan 19, 2025

Without giving it too much tought, using a separate ParamSpec/TypeVar could work:

_P = ParamSpec("_P")
_R = TypeVar("_R")

class _WrappedCallable(Generic[_P, _R]):
    __wrapped__: Callable[_P, _R]
    def __call__(self, *args: P.args, **kwargs: P.kwargs) -> _R: ...

class ContextDecorator:
    def __call__(self, func: Callable[_P, _R]) -> _WrappedCallable[_P, _R]: ...

This is basically the approach used for functools._Wrapped.

@ncoghlan
Copy link
Contributor Author

#13416 implements the above suggestion.

@ncoghlan
Copy link
Contributor Author

Iterating on the PR showed that there's actually a deeper problem with the current nominal signature here than not being able to access __wrapped__ without type checkers complaining: the existing signature claims something that isn't true.

Specifically, it claims that the returned value will have the same type as the input value. This is not accurate: the returned value will be a callable with the same call signature as the input value, but it will otherwise be a different type (so no extra attributes or methods that were defined on the original callable will be available).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
stubs: false positive Type checkers report false errors
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants