Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can't make Mypy work with __init__.py aliases

Tags:

python

mypy

Below is the minimal example of my problem:

[test/__init__.py]
from test.test1 import Test1
from test.test2 import Test2

[test/test1.py]
class Test1:
    pass

[test/test2.py]
from test import Test1

class Test2:
    pass

Mypy output for module or __init__.py:

test/test2.py:1: error: Module 'test' has no attribute 'Test1'

Code itself works well both on Python 2 and Python 3.

like image 358
freopen Avatar asked Jun 03 '17 13:06

freopen


1 Answers

This is because your code has an import cycle -- in order for mypy to parse test/__init__.py, it needs to understand exactly what Test2 is. (After all, what if you decide to use Test2 later on in that file/call one of its methods? Then mypy needs to know what the output is).

So, it hits that import, effectively pauses, and jumps to trying to understand what test/test2.py is doing*.

But within test/test2.py, we run into the exact same problem -- we see the import, and need to jump back to test/__init__.py in order to understand what Test1 is... But we haven't finished parsing that file!

This is where mypy differs from the Python runtime, fyi -- mypy can only parse entire files at a time, but the Python runtime will actually pause execution to run test/test2.py. That means when you do from test import Test1, the test module has a partially complete symbol space that happens to currently contain only Test1, instead of both Test1 and Test2, which is why your code works at runtime.

The fix, in this case, would be to modify the import in test/test2.py to:

from test.test1 import Test1

This breaks the import cycle.


*This isn't actually what mypy does -- what it actually does is attempt to resolve import cycles by first identifying all strongly connected components (SCC) -- each SCC is basically an import cycle.

It then applies some heuristics to determine the order in which the files within the SCC should be processed, but it's an imperfect process and can't resolve all import cycles.

For example, in your case, whether we process test or test.test2 first, we're going to run into issues.

You can see the SCCs mypy identifies by re-running mypy using the -v flag (for verbose mode).

You can find more details about the algorithm mypy uses within the source code here. Specific details about the import cycle resolution algorithm can be found a little lower down, here.

(I suspect this entire comment is slightly out-of-date/incomplete, fyi -- there are several other wrinkles related to mypy's silent import mechanism and its incremental mode mechanism that aren't really explained.)

Info about the exact heuristics mypy uses to order SCCs can be found here. (Basically, the exact syntax we use for importing something can give some hints about how that SCC should be ordered).

like image 109
Michael0x2a Avatar answered Nov 01 '22 18:11

Michael0x2a