Skip to content

wrapt-decorated method does not strip self due to positional-or-keyword self and ParamSpec invariance #3746

Description

@injust

Summary

ty isn't happy when wrapt-decorated methods are used as an attrs converter. This is something that basedpyright is okay with.

The pass_through decorator comes from https://wrapt.readthedocs.io/en/latest/typing.html#function-decorators.

Environment: Python 3.14.5 with attrs==26.1.0 and wrapt==2.2.1


from typing import TYPE_CHECKING, Any, ParamSpec, TypeVar, reveal_type

import wrapt
from attrs import define, field

if TYPE_CHECKING:
    from collections.abc import Callable

    P = ParamSpec("P")
    R = TypeVar("R")


@wrapt.decorator
def pass_through(wrapped: Callable[P, R], instance: Any, args: tuple[Any, ...], kwargs: dict[str, Any]) -> R:
    return wrapped(*args, **kwargs)


class Util:
    def converter(self, s: str) -> int:
        return int(s)

    @pass_through
    def decorated_converter(self, s: str) -> int:
        return int(s)


util = Util()
reveal_type(util.converter)
reveal_type(util.decorated_converter)


@define
class Foo:
    val: int = field(converter=util.converter)


@define
class Bar:
    val: int = field(converter=util.decorated_converter)

info[revealed-type]: Revealed type
  --> test.py:28:13
   |
28 | reveal_type(util.converter)
   |             ^^^^^^^^^^^^^^ `bound method Util.converter(s: str) -> int`
   |

info[revealed-type]: Revealed type
  --> test.py:29:13
   |
29 | reveal_type(util.decorated_converter)
   |             ^^^^^^^^^^^^^^^^^^^^^^^^ `BoundFunctionWrapper[(self, s: str), int]`
   |

error[no-matching-overload]: No overload of function `field` matches arguments
  --> test.py:39:16
   |
39 |     val: int = field(converter=util.decorated_converter)
   |                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
info: First overload defined here
  --> .venv/lib/python3.14/site-packages/attrs/__init__.pyi:69:1
   |
69 | / @overload
70 | | def field(
71 | |     *,
72 | |     default: None = ...,
73 | |     validator: None = ...,
74 | |     repr: _ReprArgType = ...,
75 | |     hash: bool | None = ...,
76 | |     init: bool = ...,
77 | |     metadata: Mapping[Any, Any] | None = ...,
78 | |     converter: None = ...,
79 | |     factory: None = ...,
80 | |     kw_only: bool | None = ...,
81 | |     eq: bool | None = ...,
82 | |     order: bool | None = ...,
83 | |     on_setattr: _OnSetAttrArgType | None = ...,
84 | |     alias: str | None = ...,
85 | |     type: type | None = ...,
86 | | ) -> Any: ...
   | |_____________^ First overload defined here
   |
info: Possible overloads for function `field`:
info:   (*, default: None = ..., validator: None = ..., repr: bool | ((Any, /) -> str) = ..., hash: bool | None = ..., init: bool = ..., metadata: Mapping[Any, Any] | None = ..., converter: None = ..., factory: None = ..., kw_only: bool | None = ..., eq: bool | None = ..., order: bool | None = ..., on_setattr: ((Any, Attribute[Any], Any, /) -> Any) | list[(Any, Attribute[Any], Any, /) -> Any] | _NoOpType | None = ..., alias: str | None = ..., type: type | None = ...) -> Any
info:   [_T](*, default: None = ..., validator: ((Any, Attribute[_T], _T, /) -> Any) | Sequence[(Any, Attribute[_T], _T, /) -> Any] | None = ..., repr: bool | ((Any, /) -> str) = ..., hash: bool | None = ..., init: bool = ..., metadata: Mapping[Any, Any] | None = ..., converter: ((Any, /) -> Any) | Converter[Any, Any] | list[((Any, /) -> Any) | Converter[Any, Any]] | tuple[((Any, /) -> Any) | Converter[Any, Any], ...] | None = ..., factory: (() -> _T) | None = ..., kw_only: bool | None = ..., eq: bool | ((Any, /) -> Any) | None = ..., order: bool | ((Any, /) -> Any) | None = ..., on_setattr: ((Any, Attribute[Any], Any, /) -> Any) | list[(Any, Attribute[Any], Any, /) -> Any] | _NoOpType | None = ..., alias: str | None = ..., type: type | None = ...) -> _T
info:   [_T](*, default: _T, validator: ((Any, Attribute[_T], _T, /) -> Any) | Sequence[(Any, Attribute[_T], _T, /) -> Any] | None = ..., repr: bool | ((Any, /) -> str) = ..., hash: bool | None = ..., init: bool = ..., metadata: Mapping[Any, Any] | None = ..., converter: ((Any, /) -> Any) | Converter[Any, Any] | list[((Any, /) -> Any) | Converter[Any, Any]] | tuple[((Any, /) -> Any) | Converter[Any, Any], ...] | None = ..., factory: (() -> _T) | None = ..., kw_only: bool | None = ..., eq: bool | ((Any, /) -> Any) | None = ..., order: bool | ((Any, /) -> Any) | None = ..., on_setattr: ((Any, Attribute[Any], Any, /) -> Any) | list[(Any, Attribute[Any], Any, /) -> Any] | _NoOpType | None = ..., alias: str | None = ..., type: type | None = ...) -> _T
info:   [_T](*, default: _T | None = ..., validator: ((Any, Attribute[_T], _T, /) -> Any) | Sequence[(Any, Attribute[_T], _T, /) -> Any] | None = ..., repr: bool | ((Any, /) -> str) = ..., hash: bool | None = ..., init: bool = ..., metadata: Mapping[Any, Any] | None = ..., converter: ((Any, /) -> Any) | Converter[Any, Any] | list[((Any, /) -> Any) | Converter[Any, Any]] | tuple[((Any, /) -> Any) | Converter[Any, Any], ...] | None = ..., factory: (() -> _T) | None = ..., kw_only: bool | None = ..., eq: bool | ((Any, /) -> Any) | None = ..., order: bool | ((Any, /) -> Any) | None = ..., on_setattr: ((Any, Attribute[Any], Any, /) -> Any) | list[(Any, Attribute[Any], Any, /) -> Any] | _NoOpType | None = ..., alias: str | None = ..., type: type | None = ...) -> Any

Found 3 diagnostics

Version

ty v0.0.43 via uv check

Metadata

Metadata

Assignees

No one assigned

    Labels

    typing semanticstyping-module features, spec compliance, etc

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions