Skip to content

Require first argument of namedtuple to match with variable name #9577

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Oct 18, 2020
2 changes: 1 addition & 1 deletion mypy/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ def final_iteration(self) -> bool:
# A context for querying for configuration data about a module for
# cache invalidation purposes.
ReportConfigContext = NamedTuple(
'DynamicClassDefContext', [
'ReportConfigContext', [
('id', str), # Module name
('path', str), # Module file path
('is_check', bool) # Is this invocation for checking whether the config matches
Expand Down
16 changes: 10 additions & 6 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -2177,13 +2177,17 @@ def analyze_namedtuple_assign(self, s: AssignmentStmt) -> bool:
return False
lvalue = s.lvalues[0]
name = lvalue.name
is_named_tuple, info = self.named_tuple_analyzer.check_namedtuple(s.rvalue, name,
self.is_func_scope())
if not is_named_tuple:
internal_name, info = self.named_tuple_analyzer.check_namedtuple(s.rvalue, name,
self.is_func_scope())
if internal_name is None:
return False
if isinstance(lvalue, MemberExpr):
self.fail("NamedTuple type as an attribute is not supported", lvalue)
return False
if internal_name != name:
self.fail("First argument to namedtuple() should be '{}', not '{}'".format(
name, internal_name), s.rvalue)
return True
# Yes, it's a valid namedtuple, but defer if it is not ready.
if not info:
self.mark_incomplete(name, lvalue, becomes_typeinfo=True)
Expand Down Expand Up @@ -4819,9 +4823,9 @@ def expr_to_analyzed_type(self,
allow_placeholder: bool = False) -> Optional[Type]:
if isinstance(expr, CallExpr):
expr.accept(self)
is_named_tuple, info = self.named_tuple_analyzer.check_namedtuple(expr, None,
self.is_func_scope())
if not is_named_tuple:
internal_name, info = self.named_tuple_analyzer.check_namedtuple(expr, None,
self.is_func_scope())
if internal_name is None:
# Some form of namedtuple is the only valid type that looks like a call
# expression. This isn't a valid type.
raise TypeTranslationError()
Expand Down
93 changes: 49 additions & 44 deletions mypy/semanal_namedtuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,47 +138,48 @@ def check_namedtuple_classdef(self, defn: ClassDef, is_stub_file: bool
def check_namedtuple(self,
node: Expression,
var_name: Optional[str],
is_func_scope: bool) -> Tuple[bool, Optional[TypeInfo]]:
is_func_scope: bool) -> Tuple[Optional[str], Optional[TypeInfo]]:
"""Check if a call defines a namedtuple.

The optional var_name argument is the name of the variable to
which this is assigned, if any.

Return a tuple of two items:
* Can it be a valid named tuple?
* Internal name of the named tuple (e.g. the name passed as an argument to namedtuple)
or None if it is not a valid named tuple
* Corresponding TypeInfo, or None if not ready.

If the definition is invalid but looks like a namedtuple,
report errors but return (some) TypeInfo.
"""
if not isinstance(node, CallExpr):
return False, None
return None, None
call = node
callee = call.callee
if not isinstance(callee, RefExpr):
return False, None
return None, None
fullname = callee.fullname
if fullname == 'collections.namedtuple':
is_typed = False
elif fullname == 'typing.NamedTuple':
is_typed = True
else:
return False, None
return None, None
result = self.parse_namedtuple_args(call, fullname)
if result:
items, types, defaults, ok = result
items, types, defaults, typename, ok = result
else:
# This is a valid named tuple but some types are not ready.
return True, None
if not ok:
# Error. Construct dummy return value.
if var_name:
name = var_name
else:
name = 'namedtuple@' + str(call.line)
info = self.build_namedtuple_typeinfo(name, [], [], {}, node.line)
self.store_namedtuple_info(info, name, call, is_typed)
return True, info
return name, info
if not ok:
# This is a valid named tuple but some types are not ready.
return typename, None

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

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

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

def parse_namedtuple_args(self, call: CallExpr, fullname: str
) -> Optional[Tuple[List[str], List[Type], List[Expression], bool]]:
) -> Optional[Tuple[List[str], List[Type], List[Expression],
str, bool]]:
"""Parse a namedtuple() call into data needed to construct a type.

Returns a 4-tuple:
Returns a 5-tuple:
- List of argument names
- List of argument types
- Number of arguments that have a default value
- Whether the definition typechecked.
- List of default values
- First argument of namedtuple
- Whether all types are ready.

Return None if at least one of the types is not ready.
Return None if the definition didn't typecheck.
"""
# TODO: Share code with check_argument_count in checkexpr.py?
args = call.args
if len(args) < 2:
return self.fail_namedtuple_arg("Too few arguments for namedtuple()", call)
self.fail("Too few arguments for namedtuple()", call)
return None
defaults = [] # type: List[Expression]
if len(args) > 2:
# Typed namedtuple doesn't support additional arguments.
if fullname == 'typing.NamedTuple':
return self.fail_namedtuple_arg("Too many arguments for NamedTuple()", call)
self.fail("Too many arguments for NamedTuple()", call)
return None
for i, arg_name in enumerate(call.arg_names[2:], 2):
if arg_name == 'defaults':
arg = args[i]
Expand All @@ -272,38 +277,42 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
)
break
if call.arg_kinds[:2] != [ARG_POS, ARG_POS]:
return self.fail_namedtuple_arg("Unexpected arguments to namedtuple()", call)
self.fail("Unexpected arguments to namedtuple()", call)
return None
if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)):
return self.fail_namedtuple_arg(
self.fail(
"namedtuple() expects a string literal as the first argument", call)
return None
typename = cast(Union[StrExpr, BytesExpr, UnicodeExpr], call.args[0]).value
types = [] # type: List[Type]
ok = True
if not isinstance(args[1], (ListExpr, TupleExpr)):
if (fullname == 'collections.namedtuple'
and isinstance(args[1], (StrExpr, BytesExpr, UnicodeExpr))):
str_expr = args[1]
items = str_expr.value.replace(',', ' ').split()
else:
return self.fail_namedtuple_arg(
self.fail(
"List or tuple literal expected as the second argument to namedtuple()", call)
return None
else:
listexpr = args[1]
if fullname == 'collections.namedtuple':
# The fields argument contains just names, with implicit Any types.
if any(not isinstance(item, (StrExpr, BytesExpr, UnicodeExpr))
for item in listexpr.items):
return self.fail_namedtuple_arg("String literal expected as namedtuple() item",
call)
self.fail("String literal expected as namedtuple() item", call)
return None
items = [cast(Union[StrExpr, BytesExpr, UnicodeExpr], item).value
for item in listexpr.items]
else:
# The fields argument contains (name, type) tuples.
result = self.parse_namedtuple_fields_with_types(listexpr.items, call)
if result:
items, types, _, ok = result
else:
if result is None:
# One of the types is not ready, defer.
return None
items, types, _, ok = result
if not ok:
return [], [], [], typename, False
if not types:
types = [AnyType(TypeOfAny.unannotated) for _ in items]
underscore = [item for item in items if item.startswith('_')]
Expand All @@ -313,50 +322,46 @@ def parse_namedtuple_args(self, call: CallExpr, fullname: str
if len(defaults) > len(items):
self.fail("Too many defaults given in call to namedtuple()", call)
defaults = defaults[:len(items)]
return items, types, defaults, ok
return items, types, defaults, typename, True

def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: Context
) -> Optional[Tuple[List[str], List[Type],
List[Expression],
bool]]:
List[Expression], bool]]:
"""Parse typed named tuple fields.

Return (names, types, defaults, error occurred), or None if at least one of
the types is not ready.
Return (names, types, defaults, whether types are all ready), or None if error occurred.
"""
items = [] # type: List[str]
types = [] # type: List[Type]
for item in nodes:
if isinstance(item, TupleExpr):
if len(item.items) != 2:
return self.fail_namedtuple_arg("Invalid NamedTuple field definition",
item)
self.fail("Invalid NamedTuple field definition", item)
return None
name, type_node = item.items
if isinstance(name, (StrExpr, BytesExpr, UnicodeExpr)):
items.append(name.value)
else:
return self.fail_namedtuple_arg("Invalid NamedTuple() field name", item)
self.fail("Invalid NamedTuple() field name", item)
return None
try:
type = expr_to_unanalyzed_type(type_node)
except TypeTranslationError:
return self.fail_namedtuple_arg('Invalid field type', type_node)
self.fail('Invalid field type', type_node)
return None
analyzed = self.api.anal_type(type)
# Workaround #4987 and avoid introducing a bogus UnboundType
if isinstance(analyzed, UnboundType):
analyzed = AnyType(TypeOfAny.from_error)
# These should be all known, otherwise we would defer in visit_assignment_stmt().
if analyzed is None:
return None
return [], [], [], False
types.append(analyzed)
else:
return self.fail_namedtuple_arg("Tuple expected as NamedTuple() field", item)
self.fail("Tuple expected as NamedTuple() field", item)
return None
return items, types, [], True

