Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9165bb1

Browse files
authored
Require first argument of namedtuple to match with variable name (#9577)
Closes #4589 This PR modifies check_namedtuple to return the internal name of the namedtuples (e.g. the content of the first argument of namedtuple/NamedTuple) so that the callers, especially analyze_namedtuple_assign, can check if the name of the variable on the l.h.s. matches with the first argument of the namedtuple.
1 parent 5db3e1a commit 9165bb1

6 files changed

Lines changed: 75 additions & 74 deletions

File tree

mypy/plugin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ def final_iteration(self) -> bool:
359359
# A context for querying for configuration data about a module for
360360
# cache invalidation purposes.
361361
ReportConfigContext = NamedTuple(
362-
'DynamicClassDefContext', [
362+
'ReportConfigContext', [
363363
('id', str), # Module name
364364
('path', str), # Module file path
365365
('is_check', bool) # Is this invocation for checking whether the config matches

mypy/semanal.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2177,13 +2177,17 @@ def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
21772177
return False
21782178
lvalue = s.lvalues[0]
21792179
name = lvalue.name
2180-
is_named_tuple, info = self.named_tuple_analyzer.check_namedtuple(s.rvalue, name,
2181-
self.is_func_scope())
2182-
if not is_named_tuple:
2180+
internal_name, info = self.named_tuple_analyzer.check_namedtuple(s.rvalue, name,
2181+
self.is_func_scope())
2182+
if internal_name is None:
21832183
return False
21842184
if isinstance(lvalue, MemberExpr):
21852185
self.fail("NamedTuple type as an attribute is not supported", lvalue)
21862186
return False
2187+
if internal_name != name:
2188+
self.fail("First argument to namedtuple() should be '{}', not '{}'".format(
2189+
name, internal_name), s.rvalue)
2190+
return True
21872191
# Yes, it's a valid namedtuple, but defer if it is not ready.
21882192
if not info:
21892193
self.mark_incomplete(name, lvalue, becomes_typeinfo=True)
@@ -4819,9 +4823,9 @@ def expr_to_analyzed_type(self,
48194823
allow_placeholder: bool = False) -> Optional[Type]:
48204824
if isinstance(expr, CallExpr):
48214825
expr.accept(self)
4822-
is_named_tuple, info = self.named_tuple_analyzer.check_namedtuple(expr, None,
4823-
self.is_func_scope())
4824-
if not is_named_tuple:
4826+
internal_name, info = self.named_tuple_analyzer.check_namedtuple(expr, None,
4827+
self.is_func_scope())
4828+
if internal_name is None:
48254829
# Some form of namedtuple is the only valid type that looks like a call
48264830
# expression. This isn't a valid type.
48274831
raise TypeTranslationError()

mypy/semanal_namedtuple.py

Lines changed: 49 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -138,47 +138,48 @@ def check_namedtuple_classdef(self, defn: ClassDef, is_stub_file: bool
138138
def check_namedtuple(self,
139139
node: Expression,
140140
var_name: Optional[str],
141-
is_func_scope: bool) -> Tuple[bool, Optional[TypeInfo]]:
141+
is_func_scope: bool) -> Tuple[Optional[str], Optional[TypeInfo]]:
142142
"""Check if a call defines a namedtuple.
143143
144144
The optional var_name argument is the name of the variable to
145145
which this is assigned, if any.
146146
147147
Return a tuple of two items:
148-
* Can it be a valid named tuple?
148+
* Internal name of the named tuple (e.g. the name passed as an argument to namedtuple)
149+
or None if it is not a valid named tuple
149150
* Corresponding TypeInfo, or None if not ready.
150151
151152
If the definition is invalid but looks like a namedtuple,
152153
report errors but return (some) TypeInfo.
153154
"""
154155
if not isinstance(node, CallExpr):
155-
return False, None
156+
return None, None
156157
call = node
157158
callee = call.callee
158159
if not isinstance(callee, RefExpr):
159-
return False, None
160+
return None, None
160161
fullname = callee.fullname
161162
if fullname == 'collections.namedtuple':
162163
is_typed = False
163164
elif fullname == 'typing.NamedTuple':
164165
is_typed = True
165166
else:
166-
return False, None
167+
return None, None
167168
result = self.parse_namedtuple_args(call, fullname)
168169
if result:
169-
items, types, defaults, ok = result
170+
items, types, defaults, typename, ok = result
170171
else:
171-
# This is a valid named tuple but some types are not ready.
172-
return True, None
173-
if not ok:
174172
# Error. Construct dummy return value.
175173
if var_name:
176174
name = var_name
177175
else:
178176
name = 'namedtuple@' + str(call.line)
179177
info = self.build_namedtuple_typeinfo(name, [], [], {}, node.line)
180178
self.store_namedtuple_info(info, name, call, is_typed)
181-
return True, info
179+
return name, info
180+
if not ok:
181+
# This is a valid named tuple but some types are not ready.
182+
return typename, None
182183

183184
# We use the variable name as the class name if it exists. If
184185
# it doesn't, we use the name passed as an argument. We prefer
@@ -188,7 +189,7 @@ def check_namedtuple(self,
188189
if var_name:
189190
name = var_name
190191
else:
191-
name = cast(Union[StrExpr, BytesExpr, UnicodeExpr], call.args[0]).value
192+
name = typename
192193

193194
if var_name is None or is_func_scope:
194195
# There are two special cases where need to give it a unique name derived
@@ -228,7 +229,7 @@ def check_namedtuple(self,
228229
if name != var_name or is_func_scope:
229230
# NOTE: we skip local namespaces since they are not serialized.
230231
self.api.add_symbol_skip_local(name, info)
231-
return True, info
232+
return typename, info
232233

233234
def store_namedtuple_info(self, info: TypeInfo, name: str,
234235
call: CallExpr, is_typed: bool) -> None:
@@ -237,26 +238,30 @@ def store_namedtuple_info(self, info: TypeInfo, name: str,
237238
call.analyzed.set_line(call.line, call.column)
238239

239240
def parse_namedtuple_args(self, call: CallExpr, fullname: str
240-
) -> Optional[Tuple[List[str], List[Type], List[Expression], bool]]:
241+
) -> Optional[Tuple[List[str], List[Type], List[Expression],
242+
str, bool]]:
241243
"""Parse a namedtuple() call into data needed to construct a type.
242244
243-
Returns a 4-tuple:
245+
Returns a 5-tuple:
244246
- List of argument names
245247
- List of argument types
246-
- Number of arguments that have a default value
247-
- Whether the definition typechecked.
248+
- List of default values
249+
- First argument of namedtuple
250+
- Whether all types are ready.
248251
249-
Return None if at least one of the types is not ready.
252+
Return None if the definition didn't typecheck.
250253
"""
251254
# TODO: Share code with check_argument_count in checkexpr.py?
252255
args = call.args
253256
if len(args) < 2:
254-
return self.fail_namedtuple_arg("Too few arguments for namedtuple()", call)
257+
self.fail("Too few arguments for namedtuple()", call)
258+
return None
255259
defaults = [] # type: List[Expression]
256260
if len(args) > 2:
257261
# Typed namedtuple doesn't support additional arguments.
258262
if fullname == 'typing.NamedTuple':
259-
return self.fail_namedtuple_arg("Too many arguments for NamedTuple()", call)
263+
self.fail("Too many arguments for NamedTuple()", call)
264+
return None
260265
for i, arg_name in enumerate(call.arg_names[2:], 2):
261266
if arg_name == 'defaults':
262267
arg = args[i]
@@ -272,38 +277,42 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
272277
)
273278
break
274279
if call.arg_kinds[:2] != [ARG_POS, ARG_POS]:
275-
return self.fail_namedtuple_arg("Unexpected arguments to namedtuple()", call)
280+
self.fail("Unexpected arguments to namedtuple()", call)
281+
return None
276282
if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)):
277-
return self.fail_namedtuple_arg(
283+
self.fail(
278284
"namedtuple() expects a string literal as the first argument", call)
285+
return None
286+
typename = cast(Union[StrExpr, BytesExpr, UnicodeExpr], call.args[0]).value
279287
types = [] # type: List[Type]
280-
ok = True
281288
if not isinstance(args[1], (ListExpr, TupleExpr)):
282289
if (fullname == 'collections.namedtuple'
283290
and isinstance(args[1], (StrExpr, BytesExpr, UnicodeExpr))):
284291
str_expr = args[1]
285292
items = str_expr.value.replace(',', ' ').split()
286293
else:
287-
return self.fail_namedtuple_arg(
294+
self.fail(
288295
"List or tuple literal expected as the second argument to namedtuple()", call)
296+
return None
289297
else:
290298
listexpr = args[1]
291299
if fullname == 'collections.namedtuple':
292300
# The fields argument contains just names, with implicit Any types.
293301
if any(not isinstance(item, (StrExpr, BytesExpr, UnicodeExpr))
294302
for item in listexpr.items):
295-
return self.fail_namedtuple_arg("String literal expected as namedtuple() item",
296-
call)
303+
self.fail("String literal expected as namedtuple() item", call)
304+
return None
297305
items = [cast(Union[StrExpr, BytesExpr, UnicodeExpr], item).value
298306
for item in listexpr.items]
299307
else:
300308
# The fields argument contains (name, type) tuples.
301309
result = self.parse_namedtuple_fields_with_types(listexpr.items, call)
302-
if result:
303-
items, types, _, ok = result
304-
else:
310+
if result is None:
305311
# One of the types is not ready, defer.
306312
return None
313+
items, types, _, ok = result
314+
if not ok:
315+
return [], [], [], typename, False
307316
if not types:
308317
types = [AnyType(TypeOfAny.unannotated) for _ in items]
309318
underscore = [item for item in items if item.startswith('_')]
@@ -313,50 +322,46 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
313322
if len(defaults) > len(items):
314323
self.fail("Too many defaults given in call to namedtuple()", call)
315324
defaults = defaults[:len(items)]
316-
return items, types, defaults, ok
325+
return items, types, defaults, typename, True
317326

318327
def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: Context
319328
) -> Optional[Tuple[List[str], List[Type],
320-
List[Expression],
321-
bool]]:
329+
List[Expression], bool]]:
322330
"""Parse typed named tuple fields.
323331
324-
Return (names, types, defaults, error occurred), or None if at least one of
325-
the types is not ready.
332+
Return (names, types, defaults, whether types are all ready), or None if error occurred.
326333
"""
327334
items = [] # type: List[str]
328335
types = [] # type: List[Type]
329336
for item in nodes:
330337
if isinstance(item, TupleExpr):
331338
if len(item.items) != 2:
332-
return self.fail_namedtuple_arg("Invalid NamedTuple field definition",
333-
item)
339+
self.fail("Invalid NamedTuple field definition", item)
340+
return None
334341
name, type_node = item.items
335342
if isinstance(name, (StrExpr, BytesExpr, UnicodeExpr)):
336343
items.append(name.value)
337344
else:
338-
return self.fail_namedtuple_arg("Invalid NamedTuple() field name", item)
345+
self.fail("Invalid NamedTuple() field name", item)
346+
return None
339347
try:
340348
type = expr_to_unanalyzed_type(type_node)
341349
except TypeTranslationError:
342-
return self.fail_namedtuple_arg('Invalid field type', type_node)
350+
self.fail('Invalid field type', type_node)
351+
return None
343352
analyzed = self.api.anal_type(type)
344353
# Workaround #4987 and avoid introducing a bogus UnboundType
345354
if isinstance(analyzed, UnboundType):
346355
analyzed = AnyType(TypeOfAny.from_error)
347356
# These should be all known, otherwise we would defer in visit_assignment_stmt().
348357
if analyzed is None:
349-
return None
358+
return [], [], [], False
350359
types.append(analyzed)
351360
else:
352-
return self.fail_namedtuple_arg("Tuple expected as NamedTuple() field", item)
361+
self.fail("Tuple expected as NamedTuple() field", item)
362+
return None
353363
return items, types, [], True
354364

355-
def fail_namedtuple_arg(self, message: str, context: Context
356-
) -> Tuple[List[str], List[Type], List[Expression], bool]:
357-
self.fail(message, context)
358-
return [], [], [], False
359-
360365
def build_namedtuple_typeinfo(self,
361366
name: str,
362367
items: List[str],

test-data/unit/check-incremental.test

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5056,7 +5056,9 @@ from typing import NamedTuple
50565056
NT = NamedTuple('BadName', [('x', int)])
50575057
[builtins fixtures/tuple.pyi]
50585058
[out]
5059+
tmp/b.py:2: error: First argument to namedtuple() should be 'NT', not 'BadName'
50595060
[out2]
5061+
tmp/b.py:2: error: First argument to namedtuple() should be 'NT', not 'BadName'
50605062
tmp/a.py:3: note: Revealed type is 'Tuple[builtins.int, fallback=b.NT]'
50615063

50625064
[case testNewAnalyzerIncrementalBrokenNamedTupleNested]
@@ -5076,7 +5078,9 @@ def test() -> None:
50765078
NT = namedtuple('BadName', ['x', 'y'])
50775079
[builtins fixtures/list.pyi]
50785080
[out]
5081+
tmp/b.py:4: error: First argument to namedtuple() should be 'NT', not 'BadName'
50795082
[out2]
5083+
tmp/b.py:4: error: First argument to namedtuple() should be 'NT', not 'BadName'
50805084

50815085
[case testNewAnalyzerIncrementalMethodNamedTuple]
50825086

test-data/unit/check-namedtuple.test

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -962,3 +962,14 @@ def foo():
962962
Type1 = NamedTuple('Type1', [('foo', foo)]) # E: Function "b.foo" is not valid as a type # N: Perhaps you need "Callable[...]" or a callback protocol?
963963

964964
[builtins fixtures/tuple.pyi]
965+
966+
[case testNamedTupleTypeNameMatchesVariableName]
967+
from typing import NamedTuple
968+
from collections import namedtuple
969+
970+
A = NamedTuple('X', [('a', int)]) # E: First argument to namedtuple() should be 'A', not 'X'
971+
B = namedtuple('X', ['a']) # E: First argument to namedtuple() should be 'B', not 'X'
972+
973+
C = NamedTuple('X', [('a', 'Y')]) # E: First argument to namedtuple() should be 'C', not 'X'
974+
class Y: ...
975+
[builtins fixtures/tuple.pyi]

test-data/unit/fine-grained.test

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9622,26 +9622,3 @@ class C:
96229622
[out]
96239623
==
96249624
main:5: error: Unsupported left operand type for + ("str")
9625-
9626-
[case testReexportNamedTupleChange]
9627-
from m import M
9628-
9629-
def f(x: M) -> None: ...
9630-
9631-
f(M(0))
9632-
9633-
[file m.py]
9634-
from n import M
9635-
9636-
[file n.py]
9637-
from typing import NamedTuple
9638-
M = NamedTuple('_N', [('x', int)])
9639-
9640-
[file n.py.2]
9641-
# change the line numbers
9642-
from typing import NamedTuple
9643-
M = NamedTuple('_N', [('x', int)])
9644-
9645-
[builtins fixtures/tuple.pyi]
9646-
[out]
9647-
==

0 commit comments

Comments
 (0)