Skip to content

Commit 9165bb1

Browse files
authored
Require first argument of namedtuple to match with variable name (python#9577)
Closes python#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

File tree

6 files changed

+75
-74
lines changed

6 files changed

+75
-74
lines changed

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy