Plugin for mypy to support zope.interface

Overview

Plugin for mypy to support zope.interface

Build Status Coverage Status Checked with mypy

The goal is to be able to make zope interfaces to be treated as types in mypy sense.

Usage

Install both mypy and mypy-zope:

pip install mypy-zope

Edit mypy.ini file in your project to enable the plugin:

[mypy]
namespace_packages=True
plugins=mypy_zope:plugin

You're done! You can now check your project with mypy:

mypy your-project-dir

What is supported?

You can browse sample files to get some sense on what features are supported and how they are handled.

Interface declarations

You can define the interface and provide implementation:

class IAnimal(zope.interface.Interface):
    def say() -> None:
        pass

@zope.interface.implementer(IAnimal)
class Cow(object):
    def say(self) -> None:
        print("Moooo")

animal: IAnimal = Cow()
animal.say()

The interface IAnimal will be treated as superclass of the implementation Cow: you will be able to pass an implementation to functions accepting an interface and all the usual polymorphism tricks.

It is also possible to declare the implementation using classImplements function with the same effect as @imlementer decorator. This is useful if you do not control the code that defines the implementation class.

classImplements(Cow, IAnimal)

animal: IAnimal = Cow()

Schema field type inference

A limited support for defining attributes as zope.schema.Fields is supported too:

class IAnimal(zope.interface.Interface):
    number_of_legs = zope.schema.Int(title="Number of legs")

@zope.interface.implementer(IAnimal)
class Cow(object):
    number_of_legs = 4

In context of an interface, some known zope.schema field types are automatically translated to python types, so the number_of_legs attributes is getting the type int in the example above. That means mypy will report an error if you try to assign string to that attribute on an instance of IAnimal type. Custom fields or fields not recognized by plugin are given type Any.

Field properties

Support for zope.schema.FieldProperty is limited, because type information is not transferred from an interface to implementation attribute, but mypy doesn't report errors on sources like this:

class IAnimal(zope.interface.Interface):
    number_of_legs = zope.schema.Int(title="Number of legs")

@zope.interface.implementer(IAnimal)
class Cow(object):
    number_of_legs = zope.schema.FieldProperty(IAnimal['number_of_legs'])

The type of Cow.number_of_legs will become Any in this case, even though IAnimal.number_of_legs would be inferred as int.

Adaptation pattern

Zope interfaces can be "called" to lookup an adapter, like this:

class IEUPowerSocket(zope.interface.Interface):
    def fit():
        pass

adapter = IEUPowerSocket(us_plug)
adapter.fit()

Type of the adapter variable will be set to IEUPowerSocket.

Conditional type inference

When using zope.interface's implementedBy() and providedBy() methods in an if statement, mypy will know which type it is inside those statements.

if IAnimal.providedBy(ob):
    ob.number_of_legs += 2

Declaration of overloaded methods in interfaces

Similarly to regular overloaded functions, @overload declarations are supported in interfaces as well:

class IAnimal(zope.interface.Interface):
    @overload
    def say() -> str:
        ...

    @overload
    def say(count: int) -> List[str]:
        ...

    def say(count: int = None) -> Union[str, List[str]]:
        pass


@zope.interface.implementer(IAnimal)
class Cow(object):
    @overload
    def say(self) -> str:
        ...

    @overload
    def say(self, count: int) -> List[str]:
        ...

    def say(self, count: int = None) -> Union[str, List[str]]:
        if count is None:
            return "Mooo"
        return ["Mooo"] * count

Type stubs for zope.interface and zope.schema

mypy-zope ships with type stubs (*.pyi files) for zope.interface and zope.schema packages. They are enabled automatically as soon as plugin is enabled.

What is not supported?

These zope.interface features are not supported:

  • Declaring modules as interface implementers.
  • Type inference for zope.schema.List and zope.schema.Dict fields.
  • Stub files are largely incomplete
  • Interface compatibility checker will not type-check non-method attributes

Under development!

Currently the project is in a very early stage of development and might not be practically usable yet. Suggestions and pull requests are welcomed!

