From 9150e8b9ff0789864e6afd4fc2693222efdbc7e4 Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Sat, 29 Jul 2023 15:10:21 +0300
Subject: [PATCH 1/6] Allow nested classes in `NamedTuple` bodies

---
 mypy/semanal_namedtuple.py                 | 10 +++++--
 test-data/unit/check-class-namedtuple.test | 34 ++++++++++++++++++++++
 2 files changed, 41 insertions(+), 3 deletions(-)

diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py
index 51ea90e07f3d..9bb4863a406e 100644
--- a/mypy/semanal_namedtuple.py
+++ b/mypy/semanal_namedtuple.py
@@ -149,7 +149,7 @@ def check_namedtuple_classdef(
         default_items: dict[str, Expression] = {}
         statements: list[Statement] = []
         for stmt in defn.defs.body:
-            statements.append(stmt)
+            # Processing fields of a namedtuple:
             if not isinstance(stmt, AssignmentStmt):
                 # Still allow pass or ... (for empty namedtuples).
                 if isinstance(stmt, PassStmt) or (
@@ -162,16 +162,20 @@ def check_namedtuple_classdef(
                 # And docstrings.
                 if isinstance(stmt, ExpressionStmt) and isinstance(stmt.expr, StrExpr):
                     continue
-                statements.pop()
+                # And nested classes, they need to be analyzed further:
+                if isinstance(stmt, ClassDef):
+                    statements.append(stmt)
+                    continue
+
                 defn.removed_statements.append(stmt)
                 self.fail(NAMEDTUP_CLASS_ERROR, stmt)
             elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
                 # An assignment, but an invalid one.
-                statements.pop()
                 defn.removed_statements.append(stmt)
                 self.fail(NAMEDTUP_CLASS_ERROR, stmt)
             else:
                 # Append name and type in this case...
+                statements.append(stmt)
                 name = stmt.lvalues[0].name
                 items.append(name)
                 if stmt.type is None:
diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test
index a095f212b900..b914bb3be515 100644
--- a/test-data/unit/check-class-namedtuple.test
+++ b/test-data/unit/check-class-namedtuple.test
@@ -368,6 +368,40 @@ class X(NamedTuple):
     y = 2  # E: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
 [builtins fixtures/tuple.pyi]
 
+[case testNewNamedTupleWithNestedClass]
+from typing import NamedTuple
+
+class A(NamedTuple):
+    x: int
+    class B:
+        x: str
+
+a: A
+reveal_type(A.B)  # N: Revealed type is "def () -> __main__.A.B"
+b: A.B
+reveal_type(b.x)  # N: Revealed type is "builtins.str"
+[builtins fixtures/tuple.pyi]
+
+[case testNewNamedTupleWithNestedNamedTuple]
+from typing import NamedTuple
+
+class A(NamedTuple):
+    x: int
+    class B(NamedTuple):
+        x: str
+        y: int = 1
+
+# Correct:
+A(1)
+A.B('a')
+A.B('a', 2)
+
+# Incorrect:
+A.B()  # E: Missing positional argument "x" in call to "B"
+A.B(1, 'a')  # E: Argument 1 to "B" has incompatible type "int"; expected "str" \
+             # E: Argument 2 to "B" has incompatible type "str"; expected "int"
+[builtins fixtures/tuple.pyi]
+
 [case testTypeUsingTypeCNamedTuple]
 from typing import NamedTuple, Type
 

From 12e94dbfd15904e35be4451bf1e136ec1d8296bd Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Sat, 29 Jul 2023 15:15:05 +0300
Subject: [PATCH 2/6] Adjust `stubgen` test

---
 test-data/unit/stubgen.test | 31 +++++++++++++++++--------------
 1 file changed, 17 insertions(+), 14 deletions(-)

diff --git a/test-data/unit/stubgen.test b/test-data/unit/stubgen.test
index f6b71a994153..7cc3d5b191a8 100644
--- a/test-data/unit/stubgen.test
+++ b/test-data/unit/stubgen.test
@@ -717,6 +717,16 @@ class RegularClass:
         x: int
         y: str = 'a'
     z: str = 'b'
+
+class WithNestedNT(NamedTuple):
+    x: int
+    y: str = 'a'
+    class Regular:
+        x: int
+        y: str = 'a'
+    class NestedNamedTuple(NamedTuple):
+        x: int
+        y: str = 'a'
 [out]
 from typing import NamedTuple
 
@@ -737,22 +747,15 @@ class RegularClass:
         y: str = ...
     z: str
 
-
-[case testNestedClassInNamedTuple_semanal-xfail]
-from typing import NamedTuple
-
-# TODO: make sure that nested classes in `NamedTuple` are supported:
-class NamedTupleWithNestedClass(NamedTuple):
-    class Nested:
-        x: int
-        y: str = 'a'
-[out]
-from typing import NamedTuple
-
-class NamedTupleWithNestedClass(NamedTuple):
-    class Nested:
+class WithNestedNT(NamedTuple):
+    x: int
+    y: str = ...
+    class Regular:
         x: int
         y: str
+    class NestedNamedTuple(NamedTuple):
+        x: int
+        y: str = ...
 
 [case testEmptyNamedtuple]
 import collections, typing

From f1441cff5fef90b0c72fad11e12750ec917f8394 Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Sat, 29 Jul 2023 15:16:02 +0300
Subject: [PATCH 3/6] Remove comment

---
 mypy/semanal_namedtuple.py | 1 -
 1 file changed, 1 deletion(-)

diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py
index 9bb4863a406e..cc8f9874bf59 100644
--- a/mypy/semanal_namedtuple.py
+++ b/mypy/semanal_namedtuple.py
@@ -149,7 +149,6 @@ def check_namedtuple_classdef(
         default_items: dict[str, Expression] = {}
         statements: list[Statement] = []
         for stmt in defn.defs.body:
-            # Processing fields of a namedtuple:
             if not isinstance(stmt, AssignmentStmt):
                 # Still allow pass or ... (for empty namedtuples).
                 if isinstance(stmt, PassStmt) or (

From ddb2e52776abeba7a2003ff1897e1e5f3236d43b Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Sat, 29 Jul 2023 15:44:43 +0300
Subject: [PATCH 4/6] Fix CI

---
 mypy/semanal_namedtuple.py                 | 6 +++---
 test-data/unit/check-class-namedtuple.test | 6 ++++++
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/mypy/semanal_namedtuple.py b/mypy/semanal_namedtuple.py
index cc8f9874bf59..111da9643586 100644
--- a/mypy/semanal_namedtuple.py
+++ b/mypy/semanal_namedtuple.py
@@ -149,6 +149,7 @@ def check_namedtuple_classdef(
         default_items: dict[str, Expression] = {}
         statements: list[Statement] = []
         for stmt in defn.defs.body:
+            statements.append(stmt)
             if not isinstance(stmt, AssignmentStmt):
                 # Still allow pass or ... (for empty namedtuples).
                 if isinstance(stmt, PassStmt) or (
@@ -163,18 +164,17 @@ def check_namedtuple_classdef(
                     continue
                 # And nested classes, they need to be analyzed further:
                 if isinstance(stmt, ClassDef):
-                    statements.append(stmt)
                     continue
-
+                statements.pop()
                 defn.removed_statements.append(stmt)
                 self.fail(NAMEDTUP_CLASS_ERROR, stmt)
             elif len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
                 # An assignment, but an invalid one.
+                statements.pop()
                 defn.removed_statements.append(stmt)
                 self.fail(NAMEDTUP_CLASS_ERROR, stmt)
             else:
                 # Append name and type in this case...
-                statements.append(stmt)
                 name = stmt.lvalues[0].name
                 items.append(name)
                 if stmt.type is None:
diff --git a/test-data/unit/check-class-namedtuple.test b/test-data/unit/check-class-namedtuple.test
index b914bb3be515..a4e391f13729 100644
--- a/test-data/unit/check-class-namedtuple.test
+++ b/test-data/unit/check-class-namedtuple.test
@@ -390,12 +390,18 @@ class A(NamedTuple):
     class B(NamedTuple):
         x: str
         y: int = 1
+        def method(self) -> int: ...
 
 # Correct:
 A(1)
 A.B('a')
 A.B('a', 2)
 
+b: A.B
+reveal_type(b.x)  # N: Revealed type is "builtins.str"
+reveal_type(b.y)  # N: Revealed type is "builtins.int"
+reveal_type(b.method())  # N: Revealed type is "builtins.int"
+
 # Incorrect:
 A.B()  # E: Missing positional argument "x" in call to "B"
 A.B(1, 'a')  # E: Argument 1 to "B" has incompatible type "int"; expected "str" \

From 2937378b93417b1f03ac63aa2310d9b636d541fc Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Sat, 29 Jul 2023 17:22:57 +0300
Subject: [PATCH 5/6] Fix CI

---
 mypy/server/astmerge.py          |  2 ++
 test-data/unit/fine-grained.test | 10 ++++++----
 2 files changed, 8 insertions(+), 4 deletions(-)

diff --git a/mypy/server/astmerge.py b/mypy/server/astmerge.py
index 5e3759227c7b..6085308831ac 100644
--- a/mypy/server/astmerge.py
+++ b/mypy/server/astmerge.py
@@ -400,6 +400,8 @@ def process_synthetic_type_info(self, info: TypeInfo) -> None:
         # tables separately, unlike normal classes.
         self.process_type_info(info)
         for name, node in info.names.items():
+            if isinstance(node.node, TypeInfo):
+                continue  # There might be nested classes in some cases, skip them
             if node.node:
                 node.node.accept(self)
 
diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
index 66c5ee46db2f..62b035b1cd92 100644
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -10192,23 +10192,25 @@ import m
 from typing import NamedTuple
 
 class NT(NamedTuple):
-    class C: ...
+    class C: ...  # classes are fine
     x: int
     y: int
+    assert True  # assert statements are not allowed
 
 [file m.py.2]
 from typing import NamedTuple
 
 class NT(NamedTuple):
-    class C: ...
+    class C: ...  # classes are fine
     x: int
     y: int
+    assert True  # assert statements are not allowed
 # change
 [builtins fixtures/tuple.pyi]
 [out]
-m.py:4: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
+m.py:7: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
 ==
-m.py:4: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
+m.py:7: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
 
 [case testNamedTupleNestedClassRecheck]
 import n

From d19907cc69cdc4f1b3f13f7249da94676cba8b71 Mon Sep 17 00:00:00 2001
From: sobolevn <mail@sobolevn.me>
Date: Sat, 29 Jul 2023 17:46:38 +0300
Subject: [PATCH 6/6] Fix CI

---
 test-data/unit/fine-grained.test | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/test-data/unit/fine-grained.test b/test-data/unit/fine-grained.test
index 62b035b1cd92..e28f1be902d3 100644
--- a/test-data/unit/fine-grained.test
+++ b/test-data/unit/fine-grained.test
@@ -10225,6 +10225,7 @@ class NT(NamedTuple):
     class C: ...
     x: int
     y: A
+    assert True
 
 [file f.py]
 A = int
@@ -10232,9 +10233,9 @@ A = int
 A = str
 [builtins fixtures/tuple.pyi]
 [out]
-m.py:5: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
+m.py:8: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
 ==
-m.py:5: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
+m.py:8: error: Invalid statement in NamedTuple definition; expected "field_name: field_type [= default]"
 
 [case testTypedDictNestedClassRecheck]
 import n