Skip to content

Return value of @functools.wraps not recognized to be (of the same type as) its input #15868

@TTsangSC

Description

@TTsangSC

Summary

As implemented in CPython, @functools.wraps updates its argument (the wrapper) and returns the same object back. Since it is explicitly described as a convenience wrapper around functools.update_wrapper() in the docs, I would suppose this update-not-replace behavior to be part of the standard.

However, as it is currently implemented in typeshed, the return value of functools.update_wrapper() is typed only on the signatures of the wrapper and wrapped callables (_Wrapped[_PWrapped, _RWrapped, _PWrapper, _RWrapper]), without consideration of the type of wrapper. This results in permissible operations on the return value being marked illegal by type-checkers like mypy.

Example

(mypy playground: https://mypy-play.net/?mypy=latest&python=3.13&gist=26b45882b3db03d4b4c2ec74db78cd02)

Here is an example passed to mypy 2.1.0:

from collections.abc import Callable
from functools import wraps
from inspect import signature
from types import FunctionType


def func() -> None:
    pass


def wrap_vanilla[**PS, T](func: Callable[PS, T]) -> Callable[PS, T]:
    @wraps(func)
    def wrapper(*a: PS.args, **k: PS.kwargs) -> T:
        return func(*a, **k)

    wrapper.__signature__ = signature(func)
    return wrapper


def wrap_with_extra_nudge[**PS, T](func: Callable[PS, T]) -> Callable[PS, T]:
    @wraps(func)
    def wrapper(*a: PS.args, **k: PS.kwargs) -> T:
        return func(*a, **k)

    # Because `typeshed` represents the return value of `@wraps` with
    # `_Wrapper[PSWrapped, TWrapped, PSWrapper, TWrapper]`, it is not recognized
    # as a plain function without this assertion, despite the implementation of
    # `functools.update_wrapper()` guaranteeing that the `wrapper` object itself
    # is returned
    assert isinstance(wrapper, FunctionType)
    wrapper.__signature__ = signature(func)
    return wrapper

Expected behavior

Both wrap_vanilla() and wrap_with_extra_nudge() pass type-checking.

Actual behavior

wrap_vanilla() fails type-checking because wraps(func)(wrapper) is not recognized as being of the same type as, and indeed being identical to, wrapper (a FunctionType):

main.py:16: error: "_Wrapped[PS, T, PS, T]" has no attribute "__signature__"  [attr-defined]
Found 1 error in 1 file (checked 1 source file)

Possible related issues

#4626, #9846

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions