Skip to content

Mypy *sometimes* doesn't see metaclass-provided properties for protocols #6393

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

Closed
posita opened this issue Nov 25, 2021 · 3 comments · Fixed by #6394
Closed

Mypy *sometimes* doesn't see metaclass-provided properties for protocols #6393

posita opened this issue Nov 25, 2021 · 3 comments · Fixed by #6394

Comments

@posita
Copy link
Contributor

posita commented Nov 25, 2021

This is just weird. Somehow, SupportsInt and other standard library Supports protocols (and subclasses) are "special" when used in composing protocols. I do not know if this is related to (or the same root cause as) python/mypy#7945.

# test_case.py
from abc import abstractmethod
from typing import Any, Protocol, SupportsInt, _ProtocolMeta, runtime_checkable

class MyProtocolMeta(_ProtocolMeta):
    def foo(cls) -> None:
        print("foo")

@runtime_checkable
class MySupportsInt(
    SupportsInt,
    Protocol,
    metaclass=MyProtocolMeta,
):
    pass

@runtime_checkable
class SupportsFoo(
    Protocol,
    metaclass=MyProtocolMeta,
):
    @abstractmethod
    def foo(self) -> Any:
        pass

@runtime_checkable
class SupportsBar(
    Protocol,
    metaclass=MyProtocolMeta,
):
    @abstractmethod
    def bar(self) -> Any:
        pass

@runtime_checkable
class SupportsComposedThing(
    MySupportsInt,  # <-- if you comment this out ...
    # SupportsFoo,  # <-- ... or uncomment this ...
    SupportsBar,
    Protocol,
    # metaclass=MyProtocolMeta,  # <-- ... or uncomment this ...
):
    pass

# ... then this error goes away
SupportsComposedThing.foo  # "Type[SupportsComposedThing]" has no attribute "foo"

assert SupportsComposedThing.__class__ is MyProtocolMeta  # <-- succeeds
MySupportsInt.foo  # <-- this is fine
SupportsFoo.foo  # <-- so is this
SupportsBar.foo  # <-- and this
# reveal_type(SupportsComposedThing)  # Revealed type is "def () -> test_case.SupportsComposedThing"
% mypy --version
mypy 0.910
% python --version
Python 3.9.7
% mypy --config=/dev/null test_case.py
/dev/null: No [mypy] section in config file
test_case.py:3: error: Module "typing" has no attribute "_ProtocolMeta"; maybe "Protocol"?
test_case.py:46: error: "Type[SupportsComposedThing]" has no attribute "foo"
Found 2 errors in 1 file (checked 1 source file)
@posita
Copy link
Contributor Author

posita commented Nov 26, 2021

Wait, is this because the standard library's protocols themselves are lies? Why do (redundant?) definitions exist at all inside typing.pyi? If so, it may be a typeshed issue. But still, doesn't having to externally type your protocols sort of defeat the purpose of protocol types? What dark magic is this? Do we even need Supports definitions in typing.pyi anymore? (Apologies in advance for my ignorance. Or redundancy.)


Taking note of some history here to partly explain how we got here….

@hauntsaninja
Copy link
Collaborator

mypy and other static type checkers don't look at CPython source code at all.

typeshed doesn't even have _ProtocolMeta (speaking generally, typeshed doesn't type typing.py internals too closely, since they tend to change a lot from version to version, type checkers special case them anyway, and those details tend to confuse users who aren't familiar with the inner workings of type checkers).

This means that _ProtocolMeta gets resolved to Any, so it's unsurprising to me that you get weirdness that only shows up later.

If you use the following I think you get the results you expect:

if TYPE_CHECKING:
    _ProtocolMeta = ABCMeta
else:
    from typing import _ProtocolMeta

@posita
Copy link
Contributor Author

posita commented Nov 27, 2021

… [I]t's unsurprising to me that you get weirdness that only shows up later.

That's because you're a 17th level wizard specializing in typedivination. For the rest of us mere mortals, this is downright counterintuitive to say nothing of ergonomic. Why the inconsistent behavior among user-defined protocols and standard library ones? 😕 Or is that your point about "weirdness"?

Thanks for the work-around, though! ❤️

@hauntsaninja hauntsaninja transferred this issue from python/mypy Nov 27, 2021
hauntsaninja pushed a commit to hauntsaninja/typeshed that referenced this issue Nov 27, 2021
It looks like a couple people are using this in the wild.

Resolves python#6393
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants