Shoobx / mypy-zope

Plugin for mypy to support zope.interface
MIT License
39 stars 11 forks source link

Is there a way to declare Interface intersections? #40

Closed euresti closed 3 years ago

euresti commented 3 years ago

I'm trying to use the new types coming out of Twisted in particular the IReactorTCP and IReactorTime but having a bit of an issue with how they are declared. Here's a distilled example:

from zope.interface import implementer
from zope.interface import Interface
from zope.interface import classImplements

class IFoo(Interface):
    x: int

class IBar(Interface):
    y: int

def foo_it(obj: IFoo) -> int:
    return obj.x

def bar_it(obj: IBar) -> int:
    return obj.y

# An attempt at combining IFoo and IBar into one type.
class IFooBar(IFoo, IBar):
    ...

def both_it(obj: IFooBar) -> int:
    assert IFooBar.providedBy(obj)
    return foo_it(obj) + bar_it(obj)

@implementer(IFoo, IBar)
class FooBar:
    def __init__(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

# This would make IFooBar.providedBy(obj) but doesn't help mypy
classImplements(FooBar, IFooBar)

obj = FooBar(6, 7)
foo_it(obj)
bar_it(obj)
both_it(obj)   # ERROR: Argument 1 to "both_it" has incompatible type "FooBar"; expected "IFooBar"

So I thought, ah this is what Protocols in mypy were made for:

from typing import Protocol
class IFooBarProtoco(IFoo, IBar, Protocol):
    ...

Which sadly gives error: All bases of a protocol must be protocols

Is there another way to possibly declare the intersection? Or maybe the plugin can do something special with the Protocol declaration? (But is that even possible)

There's a mypy issue about it too: https://github.com/python/typing/issues/21 but it doesn't provide any solutions.

kedder commented 3 years ago

The reason your example doesn't work is because classImplements is not understood by mypy-zope. I think it will work if you would declare FooBar as @implementer(IFooBar) directly.

I think it should be possible to implement support for classImplements, but TBH I don't see a good reason why one wouldn't use @implementer instead. Is there a use case where you would prefer classImplements over @implementer?

I don't think you can marry typing.Protocol and zope interfaces together - to my understanding protocols are kind of alternative way to have interface-like declarations.

euresti commented 3 years ago

In the real world (e.g. twisted) I can't edit IFoo, IBar and FooBar. They are part of a distributed library.

clokep commented 3 years ago

I think I ran into this issue in Synapse and it worked by doing:

class IFooBar(IFoo, IBar, Interface):
    ...

(Note the extra inheritance from Interface). I'm not 100% sure this was the same error message though. 😢

euresti commented 3 years ago

Interesting, Not sure what's going on with Synapse or your code, but the extra Interface didn't help with the sample code nor with the real code.

kedder commented 3 years ago

I don't think having Interface in base classes helps in this case. I suspect the Interface helps when neither IFoo or IBar is actually recognized as zope interface by the plugin. This can happen when libraries that provide those interfaces have no mypy stubs, so there is no way for plugin to know these are actually interfaces.

The correct way to address that is to provide stubs for those libs in your project (can be generated with stubgen from mypy).

kedder commented 3 years ago

I'm planning to implement support for classImplements, but haven't had a chance to actually do that. I expect doing the same thing as we do to support @implementer - we get both interface and implementation TypeInfos and add interface to the MRO of implementation.

kedder commented 3 years ago

The next mypy-zope release will have support for classImplements declarations (see #47).