Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Only defer top-level functions #18718

Merged
merged 3 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 25 additions & 20 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,17 +221,17 @@
# Maximum length of fixed tuple types inferred when narrowing from variadic tuples.
MAX_PRECISE_TUPLE_SIZE: Final = 8

DeferredNodeType: _TypeAlias = Union[FuncDef, LambdaExpr, OverloadedFuncDef, Decorator]
DeferredNodeType: _TypeAlias = Union[FuncDef, OverloadedFuncDef, Decorator]
FineGrainedDeferredNodeType: _TypeAlias = Union[FuncDef, MypyFile, OverloadedFuncDef]


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


Expand Down Expand Up @@ -528,10 +528,7 @@ def check_partial(self, node: DeferredNodeType | FineGrainedDeferredNodeType) ->
else:
self.recurse_into_functions = True
with self.binder.top_frame_context():
if isinstance(node, LambdaExpr):
self.expr_checker.accept(node)
else:
self.accept(node)
self.accept(node)

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

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

def check_return_stmt(self, s: ReturnStmt) -> None:
defn = self.scope.top_function()
defn = self.scope.current_function()
if defn is not None:
if defn.is_generator:
return_type = self.get_generator_return_type(
Expand All @@ -4779,7 +4783,7 @@ def check_return_stmt(self, s: ReturnStmt) -> None:
return_type = self.return_types[-1]
return_type = get_proper_type(return_type)

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

def top_function(self) -> FuncItem | None:
def current_function(self) -> FuncItem | None:
for e in reversed(self.stack):
if isinstance(e, FuncItem):
return e
return None

def top_non_lambda_function(self) -> FuncItem | None:
for e in reversed(self.stack):
def top_level_function(self) -> FuncItem | None:
"""Return top-level non-lambda function."""
for e in self.stack:
if isinstance(e, FuncItem) and not isinstance(e, LambdaExpr):
return e
return None
Expand All @@ -8571,11 +8576,11 @@ def active_class(self) -> TypeInfo | None:
return self.stack[-1]
return None

def enclosing_class(self) -> TypeInfo | None:
def enclosing_class(self, func: FuncItem | None = None) -> TypeInfo | None:
"""Is there a class *directly* enclosing this function?"""
top = self.top_function()
assert top, "This method must be called from inside a function"
index = self.stack.index(top)
func = func or self.current_function()
assert func, "This method must be called from inside a function"
index = self.stack.index(func)
assert index, "CheckerScope stack must always start with a module"
enclosing = self.stack[index - 1]
if isinstance(enclosing, TypeInfo):
Expand All @@ -8589,7 +8594,7 @@ def active_self_type(self) -> Instance | TupleType | None:
In particular, inside a function nested in method this returns None.
"""
info = self.active_class()
if not info and self.top_function():
if not info and self.current_function():
info = self.enclosing_class()
if info:
return fill_typevars(info)
Expand Down
4 changes: 2 additions & 2 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5523,7 +5523,7 @@ def visit_super_expr(self, e: SuperExpr) -> Type:
if type_info in mro:
index = mro.index(type_info)
else:
method = self.chk.scope.top_function()
method = self.chk.scope.current_function()
# Mypy explicitly allows supertype upper bounds (and no upper bound at all)
# for annotating self-types. However, if such an annotation is used for
# checking super() we will still get an error. So to be consistent, we also
Expand Down Expand Up @@ -5598,7 +5598,7 @@ def _super_arg_types(self, e: SuperExpr) -> Type | tuple[Type, Type]:
type_type: ProperType = TypeType(current_type)

# Use the type of the self argument, in case it was annotated
method = self.chk.scope.top_function()
method = self.chk.scope.current_function()
assert method is not None
if method.arguments:
instance_type: Type = method.arguments[0].variable.type or current_type
Expand Down
4 changes: 2 additions & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3708,9 +3708,9 @@ def store_final_status(self, s: AssignmentStmt) -> None:
cur_node = self.type.names.get(lval.name, None)
if cur_node and isinstance(cur_node.node, Var) and cur_node.node.is_final:
assert self.function_stack
top_function = self.function_stack[-1]
current_function = self.function_stack[-1]
if (
top_function.name == "__init__"
current_function.name == "__init__"
and cur_node.node.final_unset_in_class
and not cur_node.node.final_set_in_init
and not (isinstance(s.rvalue, TempNode) and s.rvalue.no_rhs)
Expand Down
32 changes: 32 additions & 0 deletions test-data/unit/check-inference.test
Original file line number Diff line number Diff line change
Expand Up @@ -3913,3 +3913,35 @@ x = "abc"
for x in list[int]():
reveal_type(x) # N: Revealed type is "builtins.int"
reveal_type(x) # N: Revealed type is "Union[builtins.int, builtins.str]"

[case testNarrowInFunctionDefer]
from typing import Optional, Callable, TypeVar

def top() -> None:
x: Optional[int]
assert x is not None

def foo() -> None:
defer()
reveal_type(x) # N: Revealed type is "builtins.int"

T = TypeVar("T")
def deco(fn: Callable[[], T]) -> Callable[[], T]: ...

@deco
def defer() -> int: ...

[case testDeferMethodOfNestedClass]
from typing import Optional, Callable, TypeVar

class Out:
def meth(self) -> None:
class In:
def meth(self) -> None:
reveal_type(defer()) # N: Revealed type is "builtins.int"

T = TypeVar("T")
def deco(fn: Callable[[], T]) -> Callable[[], T]: ...

@deco
def defer() -> int: ...