Skip to content

Commit a4da89e

Browse files
authored
Better story for import redefinitions (#13969)
This changes our importing logic to be more consistent and to treat import statements more like assignments. Fixes #13803, fixes #13914, fixes half of #12965, probably fixes #12574 The primary motivation for this is when typing modules as protocols, as in #13803. But it turns out we already allowed redefinition with "from" imports, so this also seems like a nice consistency win. We move shared logic from visit_import_all and visit_import_from (via process_imported_symbol) into add_imported_symbol. We then reuse it in visit_import. To simplify stuff, we inline the code from add_module_symbol into visit_import. Then we copy over logic from add_symbol, because MypyFile is not a SymbolTableNode, but this isn't the worst thing ever. Finally, we now need to check non-from import statements like assignments, which was a thing we weren't doing earlier.
1 parent 0457d33 commit a4da89e

7 files changed

+119
-54
lines changed

mypy/checker.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -2527,8 +2527,8 @@ def visit_import_from(self, node: ImportFrom) -> None:
25272527
def visit_import_all(self, node: ImportAll) -> None:
25282528
self.check_import(node)
25292529

2530-
def visit_import(self, s: Import) -> None:
2531-
pass
2530+
def visit_import(self, node: Import) -> None:
2531+
self.check_import(node)
25322532

25332533
def check_import(self, node: ImportBase) -> None:
25342534
for assign in node.assignments:

mypy/semanal.py

+39-45
Original file line numberDiff line numberDiff line change
@@ -2235,13 +2235,33 @@ def visit_import(self, i: Import) -> None:
22352235
base_id = id.split(".")[0]
22362236
imported_id = base_id
22372237
module_public = use_implicit_reexport
2238-
self.add_module_symbol(
2239-
base_id,
2240-
imported_id,
2241-
context=i,
2242-
module_public=module_public,
2243-
module_hidden=not module_public,
2244-
)
2238+
2239+
if base_id in self.modules:
2240+
node = self.modules[base_id]
2241+
if self.is_func_scope():
2242+
kind = LDEF
2243+
elif self.type is not None:
2244+
kind = MDEF
2245+
else:
2246+
kind = GDEF
2247+
symbol = SymbolTableNode(
2248+
kind, node, module_public=module_public, module_hidden=not module_public
2249+
)
2250+
self.add_imported_symbol(
2251+
imported_id,
2252+
symbol,
2253+
context=i,
2254+
module_public=module_public,
2255+
module_hidden=not module_public,
2256+
)
2257+
else:
2258+
self.add_unknown_imported_symbol(
2259+
imported_id,
2260+
context=i,
2261+
target_name=base_id,
2262+
module_public=module_public,
2263+
module_hidden=not module_public,
2264+
)
22452265

22462266
def visit_import_from(self, imp: ImportFrom) -> None:
22472267
self.statement = imp
@@ -2377,19 +2397,6 @@ def process_imported_symbol(
23772397
module_hidden=module_hidden,
23782398
becomes_typeinfo=True,
23792399
)
2380-
existing_symbol = self.globals.get(imported_id)
2381-
if (
2382-
existing_symbol
2383-
and not isinstance(existing_symbol.node, PlaceholderNode)
2384-
and not isinstance(node.node, PlaceholderNode)
2385-
):
2386-
# Import can redefine a variable. They get special treatment.
2387-
if self.process_import_over_existing_name(imported_id, existing_symbol, node, context):
2388-
return
2389-
if existing_symbol and isinstance(node.node, PlaceholderNode):
2390-
# Imports are special, some redefinitions are allowed, so wait until
2391-
# we know what is the new symbol node.
2392-
return
23932400
# NOTE: we take the original node even for final `Var`s. This is to support
23942401
# a common pattern when constants are re-exported (same applies to import *).
23952402
self.add_imported_symbol(
@@ -2507,14 +2514,9 @@ def visit_import_all(self, i: ImportAll) -> None:
25072514
if isinstance(node.node, MypyFile):
25082515
# Star import of submodule from a package, add it as a dependency.
25092516
self.imports.add(node.node.fullname)
2510-
existing_symbol = self.lookup_current_scope(name)
2511-
if existing_symbol and not isinstance(node.node, PlaceholderNode):
2512-
# Import can redefine a variable. They get special treatment.
2513-
if self.process_import_over_existing_name(name, existing_symbol, node, i):
2514-
continue
25152517
# `from x import *` always reexports symbols
25162518
self.add_imported_symbol(
2517-
name, node, i, module_public=True, module_hidden=False
2519+
name, node, context=i, module_public=True, module_hidden=False
25182520
)
25192521

25202522
else:
@@ -5589,24 +5591,6 @@ def add_local(self, node: Var | FuncDef | OverloadedFuncDef, context: Context) -
55895591
node._fullname = name
55905592
self.add_symbol(name, node, context)
55915593

5592-
def add_module_symbol(
5593-
self, id: str, as_id: str, context: Context, module_public: bool, module_hidden: bool
5594-
) -> None:
5595-
"""Add symbol that is a reference to a module object."""
5596-
if id in self.modules:
5597-
node = self.modules[id]
5598-
self.add_symbol(
5599-
as_id, node, context, module_public=module_public, module_hidden=module_hidden
5600-
)
5601-
else:
5602-
self.add_unknown_imported_symbol(
5603-
as_id,
5604-
context,
5605-
target_name=id,
5606-
module_public=module_public,
5607-
module_hidden=module_hidden,
5608-
)
5609-
56105594
def _get_node_for_class_scoped_import(
56115595
self, name: str, symbol_node: SymbolNode | None, context: Context
56125596
) -> SymbolNode | None:
@@ -5653,13 +5637,23 @@ def add_imported_symbol(
56535637
self,
56545638
name: str,
56555639
node: SymbolTableNode,
5656-
context: Context,
5640+
context: ImportBase,
56575641
module_public: bool,
56585642
module_hidden: bool,
56595643
) -> None:
56605644
"""Add an alias to an existing symbol through import."""
56615645
assert not module_hidden or not module_public
56625646

5647+
existing_symbol = self.lookup_current_scope(name)
5648+
if (
5649+
existing_symbol
5650+
and not isinstance(existing_symbol.node, PlaceholderNode)
5651+
and not isinstance(node.node, PlaceholderNode)
5652+
):
5653+
# Import can redefine a variable. They get special treatment.
5654+
if self.process_import_over_existing_name(name, existing_symbol, node, context):
5655+
return
5656+
56635657
symbol_node: SymbolNode | None = node.node
56645658

56655659
if self.is_class_scope():

test-data/unit/check-classes.test

+1-2
Original file line numberDiff line numberDiff line change
@@ -7414,8 +7414,7 @@ class Foo:
74147414
def meth1(self, a: str) -> str: ... # E: Name "meth1" already defined on line 5
74157415

74167416
def meth2(self, a: str) -> str: ...
7417-
from mod1 import meth2 # E: Unsupported class scoped import \
7418-
# E: Name "meth2" already defined on line 8
7417+
from mod1 import meth2 # E: Incompatible import of "meth2" (imported name has type "Callable[[int], int]", local name has type "Callable[[Foo, str], str]")
74197418

74207419
class Bar:
74217420
from mod1 import foo # E: Unsupported class scoped import

test-data/unit/check-incremental.test

+1-4
Original file line numberDiff line numberDiff line change
@@ -1025,10 +1025,7 @@ import a.b
10251025

10261026
[file a/b.py]
10271027

1028-
[rechecked b]
1029-
[stale]
1030-
[out2]
1031-
tmp/b.py:4: error: Name "a" already defined on line 3
1028+
[stale b]
10321029

10331030
[case testIncrementalSilentImportsAndImportsInClass]
10341031
# flags: --ignore-missing-imports

test-data/unit/check-modules.test

+19
Original file line numberDiff line numberDiff line change
@@ -651,10 +651,29 @@ try:
651651
from m import f, g # E: Incompatible import of "g" (imported name has type "Callable[[Any, Any], Any]", local name has type "Callable[[Any], Any]")
652652
except:
653653
pass
654+
655+
import m as f # E: Incompatible import of "f" (imported name has type "object", local name has type "Callable[[Any], Any]")
656+
654657
[file m.py]
655658
def f(x): pass
656659
def g(x, y): pass
657660

661+
[case testRedefineTypeViaImport]
662+
from typing import Type
663+
import mod
664+
665+
X: Type[mod.A]
666+
Y: Type[mod.B]
667+
from mod import B as X
668+
from mod import A as Y # E: Incompatible import of "Y" (imported name has type "Type[A]", local name has type "Type[B]")
669+
670+
import mod as X # E: Incompatible import of "X" (imported name has type "object", local name has type "Type[A]")
671+
672+
[file mod.py]
673+
class A: ...
674+
class B(A): ...
675+
676+
658677
[case testImportVariableAndAssignNone]
659678
try:
660679
from m import x

test-data/unit/check-protocols.test

+56
Original file line numberDiff line numberDiff line change
@@ -3787,3 +3787,59 @@ from typing_extensions import Final
37873787

37883788
a: Final = 1
37893789
[builtins fixtures/module.pyi]
3790+
3791+
3792+
[case testModuleAsProtocolRedefinitionTopLevel]
3793+
from typing import Protocol
3794+
3795+
class P(Protocol):
3796+
def f(self) -> str: ...
3797+
3798+
cond: bool
3799+
t: P
3800+
if cond:
3801+
import mod1 as t
3802+
else:
3803+
import mod2 as t
3804+
3805+
import badmod as t # E: Incompatible import of "t" (imported name has type Module, local name has type "P")
3806+
3807+
[file mod1.py]
3808+
def f() -> str: ...
3809+
3810+
[file mod2.py]
3811+
def f() -> str: ...
3812+
3813+
[file badmod.py]
3814+
def nothing() -> int: ...
3815+
[builtins fixtures/module.pyi]
3816+
3817+
[case testModuleAsProtocolRedefinitionImportFrom]
3818+
from typing import Protocol
3819+
3820+
class P(Protocol):
3821+
def f(self) -> str: ...
3822+
3823+
cond: bool
3824+
t: P
3825+
if cond:
3826+
from package import mod1 as t
3827+
else:
3828+
from package import mod2 as t
3829+
3830+
from package import badmod as t # E: Incompatible import of "t" (imported name has type Module, local name has type "P")
3831+
3832+
package: int = 10
3833+
3834+
import package.mod1 as t
3835+
import package.mod1 # E: Incompatible import of "package" (imported name has type Module, local name has type "int")
3836+
3837+
[file package/mod1.py]
3838+
def f() -> str: ...
3839+
3840+
[file package/mod2.py]
3841+
def f() -> str: ...
3842+
3843+
[file package/badmod.py]
3844+
def nothing() -> int: ...
3845+
[builtins fixtures/module.pyi]

test-data/unit/check-redefine.test

+1-1
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def f() -> None:
285285
import typing as m
286286
m = 1 # E: Incompatible types in assignment (expression has type "int", variable has type Module)
287287
n = 1
288-
import typing as n # E: Name "n" already defined on line 5
288+
import typing as n # E: Incompatible import of "n" (imported name has type Module, local name has type "int")
289289
[builtins fixtures/module.pyi]
290290

291291
[case testRedefineLocalWithTypeAnnotation]

0 commit comments

Comments
 (0)