Comments
  • Do not add the same interface multiple times to the MRO.

    Do not add the same interface multiple times to the MRO.

    This fixes #34 for me. When taking a look at the zope logs I noticed that IProtocol was getting added multiple times to the MRO, which seems wrong.

    Implementation-wise, this attempts to only add a single instance of an interface into the MRO. In order for the comparison check to work (to see if the interface is already in the MRO) this adds a cache to only generate a single TypeInfo for each interface.

    I don't know the internals of mypy so maybe this is incorrect and we should be checking a different way?

    I suspect that this will work for most cases, but some odd diamond-inheritance patterns might break as the MRO might end up in the wrong order. I.e. this ensure that only a single instance of IFoo exists in the MRO, but I'm not confident it ends up in the proper place. 😄

    I attempted to make a reduced test-case of this again but was unable to. 😢 I think it has something to do with:

    • Having an interface (or two).
    • Having a base class that implements part of that interface, but isn't marked as an implementer.
    • Having a class that is an implementer.
    • Having a sub-class of the implementer.

    Additionally the result needs to be cached so I think this needs to be imported from another package / module and isn't part of what is being "inspected", but I'm not 100% sure about this.

    opened by clokep 21
  • Metaclass related internal error

    Metaclass related internal error

    Hi,

    We just ran into this stacktrace:

    https://mypy.rtfd.io/en/latest/common_issues.html#using-a-development-mypy-build
    Please report a bug at https://github.com/python/mypy/issues
    version: 0.800
    Traceback (most recent call last):
      File "mypy/semanal.py", line 4835, in accept
      File "mypy/nodes.py", line 950, in accept
      File "mypy/semanal.py", line 1048, in visit_class_def
      File "mypy/semanal.py", line 1125, in analyze_class
      File "mypy/semanal.py", line 1134, in analyze_class_body_common
      File "mypy/semanal.py", line 1187, in apply_class_plugin_hooks
      File "/home/travis/virtualenv/python3.8.1/lib/python3.8/site-packages/mypy_zope/plugin.py", line 266, in analyze_metaclass
        if info and any(node.fullname == expected for node in info.mro):
    AttributeError: 'Var' object has no attribute 'mro'
    

    I haven't had time find exactly which piece of code is causing this, but will do so as soon as I can.

    EDIT: this was tested on mypy 0.800. The line which hit this was something like this:

    @attr.s(slots=True, frozen=True)
    class Leg(metaclass=CustomMetaclass):
    	a: str = attr.ib()
    	b: str = attr.ib()
    	c: str = attr.ib()
    
    opened by arnimarj 11
  • Do not add the same interface multiple times to the MRO, round 2

    Do not add the same interface multiple times to the MRO, round 2

    For your consideration: a patch which I believe fixes #76. This tackles the same kind of problem that #41 addresses. Seems to be a regression introduced in #75 as part of supporting mypy 0.970.

    This turned out to be a bit of a rabbit hole. I've tried to thoroughly reproduce my working here because I really am not confident in my understanding of mypy and mypy-zope. Apologies for the verbosity! (I was very keen to nail this one down.)

    Reproduction

    I reproduced the problem as reported in #76:

    # Setup: in a blank venv
    pip install -e path/to/mypy/zope mypy==0.981 twisted[tls]==22.4.0
    
    cat >good.py <<EOF
    from twisted.web.client import HTTPClientFactory
    from twisted.web.client import HTTPPageGetter
    
    class Y(HTTPPageGetter):
        pass
    EOF
    
    cat >bad.py <<EOF
    from twisted.web.client import HTTPClientFactory
    from twisted.web.client import HTTPPageGetter
    
    class Y(HTTPPageGetter, object):
        pass
    EOF
    
    # Test:
    rm -r .mypy_cache/3.10/twisted/
    
    mypy good.py
    # Success: no issues found in 1 source file
    
    mypy bad.py
    # bad.py:4: error: Cannot determine consistent method resolution order (MRO) for "Y"
    # Found 1 error in 1 file (checked 1 source file)
    

    Investigation

    To attempt to understand what was going on, I added a bunch of debug print statements to mypy and mypy-zope until I reached enlightenment. I don't fully grok mypy's machinery, but from what I can tell:

    • During semantic analysis, when mypy encounters a class with multiple base classes, it computes the child class's MRO. 1, 2
    • In order to do so, it calls a function named linearize_hierarchy.

    When I run mypy bad.py I see that linear_hierarchy is called for HTTPPageGetter with the following TypeInfo:

    TypeInfo(
      Name(twisted.web.client.HTTPPageGetter)
      Bases(twisted.web.http.HTTPClient)
      Mro(twisted.web.client.HTTPPageGetter, twisted.web.http.HTTPClient, twisted.protocols.basic.LineReceiver, twisted.internet.protocol.Protocol, twisted.internet.protocol.BaseProtocol, twisted.protocols.basic._PauseableMixin, builtins.object, twisted.internet.interfaces.IProtocol, twisted.internet.interfaces.ILoggingContext, twisted.internet.interfaces.IProtocol, twisted.internet.interfaces.ILoggingContext)
      Names(
        _completelyDone (builtins.bool)
        _specialHeaders (builtins.set[builtins.bytes])
        connectionLost
        connectionMade
        failed (builtins.int)
        followRedirect (builtins.bool)
        handleEndHeaders
        handleHeader
        handleResponse
        handleStatus
        handleStatusDefault
        handleStatus_200
        handleStatus_201 (def (self: Any) -> Any)
        handleStatus_202 (def (self: Any) -> Any)
        handleStatus_301
        handleStatus_302
        handleStatus_303
        headers (Any)
        message (Any)
        quietLoss (builtins.int)
        status (Any)
        timeout
        version (Any)))
    

    In particular the MRO contains two copies of IProtocol and ILoggingContext, which doesn't seem right (c.f. #41). As I understand it, mypy bad.py is loading the TypeInfo for HttpPageGetter that we persisted at the end of mypy good.py. So what was going on when we ran mypy the first time?

    I will try to summarise a shedload of debug information. At some point mypy computes the MRO for twisted.internet.protocol.Protocol. The argument to linearize_hierarchy is

    TypeInfo(
        Name(twisted.internet.protocol.Protocol)
        Bases(twisted.internet.protocol.BaseProtocol)
        Mro()
        Names())
    

    Later, mypy computes the MRO for a subclass twisted.protocols.policies.ProtocolWrapper. Linearize hierarchy receives

    TypeInfo(
        Name(twisted.protocols.policies.ProtocolWrapper)
        Bases(twisted.internet.protocol.Protocol)
        Mro()
        Names())
    

    and recursively calls itself with the twisted.internet.protocol.Protocol TypeInfo. This time the MRO and Names have already been computed!

    
    TypeInfo(
        Name(twisted.internet.protocol.Protocol)
        Bases(twisted.internet.protocol.BaseProtocol)
        Mro(twisted.internet.protocol.Protocol, twisted.internet.protocol.BaseProtocol, builtins.object, twisted.internet.interfaces.IProtocol, twisted.internet.interfaces.ILoggingContext)
        Names(
          connectionLost
          dataReceived
          factory (Union[twisted.internet.protocol.Factory, None])
          logPrefix))
    

    The MRO looks sensible there.

    By the time mypy comes to compute the MRO for twisted.spread.banana.Banana (yeah, no idea there) we again lookup the MRO for twisted.internet.protocol.Protocol. But the MRO now has duplicate interfaces.

    TypeInfo(
        Name(twisted.spread.banana.Banana)
        Bases(twisted.internet.protocol.Protocol, twisted.persisted.styles.Ephemeral)
        Mro()
        Names())
    TypeInfo(
       Name(twisted.internet.protocol.Protocol)
        Bases(twisted.internet.protocol.BaseProtocol)
        Mro(twisted.internet.protocol.Protocol, twisted.internet.protocol.BaseProtocol, builtins.object, twisted.internet.interfaces.IProtocol, twisted.internet.interfaces.ILoggingContext, twisted.internet.interfaces.IProtocol, twisted.internet.interfaces.ILoggingContext)
    

    Explanation

    The following dirty patch adds useful logging.

    Dirty patch
    diff --git a/src/mypy_zope/plugin.py b/src/mypy_zope/plugin.py
    index 312c0b5..17fa127 100644
    --- a/src/mypy_zope/plugin.py
    +++ b/src/mypy_zope/plugin.py
    @@ -364,7 +364,7 @@ def analyze_subinterface(classdef_ctx: ClassDefContext) -> None:
         def get_customize_class_mro_hook(
             self, fullname: str
         ) -> Optional[Callable[[ClassDefContext], None]]:
    -        # print(f"get_customize_class_mro_hook: {fullname}")
    +        print(f"get_customize_class_mro_hook: {fullname}")
     
             def analyze_interface_base(classdef_ctx: ClassDefContext) -> None:
                 # Create fake constructor to mimic adaptation signature
    @@ -406,6 +406,9 @@ def analyze(classdef_ctx: ClassDefContext) -> None:
                 if directiface or subinterface:
                     self._analyze_zope_interface(classdef_ctx.api, classdef_ctx.cls)
     
    +            self.log(f"get_customize_class_mro_hook ending for {fullname}")
    +            self.log(info.mro)
    +
             if fullname == "zope.interface.interface.Interface":
                 return analyze_interface_base
     
    @@ -695,10 +698,19 @@ def _apply_interface(self, impl: TypeInfo, iface: TypeInfo) -> None:
             # there is a decorator for the class that will create a "type promotion",
             # but ensure this only gets applied a single time per interface.
             promote = Instance(iface, [])
    +        if impl.fullname == "twisted.internet.protocol.Protocol":
    +            for ti in impl.mro:
    +                self.log(f"DMR HMM: {ti._promote}, {promote}, {ti.fullname}")
    +                self.log(f"DMR HMM: {type(ti._promote)}, {type(promote)}, {ti.fullname}")
             if not any(ti._promote == promote for ti in impl.mro):
                 faketi = TypeInfo(SymbolTable(), iface.defn, iface.module_name)
                 faketi._promote = [promote]
    +            if impl.fullname=="twisted.internet.protocol.Protocol":
    +                self.log(f"============== APPENDING {iface.defn.fullname} =====")
                 impl.mro.append(faketi)
    +        self.log(f"mro of {impl.fullname}@{hex(id(impl))} ({hex(id(impl.mro))}) is now:")
    +        self.log(impl.mro)
    +
     
     
     def plugin(version: str) -> PyType[Plugin]:
    

    We can then see where mypy-zope is adding the duplicate interfaces.

      ZOPE: get_customize_class_mro_hook ending for twisted.internet.protocol.BaseProtocol
      ZOPE: [<TypeInfo twisted.internet.protocol.BaseProtocol>, <TypeInfo builtins.object>]
      ZOPE: get_customize_class_mro_hook ending for twisted.internet.protocol.Protocol
      ZOPE: [<TypeInfo twisted.internet.protocol.Protocol>, <TypeInfo twisted.internet.protocol.BaseProtocol>, <TypeInfo builtins.object>, <TypeInfo twisted.internet.interfaces.IProtocol>, <TypeInfo twisted.internet.interfaces.ILoggingContext>]
      ZOPE: Found implementation of twisted.internet.interfaces.IProtocol: twisted.internet.protocol.Protocol
    * ZOPE: DMR HMM: [], twisted.internet.interfaces.IProtocol, twisted.internet.protocol.Protocol
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, twisted.internet.protocol.Protocol
    * ZOPE: DMR HMM: [], twisted.internet.interfaces.IProtocol, twisted.internet.protocol.BaseProtocol
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, twisted.internet.protocol.BaseProtocol
    * ZOPE: DMR HMM: [], twisted.internet.interfaces.IProtocol, builtins.object
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, builtins.object
    * ZOPE: DMR HMM: [twisted.internet.interfaces.IProtocol], twisted.internet.interfaces.IProtocol, twisted.internet.interfaces.IProtocol
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, twisted.internet.interfaces.IProtocol
    * ZOPE: DMR HMM: [twisted.internet.interfaces.ILoggingContext], twisted.internet.interfaces.IProtocol, twisted.internet.interfaces.ILoggingContext
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, twisted.internet.interfaces.ILoggingContext
      ZOPE: ============== APPENDING twisted.internet.interfaces.IProtocol =====
      ZOPE: mro of twisted.internet.protocol.Protocol@0x7f7a62f0a6c0 (0x7f7a66731640) is now:
      ZOPE: [<TypeInfo twisted.internet.protocol.Protocol>, <TypeInfo twisted.internet.protocol.BaseProtocol>, <TypeInfo builtins.object>, <TypeInfo twisted.internet.interfaces.IProtocol>, <TypeInfo twisted.internet.interfaces.ILoggingContext>, <TypeInfo twisted.internet.interfaces.IProt  ocol>]
      ZOPE: Found implementation of twisted.internet.interfaces.ILoggingContext: twisted.internet.protocol.Protocol
    * ZOPE: DMR HMM: [], twisted.internet.interfaces.ILoggingContext, twisted.internet.protocol.Protocol
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, twisted.internet.protocol.Protocol
    * ZOPE: DMR HMM: [], twisted.internet.interfaces.ILoggingContext, twisted.internet.protocol.BaseProtocol
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, twisted.internet.protocol.BaseProtocol
    * ZOPE: DMR HMM: [], twisted.internet.interfaces.ILoggingContext, builtins.object
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, builtins.object
    * ZOPE: DMR HMM: [twisted.internet.interfaces.IProtocol], twisted.internet.interfaces.ILoggingContext, twisted.internet.interfaces.IProtocol
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, twisted.internet.interfaces.IProtocol
    * ZOPE: DMR HMM: [twisted.internet.interfaces.ILoggingContext], twisted.internet.interfaces.ILoggingContext, twisted.internet.interfaces.ILoggingContext
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, twisted.internet.interfaces.ILoggingContext
    * ZOPE: DMR HMM: [twisted.internet.interfaces.IProtocol], twisted.internet.interfaces.ILoggingContext, twisted.internet.interfaces.IProtocol
    * ZOPE: DMR HMM: <class 'list'>, <class 'mypy.types.Instance'>, twisted.internet.interfaces.IProtocol
      ZOPE: ============== APPENDING twisted.internet.interfaces.ILoggingContext =====
    

    In particular, notice the line:

     ZOPE: DMR HMM: [twisted.internet.interfaces.IProtocol], twisted.internet.interfaces.IProtocol, twisted.internet.interfaces.IProtocol
    

    (which comes from self.log(f"DMR HMM: {ti._promote}, {promote}, {ti.fullname}") in the patch). The log lines shows we're comparing ti._promote = [Instance(IProtocol)] with promote Instance(IProtocol). These two things are not equal[^1] therefore the any(...) expression here

    https://github.com/Shoobx/mypy-zope/blob/5139181a07febfde688af3035d00319942df4dcf/src/mypy_zope/plugin.py#L698

    is always False.[^2] So we add a duplicate IProtocol to the end of the MRO.

    A similar pattern proceeds for ILoggingContext.

    Fix

    Check if promote in ti._promote for any ti in impl.mro, now that ti._promote is a list.

    I didn't notice a huge performance cost (rm -r .mypy_cache/3.10/twisted/; mypy good.py; mypy bad.py takes 2 seconds on my machine with the patch). I haven' tried this on a bigger project like matrix-org/synapse yet. (I'm happy to, but I doubt that anyone is implementing that many interfaces for this to be a problem.)

    Regression

    I think this was introduced in #75. Presumably mypy made _promote a list of types rather than just a single type?

    [^1]: I even checked that Instance doesn't define a weird __req__. [^2]: I'm slightly surprised that mypy --strict-equality doesn't catch this!

    Tests

    Works For Me :tm: after applying the change:

     $ rm -r .mypy_cache/3.10/twisted/; mypy good.py; mypy bad.py
    Success: no issues found in 1 source file
    Success: no issues found in 1 source file
    

    make tests also passes on my machine.

    I wasn't sure how to add a test for this (apologies). I was uncertain about the following:

    • We need to run mypy on two files in a certain order. The current tests look to run each sample file in isolation(?)
    • I struggled to reproduce an example without pulling in Twisted as a dependency
    • We can't directly get at the MRO from mypy's output AFAIK. (No reveal_mro like there is reveal_type?)
    opened by DMRobertson 10
  • InterfaceClass subclass not recognized as interface

    InterfaceClass subclass not recognized as interface

    The project foolscap relies on zope.interface and creates a RemoteInterface as an instance of a subclass of zope.interface.interface.InterfaceClass. When tested against mypy with this plugin, the interface isn't recognized as such an results in "Method must have at least one argument" failed validations.

    As an example:

    draft $ cat > mypy.ini
    [mypy]
    ignore_missing_imports = True
    plugins=mypy_zope:plugin
    draft $ cat > example.py
    from foolscap.api import RemoteInterface
    
          
    class Api(RemoteInterface):
        def open():
            "open something"
    draft $ pip-run -q mypy foolscap mypy-zope -- -m mypy example.py
    example.py:5: error: Method must have at least one argument
    Found 1 error in 1 file (checked 1 source file)
    

    I've attempted to create an even more minimal example that doesn't involve foolscap, but I've been unsuccessful:

    draft $ cat > example.py
    from zope.interface import interface
    
    
    RemoteInterface = interface.InterfaceClass("RemoteInterface")
    
    
    class Api(RemoteInterface):
        def open():
            "open something"
    draft $ pip-run -q mypy mypy-zope -- -m mypy example.py
    example.py:7: error: Variable "example.RemoteInterface" is not valid as a type
    example.py:7: note: See https://mypy.readthedocs.io/en/latest/common_issues.html#variables-vs-type-aliases
    example.py:7: error: Invalid base class "RemoteInterface"
    example.py:8: error: Method must have at least one argument
    Found 3 errors in 1 file (checked 1 source file)
    

    I don't understand why

    Variable "example.RemoteInterface" is not valid as a type

    The note there isn't particularly helpful, as I'm creating neither an alias nor a variable. I'm creating a proper type and in the same way that foolscap does, so why does this example error where foolscap doesn't?

    And surely the example error isn't valid until mypy recogognizes RemoteInterface correctly.

    Can you help to determine a solution here to support foolscap.api.RemoteInterface subclasses within mypy?

    opened by jaraco 9
  • Subclasses of Twisted's protocol fail to be determined by mypy-zope

    Subclasses of Twisted's protocol fail to be determined by mypy-zope

    Thanks for making mypy-zope! We've been using it for Synapse (which uses Twisted) and ran into a hiccup.

    The following will cause mypy-zope to emit an error:

    error: Cannot determine consistent method resolution order (MRO) for "Foo" [misc]

    from twisted.internet import protocol
    
    class Foo(protocol.Protocol):
        pass
    
    reveal_type(Foo)
    

    The class hierarchy looks like Foo -> Protocol (which implements IProtocol and ILoggingContext) -> BaseProtocol.

    I tried to create a reduced test-case for this, but was not able to, so it might be specific to something Twisted has done. Any help would be appreciated!

    opened by clokep 8
  • Is there a way to declare Interface intersections?

    Is there a way to declare Interface intersections?

    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.

    opened by euresti 7
  • INTERNAL ERROR when enabling mypy-zope for Twisted Klein

    INTERNAL ERROR when enabling mypy-zope for Twisted Klein

    In commit https://github.com/twisted/klein/pull/471/commits/04a536c8b228d687fadd0d3e325753914d3e3b85, I enabled mypy-zope for Twisted Klein and I'm getting an INTERNAL ERROR in the build.

    Here is the error with --show-traceback added:

    wsanchez$ tox -e mypy -- --show-traceback src
    GLOB sdist-make: /Users/wsanchez/Dropbox/Developer/Twisted/klein/setup.py
    mypy inst-nodeps: /Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/.tmp/package/1/klein-20.6.0.zip
    mypy installed: attrs==20.3.0,Automat==20.2.0,characteristic==14.3.0,constantly==15.1.0,hyperlink==21.0.0,idna==3.1,incremental==17.5.0,klein @ file:///Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/.tmp/package/1/klein-20.6.0.zip,mypy==0.812,mypy-extensions==0.4.3,mypy-zope==0.2.11,PyHamcrest==2.0.2,six==1.15.0,Tubes==0.2.0,Twisted==20.3.0,typed-ast==1.4.2,typing-extensions==3.7.4.3,Werkzeug==1.0.1,zope.event==4.5.0,zope.interface==5.2.0,zope.schema==6.1.0
    mypy run-test-pre: PYTHONHASHSEED='3790822665'
    mypy run-test: commands[0] | mypy --cache-dir=/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy_cache --pretty --show-traceback src
    src/klein/test/test_trial.py:33: error: INTERNAL ERROR -- Please try using mypy master on Github:
    https://mypy.rtfd.io/en/latest/common_issues.html#using-a-development-mypy-build
    Please report a bug at https://github.com/python/mypy/issues
    version: 0.812
    Traceback (most recent call last):
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/bin/mypy", line 8, in <module>
        sys.exit(console_entry())
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/__main__.py", line 11, in console_entry
        main(None, sys.stdout, sys.stderr)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/main.py", line 90, in main
        res = build.build(sources, options, None, flush_errors, fscache, stdout, stderr)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/build.py", line 179, in build
        result = _build(
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/build.py", line 253, in _build
        graph = dispatch(sources, manager, stdout)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/build.py", line 2638, in dispatch
        process_graph(graph, manager)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/build.py", line 2962, in process_graph
        process_stale_scc(graph, scc, manager)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/build.py", line 3060, in process_stale_scc
        graph[id].type_check_first_pass()
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/build.py", line 2130, in type_check_first_pass
        self.type_checker().check_first_pass()
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checker.py", line 294, in check_first_pass
        self.accept(d)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checker.py", line 401, in accept
        stmt.accept(self)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/nodes.py", line 950, in accept
        return visitor.visit_class_def(self)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checker.py", line 1723, in visit_class_def
        self.accept(defn.defs)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checker.py", line 401, in accept
        stmt.accept(self)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/nodes.py", line 1015, in accept
        return visitor.visit_block(self)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checker.py", line 1972, in visit_block
        self.accept(s)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checker.py", line 401, in accept
        stmt.accept(self)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/nodes.py", line 950, in accept
        return visitor.visit_class_def(self)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checker.py", line 1749, in visit_class_def
        sig, _ = self.expr_checker.check_call(dec, [temp],
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checkexpr.py", line 930, in check_call
        result = self.check_call(call_function, args, arg_kinds, context, arg_names,
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checkexpr.py", line 911, in check_call
        return self.check_callable_call(callee, args, arg_kinds, context, arg_names,
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checkexpr.py", line 1023, in check_callable_call
        new_ret_type = self.apply_function_plugin(
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy/checkexpr.py", line 732, in apply_function_plugin
        return method_callback(
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy_zope/plugin.py", line 171, in analyze_implementation
        iface_type = self._lookup_type(ifacename, method_ctx.api)
      File "/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/lib/python3.9/site-packages/mypy_zope/plugin.py", line 583, in _lookup_type
        module = api.modules[module_name]
    KeyError: 'klein.test.test_trial.TestCaseTests'
    src/klein/test/test_trial.py:33: : note: use --pdb to drop into pdb
    ERROR: InvocationError for command /Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy/bin/mypy --cache-dir=/Users/wsanchez/Dropbox/Developer/Twisted/klein/.tox/mypy_cache --pretty --show-traceback src (exited with code 2)
    __________________________________________ summary __________________________________________
    ERROR:   mypy: commands failed
    
    opened by wsanchez 6
  • error: implementer accepts interface, not {interface subclass}

    error: implementer accepts interface, not {interface subclass}

    I think I may have stumbled onto another defect in mypy.zope. Consider this code:

    from zope.interface import implementer
    from twisted.cred.credentials import ICredentials
    
    
    class IToken(ICredentials):
        pass
    
    
    @implementer(IToken)
    class Token:
        pass
    
    

    Run that through mypy with this mypy.ini:

    [mypy]
    ignore_missing_imports = True
    plugins=mypy_zope:plugin
    

    With twisted and zope interface installed, and this error occurs:

    $ .tox/typechecks/bin/python -m mypy --no-incremental creds.py
    creds.py:9: error: zope.interface.implementer accepts interface, not creds.IToken.
    creds.py:9: error: Make sure you have stubs for all packages that provide interfaces for creds.IToken class hierarchy.
    Found 2 errors in 1 file (checked 1 source file)
    

    But make either of these tweaks and the errors go away:

    1. Save the implementer in a separate variable.
    from zope.interface import implementer
    from twisted.cred.credentials import ICredentials
    
    
    class IToken(ICredentials):
        pass
    
    
    _impl = implementer(IToken)
    
    @_impl
    class Token:
        pass
    
    1. Inline the definition of ICredentials.
    from zope.interface import implementer, Interface
    
    
    class ICredentials(Interface):
        pass
    
    
    class IToken(ICredentials):
        pass
    
    
    @implementer(IToken)
    class Token:
        pass
    
    
    opened by jaraco 6
  • Support for variable / alias types.

    Support for variable / alias types.

    Not sure if this is a mypy-zope or mypy issue.

    It looks like variable vs type aliases is not supported by mypy-zope

    I am looking at the folloing mypy documentation https://mypy.readthedocs.io/en/latest/common_issues.html#variables-vs-type-aliases

    I have the following code:

    class _IWaker(Interface):
        """
        Interface to wake up the event loop based on the self-pipe trick.
    
        The U{I{self-pipe trick}<http://cr.yp.to/docs/selfpipe.html>}, used to wake
        up the main loop from another thread or a signal handler.
        This is why we have wakeUp together with doRead
    
        This is used by threads or signals to wake up the event loop.
        """
        disconnected = Attribute('')
    
        def wakeUp():
            """
            Called when the event should be wake up.
            """
    
        def doRead():
            """
            Read some data from my connection and discard it.
            """
    
        def connectionLost(reason: failure.Failure):
            """
            Called when connection was closed and the pipes.
            """
    
    
    @implementer(_IWaker)
    class _SocketWaker(log.Logger):
        disconnected = 0
    
        def __init__(self, reactor):
            """Initialize.
            """
    
        def wakeUp(self):
            """Send a byte to my connection.
            """
    
    
        def doRead(self):
            """
            Read some data from my connection.
            """
    
    
        def connectionLost(self, reason):
    
    
    class _FDWaker(log.Logger, object):
        disconnected = 0
    
        def __init__(self, reactor):
            """Initialize.
            """
    
    
        def doRead(self):
            """
            Read some bytes from the pipe and discard them.
            """
    
    
        def connectionLost(self, reason):
            """Close both ends of my pipe.
            """
    
    
    @implementer(_IWaker)
    class _UnixWaker(_FDWaker):
        def wakeUp(self):
            """Write one byte to the pipe, and flush it.
            """
    
    
    _Waker: _IWaker
    if platformType == 'posix':
        _Waker = _UnixWaker
    else:
        # Primarily Windows and Jython.
        _Waker = _SocketWaker
    
    

    with mypy I got the following error:

    src/twisted/internet/posixbase.py:228:14: error: Incompatible types in assignment (expression has type "Type[_UnixWaker]", variable has type "_IWaker")  [assignment]
    src/twisted/internet/posixbase.py:231:14: error: Incompatible types in assignment (expression has type "Type[_SocketWaker]", variable has type "_IWaker")  [assignment]
    

    I am expecting this not to raise any issue.

    Thanks!

    opened by adiroiban 6
  • plugin causes open() signature to change

    plugin causes open() signature to change

    Hi,

    I ran across an interesting issue. The signature of open() unexpectedly became typing.IO[Any] instead of typing.TextIO when the plugin is enabled.

    example.py:

    reveal_type(open('str'))
    

    plugin.ini:

    [mypy]
    plugins = mypy_zope:plugin
    

    Here is small test:

    $ more example.py 
    reveal_type(open('str'))
    $ more plugin.ini 
    [mypy]
    plugins = mypy_zope:plugin
    $ mypy --config plugin.ini example.py 
    example.py:1: note: Revealed type is 'typing.IO[Any]'
    $ mypy example.py 
    example.py:1: note: Revealed type is 'typing.TextIO'
    

    I created a test-case here:

    https://github.com/Shoobx/mypy-zope/compare/master...arnimarj:bug/open-signature-changes

    opened by arnimarj 6
  • Return interface in functions getUtility, getAdapter...

    Return interface in functions getUtility, getAdapter...

    Hi,

    First of all, thank you for this amazing library. I'm using this library on some projects with extensive use of getUtility and getAdapter, and I was expecting that these functions would have as a return type the interface of provides argument. Right now we need to do add the interface explicitly u: IMyUtility = getUtility(IMyUtility) to get type validation.

    from zope.interface import Interface
    from zope.interface import implementer
    from zope.component import getUtility
    
    
    class IMyUtility(Interface):
        def foo(x: int):
            pass
    
    
    @implementer(IMyUtility)
    class MyUtility():
        def foo(self, x):
            return 2 * x
    
    
    u = getUtility(IMyUtility)
    # Mypy doesn't produce any error
    u.foo("2")
    
    
    u2: IMyUtility = getUtility(IMyUtility)
    # Mypy validation works and provides this error:
    #   error: Argument 1 to "foo" of "IMyUtility" has incompatible type "str"; expected "int"
    u2.foo("2")
    

    I tried adding def getUtility(self, provided: T, name: str = ...) -> T: ... (https://github.com/Shoobx/mypy-zope/blob/master/src/zope-stubs/interface/registry.pyi#L27) but I'm getting Type[IMyUtility] instead of IMyUtility.

    Any idea on how to fix it? Does it make sense to add this to mypy-zope?

    Thank you!

    opened by masipcat 5
  • Caching error (Metaclass conflict) with mypy 0.991

    Caching error (Metaclass conflict) with mypy 0.991

    Hi. With the latest version of mypy (0.991) and mypy-zope (0.3.11) we're getting mypy errors that I think are cache related. The error goes away after rm -rf .mypy_cache

    Here's the easiest repro I could make:

    iface.py

    from zope.interface import Interface
    from zope.interface import implementer
    
    class ISomething(Interface):
        pass
    
    class ISomething2(Interface):
        pass
    
    @implementer(ISomething)
    class OneInterface:
        pass
    
    @implementer(ISomething, ISomething2)
    class TwoInterfaces:
        pass
    

    foo.py

    from iface import OneInterface, TwoInterfaces
    
    class Good(OneInterface):
        pass
    
    class Bad(TwoInterfaces):
        pass
    

    I am able to reliably hit the problem by running the following commands:

    $ rm -rf .mypy_cache
    $ mypy iface.py 
    Success: no issues found in 1 source file
    $ mypy foo.py 
    foo.py:6: error: Metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases  [misc]
    Found 1 error in 1 file (checked 1 source file)
    

    As you can tell this only happens when there are multiple interfaces.

    opened by euresti 0
  • Bump mypy from 0.981 to 0.991

    Bump mypy from 0.981 to 0.991

    Bumps mypy from 0.981 to 0.991.

    Commits
    • b7788fc Update version to remove "+dev" for releasing 0.991
    • 6077d19 manually CP typeshed #9130
    • ab0ea1e Fix crash with function redefinition (#14064)
    • 592a9ce Fix another crash with report generation on namespace packages (#14063)
    • 1650ae0 Update --no-warn-no-return docs for empty body changes (#14065)
    • b9daa31 Don't ignore errors in files passed on the command line (#14060)
    • 02fd8a5 Filter out wasm32 wheel in upload-pypi.py (#14035)
    • 131c8d7 Fix crash on inference with recursive alias to recursive instance (#14038)
    • 1368338 Change version to 0.991+dev in preparation for the point release
    • b71dc3d Remove +dev from version
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    dependencies 
    opened by dependabot[bot] 0
  • Regression in upcoming mypy 0.990

    Regression in upcoming mypy 0.990

    Hi. I was testing the upcoming mypy release and it seems to be emitting new warning for interface definitions:

    import zope.interface
    
    
    class IZomething(zope.interface.Interface):
    	def Query(a: str, b: str) -> list[int]:
    		pass
    

    gives:

    $ mypy --config=mypy.ini int.py 
    int.py:5: error: Missing return statement  [empty-body]
    Found 1 error in 1 file (checked 1 source file)
    

    Replacing the function body with ellipses silences the warning. Perhaps that should be the preferred way to define interfaces moving forward?

    opened by arnimarj 3
  • adaptation callable ought to return a union type with its second argument

    adaptation callable ought to return a union type with its second argument

    i.e.

    from zope.interface import Interface
    
    class IFoo(Interface):
        def foo() -> int:
            ...
    
    reveal_type(IFoo(object(), None))
    
    Revealed type is "IFoo"
    

    This ought to be IFoo | None.

    opened by glyph 0
  • Error importing plugin

    Error importing plugin "mypy_zope": cannot import name 'SemanticAnalyzerPass2' from 'mypy.semanal'

    Tried to update to mypy 0.942, but I started seeing this:

    mypy.ini:4: error: Error importing plugin "mypy_zope": cannot import name 'SemanticAnalyzerPass2' from 'mypy.semanal' (/home/runner/.cache/pre-commit/repo_fawoait/py_env-python3.8/lib/python3.8/site-packages/mypy/semanal.cpython-38-x86_64-linux-gnu.so)  [misc]
    

    I do see a green build on https://github.com/Shoobx/mypy-zope/pull/68, which is a bit curious.

    Is there any more info I can provide to help debug this? My current workaround it to keep mypy at 0.941.

    opened by pkoch 1
Owner
Shoobx
Shoobx
Mypy stubs, i.e., type information, for numpy, pandas and matplotlib

Mypy type stubs for NumPy, pandas, and Matplotlib This is a PEP-561-compliant stub-only package which provides type information for matplotlib, numpy

Predictive Analytics Lab 194 Dec 19, 2022
A plugin for Flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle.

flake8-bugbear A plugin for Flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycode

Python Code Quality Authority 869 Dec 30, 2022
Pylint plugin for improving code analysis for when using Django

pylint-django About pylint-django is a Pylint plugin for improving code analysis when analysing code using Django. It is also used by the Prospector t

Python Code Quality Authority 544 Jan 6, 2023
❄️ A flake8 plugin to help you write better list/set/dict comprehensions.

flake8-comprehensions A flake8 plugin that helps you write better list/set/dict comprehensions. Requirements Python 3.6 to 3.9 supported. Installation

Adam Johnson 398 Dec 23, 2022
Flake8 plugin that checks import order against various Python Style Guides

flake8-import-order A flake8 and Pylama plugin that checks the ordering of your imports. It does not check anything else about the imports. Merely tha

Python Code Quality Authority 270 Nov 24, 2022
flake8 plugin that integrates isort

Flake8 meet isort Use isort to check if the imports on your python files are sorted the way you expect. Add an .isort.cfg to define how you want your

Gil Forcada Codinachs 139 Nov 8, 2022
Flake8 plugin to find commented out or dead code

flake8-eradicate flake8 plugin to find commented out (or so called "dead") code. This is quite important for the project in a long run. Based on eradi

wemake.services 277 Dec 27, 2022
A Pylint plugin to analyze Flask applications.

pylint-flask About pylint-flask is Pylint plugin for improving code analysis when editing code using Flask. Inspired by pylint-django. Problems pylint

Joe Schafer 62 Sep 18, 2022
flake8 plugin to run black for checking Python coding style

flake8-black Introduction This is an MIT licensed flake8 plugin for validating Python code style with the command line code formatting tool black. It

Peter Cock 146 Dec 15, 2022
A plugin for Flake8 that checks pandas code

pandas-vet pandas-vet is a plugin for flake8 that provides opinionated linting for pandas code. It began as a project during the PyCascades 2019 sprin

Jacob Deppen 146 Dec 28, 2022
flake8 plugin to catch useless `assert` statements

flake8-useless-assert flake8 plugin to catch useless assert statements Download or install on the PyPI page Violations Code Description Example ULA001

null 1 Feb 12, 2022
coala provides a unified command-line interface for linting and fixing all your code, regardless of the programming languages you use.

"Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live." ― John F. Woods coala provides a

coala development group 3.4k Dec 29, 2022
Mypy static type checker plugin for Pytest

pytest-mypy Mypy static type checker plugin for pytest Features Runs the mypy static type checker on your source files as part of your pytest test run

Dan Bader 218 Jan 3, 2023
A Fast API style support for Flask. Gives you MyPy types with the flexibility of flask

Flask-Fastx Flask-Fastx is a Fast API style support for Flask. It Gives you MyPy types with the flexibility of flask. Compatibility Flask-Fastx requir

Tactful.ai 18 Nov 26, 2022
open source tools to generate mypy stubs from protobufs

mypy-protobuf: Generate mypy stub files from protobuf specs We just released a new major release mypy-protobuf 2. on 02/02/2021! It includes some back

Dropbox 527 Jan 3, 2023
Mypy stubs, i.e., type information, for numpy, pandas and matplotlib

Mypy type stubs for NumPy, pandas, and Matplotlib This is a PEP-561-compliant stub-only package which provides type information for matplotlib, numpy

Predictive Analytics Lab 194 Dec 19, 2022
A shim for the typeshed changes in mypy 0.900

types-all A shim for the typeshed changes in mypy 0.900 installation pip install types-all why --install-types is annoying, this installs all the thin

Anthony Sottile 28 Oct 20, 2022
Drop-in replacement of Django admin comes with lots of goodies, fully extensible with plugin support, pretty UI based on Twitter Bootstrap.

Xadmin Drop-in replacement of Django admin comes with lots of goodies, fully extensible with plugin support, pretty UI based on Twitter Bootstrap. Liv

差沙 4.7k Dec 31, 2022