Determining why a particular object does not implement a Protocol

Phil Frost :

Consider I define a protocol Frobbable. And further I have a valid implementation of the protocol, and a broken implementation which is missing the .frob() method:

from typing import Protocol
from abc import abstractmethod


class Frobbable(Protocol):
    @abstractmethod
    def frob(self) -> None:
        raise NotImplementedError


def main(knob: Frobbable) -> None:
    knob.frob()


class Knob:
    def frob(self) -> None:
        print("knob has been frobbed")


class BrokenKnob:
    pass


main(Knob())
main(BrokenKnob())

Checking this program with mypy results in an error, as expected:

testprotocol.py:25:6: error: Argument 1 to "main" has incompatible type "BrokenKnob"; expected "Frobbable"  [arg-type]
    main(BrokenKnob())
         ^
Found 1 error in 1 file (checked 1 source file)

Unfortunately, it doesn't offer any information about why BrokenKnob is incompatible: in this case, it is missing .frob(). Without such information, correcting such an issue in a nontrivial program (with protocols with many methods, and many implementations) becomes a profoundly tedious chore.

Is there any way to get this information from mypy or any other tool without modifying the program? I know I can explicitly subclass Frobbable, but this rather defeats the purpose of using a Protocol.

Michael0x2a :

I think this is due to a limitation in the heuristics mypy uses to list missing protocol members. Normally, mypy is supposed to report any missing protocol members, but to avoid generating too many spammy error messages, it doesn't bother doing so if every one of the protocol members are missing or if the number of missing members exceeds 2.

For example, if we tweak your example so it falls under these constraints...

from typing import Protocol
from abc import abstractmethod


class Frobbable(Protocol):
    @abstractmethod
    def frob(self) -> None:
        raise NotImplementedError
    @abstractmethod
    def bob(self) -> None:
        raise NotImplementedError


def main(knob: Frobbable) -> None:
    knob.frob()


class BrokenKnob:
    def frob(self) -> None:
        raise NotImplementedError


main(BrokenKnob())

...we get the more descriptive error message as expected:

test.py:23: error: Argument 1 to "main" has incompatible type "BrokenKnob"; expected "Frobbable"
test.py:23: note: 'BrokenKnob' is missing following 'Frobbable' protocol member:
test.py:23: note:     bob

While these heuristics do seem reasonable, I also think they could probably do with some more refinement to better handle use cases like the one you're running into. For example, if all of the members are missing, I think it'd be reasonable for mypy to report a "this object doesn't implement anything in the protocol" error message instead of the more generic one, and perhaps handle cases where there are too many missing members more gracefully. You could maybe try submitting a PR to improve these heuristics, if you're up for it?

If you don't have time, one workaround you could try doing to get the full list is to:

  1. Ensure all members of Frobbable are abstract (which you're already doing)
  2. Make BrokenKnob temporarily subclass Frobbable
  3. Try temporarily creating a new instance of BrokenKnob.

The resulting error seems to list all the missing attributes without having a fixed limit.

error: Cannot instantiate abstract class 'BrokenKnob' with abstract attribute 'frob'

Then, once you finish making your fixes, you can undo your temporary changes.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=24307&siteId=1