diff --git a/mypy/scope.py b/mypy/scope.py index 766048c41180..3f0bc7d80367 100644 --- a/mypy/scope.py +++ b/mypy/scope.py @@ -124,3 +124,24 @@ def saved_scope(self, saved: SavedScope) -> Iterator[None]: with self.class_scope(info) if info else nullcontext(): with self.function_scope(function) if function else nullcontext(): yield + + @contextmanager + def without_function_locals(self): + """Temporarily disable the current function scope (used for nested class definitions).""" + # Save current function + saved_function = self.function + saved_ignored = self.ignored + saved_functions_stack = list(self.functions) + + # Temporarily remove the current function scope + self.function = None + self.functions = [] + self.ignored = 0 + + try: + yield + finally: + # Restore the previous function context + self.function = saved_function + self.functions = saved_functions_stack + self.ignored = saved_ignored diff --git a/mypy/semanal.py b/mypy/semanal.py index 17dc9bfadc1f..14283c530960 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1763,7 +1763,6 @@ def check_decorated_function_is_method(self, decorator: str, context: Context) - # # Classes # - def visit_class_def(self, defn: ClassDef) -> None: self.statement = defn self.incomplete_type_stack.append(not defn.info) @@ -1773,10 +1772,31 @@ def visit_class_def(self, defn: ClassDef) -> None: self.mark_incomplete(defn.name, defn) return - self.analyze_class(defn) + # --- PATCH START --- + # If the class is defined inside a function, skip that function's locals + if self.scope.active_function() is not None: + with self.scope.without_function_locals(): + self.analyze_class(defn) + else: + self.analyze_class(defn) + # --- PATCH END --- + self.pop_type_args(defn.type_args) self.incomplete_type_stack.pop() + # def visit_class_def(self, defn: ClassDef) -> None: + # self.statement = defn + # self.incomplete_type_stack.append(not defn.info) + # namespace = self.qualified_name(defn.name) + # with self.tvar_scope_frame(self.tvar_scope.class_frame(namespace)): + # if self.push_type_args(defn.type_args, defn) is None: + # self.mark_incomplete(defn.name, defn) + # return + + # self.analyze_class(defn) + # self.pop_type_args(defn.type_args) + # self.incomplete_type_stack.pop() + def push_type_args( self, type_args: list[TypeParam] | None, context: Context ) -> list[tuple[str, TypeVarLikeExpr]] | None: diff --git a/test-data/unit/issue_scope.test b/test-data/unit/issue_scope.test new file mode 100644 index 000000000000..2ea013dd4e62 --- /dev/null +++ b/test-data/unit/issue_scope.test @@ -0,0 +1,17 @@ +[case testScopeOptionalIntResolution] +from typing import Optional + +x: Optional[int] = None +y: Optional[int] = None + +def f() -> None: + x = 1 + y = 1 + class C: + reveal_type(x) # should be int + reveal_type(y) # should be Optional[int] + x = 2 + +[out] +note: Revealed type is "builtins.int" +note: Revealed type is "Union[builtins.int, None]" diff --git a/test-data/unit/nested-class-scope.test b/test-data/unit/nested-class-scope.test new file mode 100644 index 000000000000..2072e5597949 --- /dev/null +++ b/test-data/unit/nested-class-scope.test @@ -0,0 +1,15 @@ +[case testClassScopeInFunction] +from typing import Optional + +x: Optional[int] = None +y: Optional[int] = None + +def f() -> None: + x = 1 + y = 1 + class C: + reveal_type(x) + reveal_type(y) +[out] +note: Revealed type is "builtins.int" +note: Revealed type is "Union[builtins.int, None]"