Skip to content

hinting method decorators with protocols fails to remove the self attribute when binding #16200

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

Open
altendky opened this issue Sep 29, 2023 · 3 comments · May be fixed by #15993
Open

hinting method decorators with protocols fails to remove the self attribute when binding #16200

altendky opened this issue Sep 29, 2023 · 3 comments · May be fixed by #15993
Labels
bug mypy got something wrong

Comments

@altendky
Copy link

Bug Report

When hinting a decorator used on methods using Callable, the unbound method and bound method types differ by the first/self parameter as I expect. When I replace the Callable with a Protocol providing .__call__() instead, the unbound and bound cases are the same.

To Reproduce

https://mypy-play.net/?mypy=latest&python=3.11&gist=1046c1e56c21fd17cda1ecab5e9e131b

import typing


def d(f: typing.Callable[[C, int], str]) -> typing.Callable[[C, int], str]:
    def inner(self, x: int) -> str:
        return f(self, x)
    
    return inner

class C:
    @d
    def m(self, x: int) -> str:
        return ""

reveal_type(C.m)
reveal_type(C().m)


class P(typing.Protocol):
    def __call__(protocol_self, self: CP, x: int) -> str:
        ...

def dp(f: P) -> P:
    def inner(self, x: int) -> str:
        return f(self, x)
    
    return inner

class CP:
    @dp
    def m(self, x: int) -> str:
        return ""

reveal_type(CP.m)
reveal_type(CP().m)

Expected Behavior

I don't know the details around 'modifying' protocols such as would be required here but I would expect that unbound and bound would not be the same.

Actual Behavior

Unbound and bound methods decorated with a decorator hinted with protocols have the same type.

main.py:15: note: Revealed type is "def (__main__.C, builtins.int) -> builtins.str"
main.py:16: note: Revealed type is "def (builtins.int) -> builtins.str"
main.py:34: note: Revealed type is "__main__.P"
main.py:35: note: Revealed type is "__main__.P"
Success: no issues found in 1 source file

Your Environment

  • Mypy version used: 1.5.1, master
  • Mypy command-line flags: mypy-play defaults
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: 3.11
@altendky altendky added the bug mypy got something wrong label Sep 29, 2023
@hauntsaninja
Copy link
Collaborator

Thanks, I think #15993 makes this behave as you expect
Might need to see if a creative solution can be worked out for minimising some of the downstream impact

@RonnyPfannschmidt
Copy link

after some digging i realized,that self attribute removal is indeed incorrect
callables and methof descriptors are different things

unfortunately the "correct" version spells out like

class HasApplication(Protocol):
    @property
    def application(self) -> "Application": ...

HAS_APPLICATION = TypeVar("HAS_APPLICATION", bound=HasApplication)


class ApplicationEntityMethod(Generic[HAS_APPLICATION, P, T], Protocol):
    def __call__(protocol_self, /, self: HasApplication, *k: P.kwargs , **kw: P.kwargs ) -> T:
        ...

    @overload
    def __get__(self, instance: HAS_APPLICATION, owner: type[HAS_APPLICATION]) -> Callable[P, T]: ...

    @overload
    def __get__(self, instance: None, owner: type[HAS_APPLICATION]) -> Self: ...

    def __get__(self, instance: HAS_APPLICATION|None, owner: type[HasApplication]) -> Callable[P, T] | Self: ..
    ```
    
    which is most boilerplate ridden  and i still havent figrued how to make it match a function

@RonnyPfannschmidt
Copy link

the working version spells like

class HasApplication(Protocol):
    @property
    def application(self) -> Application: ...


R = TypeVar("R")
P = ParamSpec("P")
TM = TypeVar("TM", covariant=True)
HAS_APPLICATION = TypeVar("HAS_APPLICATION", bound=HasApplication, contravariant=True)


class ApplicationEntityMethod(Protocol, Generic[HAS_APPLICATION, P, TM]):
    def __call__(protocol_self, self: HAS_APPLICATION, *k: P.args, **kw: P.kwargs) -> TM: ...

    @overload
    def __get__(
        self, instance: HAS_APPLICATION, owner: type[HAS_APPLICATION]
    ) -> Callable[P, TM]: ...

    @overload
    def __get__(self, instance: None, owner: type[HAS_APPLICATION]) -> Self: ...

    def __get__(
        self, instance: HAS_APPLICATION | None, owner: type[HasApplication]
    ) -> Callable[P, TM] | Self: ...

i wish there was a builtin version of that

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants