Do not add the same interface multiple times to the MRO, round 2 #1
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
For your consideration: a patch which I believe fixes Shoobx#76. This tackles the same kind of problem that Shoobx#41 addresses. Seems to be a regression introduced in Shoobx#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 Shoobx#76:
Investigation
To attempt to understand what was going on, I added a bunch of debug
print
statements tomypy
andmypy-zope
until I reached enlightenment.I don't fully grok mypy's machinery, but from what I can tell:
linearize_hierarchy
.linearize_heirarchy
of each of our base classes.When I run
mypy bad.py
I see that linear_hierarchy is called forHTTPPageGetter
with the followingTypeInfo
:In particular the MRO contains two copies of
IProtocol
andILoggingContext
, which doesn't seem right (c.f. Shoobx#41). As I understand it,mypy bad.py
is loading theTypeInfo
forHttpPageGetter
that we persisted at the end ofmypy 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 tolinearize_hierarchy
isLater, mypy computes the MRO for a subclass
twisted.protocols.policies.ProtocolWrapper
. Linearize hierarchy receivesand recursively calls itself with the
twisted.internet.protocol.Protocol
TypeInfo
. This time the MRO and Names have already been computed!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 fortwisted.internet.protocol.Protocol
. But the MRO now has duplicate interfaces.Explanation
The following dirty patch adds useful logging.
Dirty patch
We can then see where mypy-zope is adding the duplicate interfaces.
In particular, notice the line:
(which comes from
self.log(f"DMR HMM: {ti._promote}, {promote}, {ti.fullname}")
in the patch). The log lines shows we're comparingti._promote = [Instance(IProtocol)]
withpromote Instance(IProtocol)
. These two things are not equal1 therefore theany(...)
expression herehttps://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 anyti in impl.mro
, now thatti._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?Tests
Works For Me ™️ after applying the change:
I wasn't sure how to add a test for this (apologies). I was uncertain about the following:
reveal_mro
like there isreveal_type
?)Footnotes
I even checked that
Instance
doesn't define a weird__req__
. ↩I'm slightly surprised that
mypy --strict-equality
doesn't catch this! ↩