Skip to content

Commit 256cf68

Browse files
authored
Only defer top-level functions (#18718)
This makes deferral logic more robust and more consistent with fine-grained mode. I also: * Change some terminology, as "top function" is ambiguous: top-level function vs top of stack function. * Update some docs and type annotations to match actual behavior (e.g. we do not defer lambdas) See also #18674 for some more motivation.
1 parent 2d3df02 commit 256cf68

File tree

4 files changed

+61
-24
lines changed

4 files changed

+61
-24
lines changed

mypy/checker.py

+25-20
Original file line numberDiff line numberDiff line change
@@ -221,17 +221,17 @@
221221
# Maximum length of fixed tuple types inferred when narrowing from variadic tuples.
222222
MAX_PRECISE_TUPLE_SIZE: Final = 8
223223

224-
DeferredNodeType: _TypeAlias = Union[FuncDef, LambdaExpr, OverloadedFuncDef, Decorator]
224+
DeferredNodeType: _TypeAlias = Union[FuncDef, OverloadedFuncDef, Decorator]
225225
FineGrainedDeferredNodeType: _TypeAlias = Union[FuncDef, MypyFile, OverloadedFuncDef]
226226

227227

228228
# A node which is postponed to be processed during the next pass.
229229
# In normal mode one can defer functions and methods (also decorated and/or overloaded)
230-
# and lambda expressions. Nested functions can't be deferred -- only top-level functions
230+
# but not lambda expressions. Nested functions can't be deferred -- only top-level functions
231231
# and methods of classes not defined within a function can be deferred.
232232
class DeferredNode(NamedTuple):
233233
node: DeferredNodeType
234-
# And its TypeInfo (for semantic analysis self type handling
234+
# And its TypeInfo (for semantic analysis self type handling)
235235
active_typeinfo: TypeInfo | None
236236

237237

@@ -528,10 +528,7 @@ def check_partial(self, node: DeferredNodeType | FineGrainedDeferredNodeType) ->
528528
else:
529529
self.recurse_into_functions = True
530530
with self.binder.top_frame_context():
531-
if isinstance(node, LambdaExpr):
532-
self.expr_checker.accept(node)
533-
else:
534-
self.accept(node)
531+
self.accept(node)
535532

536533
def check_top_level(self, node: MypyFile) -> None:
537534
"""Check only the top-level of a module, skipping function definitions."""
@@ -558,13 +555,13 @@ def defer_node(self, node: DeferredNodeType, enclosing_class: TypeInfo | None) -
558555
self.deferred_nodes.append(DeferredNode(node, enclosing_class))
559556

560557
def handle_cannot_determine_type(self, name: str, context: Context) -> None:
561-
node = self.scope.top_non_lambda_function()
558+
node = self.scope.top_level_function()
562559
if self.pass_num < self.last_pass and isinstance(node, FuncDef):
563560
# Don't report an error yet. Just defer. Note that we don't defer
564561
# lambdas because they are coupled to the surrounding function
565562
# through the binder and the inferred type of the lambda, so it
566563
# would get messy.
567-
enclosing_class = self.scope.enclosing_class()
564+
enclosing_class = self.scope.enclosing_class(node)
568565
self.defer_node(node, enclosing_class)
569566
# Set a marker so that we won't infer additional types in this
570567
# function. Any inferred types could be bogus, because there's at
@@ -2156,7 +2153,14 @@ def check_method_override_for_base_with_name(
21562153
if self.pass_num < self.last_pass:
21572154
# If there are passes left, defer this node until next pass,
21582155
# otherwise try reconstructing the method type from available information.
2159-
self.defer_node(defn, defn.info)
2156+
# For consistency, defer an enclosing top-level function (if any).
2157+
top_level = self.scope.top_level_function()
2158+
if isinstance(top_level, FuncDef):
2159+
self.defer_node(top_level, self.scope.enclosing_class(top_level))
2160+
else:
2161+
# Specify enclosing class explicitly, as we check type override before
2162+
# entering e.g. decorators or overloads.
2163+
self.defer_node(defn, defn.info)
21602164
return True
21612165
elif isinstance(original_node, (FuncDef, OverloadedFuncDef)):
21622166
original_type = self.function_type(original_node)
@@ -4767,7 +4771,7 @@ def visit_return_stmt(self, s: ReturnStmt) -> None:
47674771
self.binder.unreachable()
47684772

47694773
def check_return_stmt(self, s: ReturnStmt) -> None:
4770-
defn = self.scope.top_function()
4774+
defn = self.scope.current_function()
47714775
if defn is not None:
47724776
if defn.is_generator:
47734777
return_type = self.get_generator_return_type(
@@ -4779,7 +4783,7 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
47794783
return_type = self.return_types[-1]
47804784
return_type = get_proper_type(return_type)
47814785

4782-
is_lambda = isinstance(self.scope.top_function(), LambdaExpr)
4786+
is_lambda = isinstance(defn, LambdaExpr)
47834787
if isinstance(return_type, UninhabitedType):
47844788
# Avoid extra error messages for failed inference in lambdas
47854789
if not is_lambda and not return_type.ambiguous:
@@ -8554,14 +8558,15 @@ class CheckerScope:
85548558
def __init__(self, module: MypyFile) -> None:
85558559
self.stack = [module]
85568560

8557-
def top_function(self) -> FuncItem | None:
8561+
def current_function(self) -> FuncItem | None:
85588562
for e in reversed(self.stack):
85598563
if isinstance(e, FuncItem):
85608564
return e
85618565
return None
85628566

8563-
def top_non_lambda_function(self) -> FuncItem | None:
8564-
for e in reversed(self.stack):
8567+
def top_level_function(self) -> FuncItem | None:
8568+
"""Return top-level non-lambda function."""
8569+
for e in self.stack:
85658570
if isinstance(e, FuncItem) and not isinstance(e, LambdaExpr):
85668571
return e
85678572
return None
@@ -8571,11 +8576,11 @@ def active_class(self) -> TypeInfo | None:
85718576
return self.stack[-1]
85728577
return None
85738578

8574-
def enclosing_class(self) -> TypeInfo | None:
8579+
def enclosing_class(self, func: FuncItem | None = None) -> TypeInfo | None:
85758580
"""Is there a class *directly* enclosing this function?"""
8576-
top = self.top_function()
8577-
assert top, "This method must be called from inside a function"
8578-
index = self.stack.index(top)
8581+
func = func or self.current_function()
8582+
assert func, "This method must be called from inside a function"
8583+
index = self.stack.index(func)
85798584
assert index, "CheckerScope stack must always start with a module"
85808585
enclosing = self.stack[index - 1]
85818586
if isinstance(enclosing, TypeInfo):
@@ -8589,7 +8594,7 @@ def active_self_type(self) -> Instance | TupleType | None:
85898594
In particular, inside a function nested in method this returns None.
85908595
"""
85918596
info = self.active_class()
8592-
if not info and self.top_function():
8597+
if not info and self.current_function():
85938598
info = self.enclosing_class()
85948599
if info:
85958600
return fill_typevars(info)

mypy/checkexpr.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -5523,7 +5523,7 @@ def visit_super_expr(self, e: SuperExpr) -> Type:
55235523
if type_info in mro:
55245524
index = mro.index(type_info)
55255525
else:
5526-
method = self.chk.scope.top_function()
5526+
method = self.chk.scope.current_function()
55275527
# Mypy explicitly allows supertype upper bounds (and no upper bound at all)
55285528
# for annotating self-types. However, if such an annotation is used for
55295529
# checking super() we will still get an error. So to be consistent, we also
@@ -5598,7 +5598,7 @@ def _super_arg_types(self, e: SuperExpr) -> Type | tuple[Type, Type]:
55985598
type_type: ProperType = TypeType(current_type)
55995599

56005600
# Use the type of the self argument, in case it was annotated
5601-
method = self.chk.scope.top_function()
5601+
method = self.chk.scope.current_function()
56025602
assert method is not None
56035603
if method.arguments:
56045604
instance_type: Type = method.arguments[0].variable.type or current_type

mypy/semanal.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -3708,9 +3708,9 @@ def store_final_status(self, s: AssignmentStmt) -> None:
37083708
cur_node = self.type.names.get(lval.name, None)
37093709
if cur_node and isinstance(cur_node.node, Var) and cur_node.node.is_final:
37103710
assert self.function_stack
3711-
top_function = self.function_stack[-1]
3711+
current_function = self.function_stack[-1]
37123712
if (
3713-
top_function.name == "__init__"
3713+
current_function.name == "__init__"
37143714
and cur_node.node.final_unset_in_class
37153715
and not cur_node.node.final_set_in_init
37163716
and not (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs)

test-data/unit/check-inference.test

+32
Original file line numberDiff line numberDiff line change
@@ -3913,3 +3913,35 @@ x = "abc"
39133913
for x in list[int]():
39143914
reveal_type(x) # N: Revealed type is "builtins.int"
39153915
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"
3916+
3917+
[case testNarrowInFunctionDefer]
3918+
from typing import Optional, Callable, TypeVar
3919+
3920+
def top() -> None:
3921+
x: Optional[int]
3922+
assert x is not None
3923+
3924+
def foo() -> None:
3925+
defer()
3926+
reveal_type(x) # N: Revealed type is "builtins.int"
3927+
3928+
T = TypeVar("T")
3929+
def deco(fn: Callable[[], T]) -> Callable[[], T]: ...
3930+
3931+
@deco
3932+
def defer() -> int: ...
3933+
3934+
[case testDeferMethodOfNestedClass]
3935+
from typing import Optional, Callable, TypeVar
3936+
3937+
class Out:
3938+
def meth(self) -> None:
3939+
class In:
3940+
def meth(self) -> None:
3941+
reveal_type(defer()) # N: Revealed type is "builtins.int"
3942+
3943+
T = TypeVar("T")
3944+
def deco(fn: Callable[[], T]) -> Callable[[], T]: ...
3945+
3946+
@deco
3947+
def defer() -> int: ...

0 commit comments

Comments
 (0)