def fail_namedtuple_arg(self, message: str, context: Context
) -> Tuple[List[str], List[Type], List[Expression], bool]:
self.fail(message, context)
return [], [], [], False

def build_namedtuple_typeinfo(self,
name: str,
items: List[str],
Expand Down
4 changes: 4 additions & 0 deletions test-data/unit/check-incremental.test
Original file line number Diff line number Diff line change
Expand Up @@ -5056,7 +5056,9 @@ from typing import NamedTuple
NT = NamedTuple('BadName', [('x', int)])
[builtins fixtures/tuple.pyi]
[out]
tmp/b.py:2: error: First argument to namedtuple() should be 'NT', not 'BadName'
[out2]
tmp/b.py:2: error: First argument to namedtuple() should be 'NT', not 'BadName'
tmp/a.py:3: note: Revealed type is 'Tuple[builtins.int, fallback=b.NT]'

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

[case testNewAnalyzerIncrementalMethodNamedTuple]

Expand Down
11 changes: 11 additions & 0 deletions test-data/unit/check-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -962,3 +962,14 @@ def foo():
Type1 = NamedTuple('Type1', [('foo', foo)]) # E: Function "b.foo" is not valid as a type # N: Perhaps you need "Callable[...]" or a callback protocol?

[builtins fixtures/tuple.pyi]

[case testNamedTupleTypeNameMatchesVariableName]
from typing import NamedTuple
from collections import namedtuple

A = NamedTuple('X', [('a', int)]) # E: First argument to namedtuple() should be 'A', not 'X'
B = namedtuple('X', ['a']) # E: First argument to namedtuple() should be 'B', not 'X'

C = NamedTuple('X', [('a', 'Y')]) # E: First argument to namedtuple() should be 'C', not 'X'
class Y: ...
[builtins fixtures/tuple.pyi]
23 changes: 0 additions & 23 deletions test-data/unit/fine-grained.test
Original file line number Diff line number Diff line change
Expand Up @@ -9622,26 +9622,3 @@ class C:
[out]
==
main:5: error: Unsupported left operand type for + ("str")

[case testReexportNamedTupleChange]
from m import M

def f(x: M) -> None: ...

f(M(0))

[file m.py]
from n import M

[file n.py]
from typing import NamedTuple
M = NamedTuple('_N', [('x', int)])

[file n.py.2]
# change the line numbers
from typing import NamedTuple
M = NamedTuple('_N', [('x', int)])

[builtins fixtures/tuple.pyi]
[out]
==