Skip to content

Commit 7f2377e

Browse files
authored
Fix spurious name undefined error in class body within import cycle (#10498)
This could sometimes happen with protobuf stubs. The issue is a quite tricky one, since it only happens if files in an import cycle are checked in a specific order. The order may depend on the order files are given as arguments to mypy. The problem was with code like this, if `Foo` and `Base` are defined in different files within an import cycle: ``` # m.py from m2 import Base class Foo(Base): x: Bar # <<-- Unexpected error: "Bar" undefined class Bar: ... ``` Due to the import cycle, `Base` could be a placeholder node when semantically analyzing `m` for the first time. This caused another pass over `m`. On the second pass `Bar` was reported as undefined, because of an incorrect namespace completeness check. We were checking the completeness of the *module-level* namespace, when we should have looked at the completeness of the *class* namespace. If `Base` was ready during the first pass, the example worked as expected, since neither the module nor the class namespace was complete. Errors about undefined things are only supposed to be generated when the target namespace is complete (i.e., all names are included in the symbol table, possibly as placholders). This fixes the issue by keeping track of whether a class body is being processed for the first time. During the first time the namespace is being built, so it's incomplete. This may not work in some very tricky scenarios where we need to process the body of a class more than twice, but these cases are probably very rare, so this fix should get us most of the way there.
1 parent 02e016f commit 7f2377e

File tree

4 files changed

+60
-3
lines changed

4 files changed

+60
-3
lines changed

mypy/semanal.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ def __init__(self,
250250
self.imports = set()
251251
self.type = None
252252
self.type_stack = []
253+
# Are the namespaces of classes being processed complete?
254+
self.incomplete_type_stack = [] # type: List[bool]
253255
self.tvar_scope = TypeVarLikeScope()
254256
self.function_stack = []
255257
self.block_depth = [0]
@@ -526,6 +528,7 @@ def file_context(self,
526528
self.num_incomplete_refs = 0
527529

528530
if active_type:
531+
self.incomplete_type_stack.append(False)
529532
scope.enter_class(active_type)
530533
self.enter_class(active_type.defn.info)
531534
for tvar in active_type.defn.type_vars:
@@ -537,6 +540,7 @@ def file_context(self,
537540
scope.leave()
538541
self.leave_class()
539542
self.type = None
543+
self.incomplete_type_stack.pop()
540544
scope.leave()
541545
del self.options
542546

@@ -1047,8 +1051,10 @@ def check_decorated_function_is_method(self, decorator: str,
10471051

10481052
def visit_class_def(self, defn: ClassDef) -> None:
10491053
self.statement = defn
1054+
self.incomplete_type_stack.append(not defn.info)
10501055
with self.tvar_scope_frame(self.tvar_scope.class_frame()):
10511056
self.analyze_class(defn)
1057+
self.incomplete_type_stack.pop()
10521058

10531059
def analyze_class(self, defn: ClassDef) -> None:
10541060
fullname = self.qualified_name(defn.name)
@@ -4749,7 +4755,15 @@ def check_no_global(self,
47494755
self.name_already_defined(name, ctx, self.globals[name])
47504756

47514757
def name_not_defined(self, name: str, ctx: Context, namespace: Optional[str] = None) -> None:
4752-
if self.is_incomplete_namespace(namespace or self.cur_mod_id):
4758+
incomplete = self.is_incomplete_namespace(namespace or self.cur_mod_id)
4759+
if (namespace is None
4760+
and self.type
4761+
and not self.is_func_scope()
4762+
and self.incomplete_type_stack[-1]
4763+
and not self.final_iteration):
4764+
# We are processing a class body for the first time, so it is incomplete.
4765+
incomplete = True
4766+
if incomplete:
47534767
# Target namespace is incomplete, so it's possible that the name will be defined
47544768
# later on. Defer current target.
47554769
self.record_incomplete_ref()

mypy/semanal_main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ def semantic_analyze_target(target: str,
306306
307307
Return tuple with these items:
308308
- list of deferred targets
309-
- was some definition incomplete
309+
- was some definition incomplete (need to run another pass)
310310
- were any new names were defined (or placeholders replaced)
311311
"""
312312
state.manager.processed_targets.append(target)

test-data/unit/check-classes.test

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3799,7 +3799,7 @@ int.__eq__(int)
37993799
int.__eq__(3, 4)
38003800
[builtins fixtures/args.pyi]
38013801
[out]
3802-
main:33: error: Too few arguments for "__eq__" of "int"
3802+
main:33: error: Too few arguments for "__eq__" of "int"
38033803
main:33: error: Unsupported operand types for == ("int" and "Type[int]")
38043804

38053805
[case testMroSetAfterError]
@@ -6817,3 +6817,7 @@ class A(metaclass=ABCMeta):
68176817
@final
68186818
class B(A): # E: Final class __main__.B has abstract attributes "foo"
68196819
pass
6820+
6821+
[case testUndefinedBaseclassInNestedClass]
6822+
class C:
6823+
class C1(XX): pass # E: Name "XX" is not defined

test-data/unit/check-modules.test

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2943,3 +2943,42 @@ def f(x: str) -> None: ...
29432943
[file mypy.ini]
29442944
\[mypy]
29452945
mypy_path = tmp/xx
2946+
2947+
[case testImportCycleSpecialCase2]
2948+
import m
2949+
2950+
[file m.pyi]
2951+
from f import F
2952+
class M: pass
2953+
2954+
[file f.pyi]
2955+
from m import M
2956+
2957+
from typing import Generic, TypeVar
2958+
2959+
T = TypeVar("T")
2960+
2961+
class W(Generic[T]): ...
2962+
2963+
class F(M):
2964+
A = W[int]
2965+
x: C
2966+
class C(W[F.A]): ...
2967+
2968+
[case testImportCycleSpecialCase3]
2969+
import f
2970+
2971+
[file m.pyi]
2972+
from f import F
2973+
class M: pass
2974+
2975+
[file f.pyi]
2976+
from m import M
2977+
2978+
from typing import Generic, TypeVar
2979+
2980+
T = TypeVar("T")
2981+
2982+
class F(M):
2983+
x: C
2984+
class C: ...

0 commit comments

Comments
 (0)