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