Skip to content

Commit 223d104

Browse files
Support additional args to namedtuple() (#5215)
1 parent 2714daf commit 223d104

11 files changed

+135
-45
lines changed

mypy/checkexpr.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,15 @@ def analyze_var_ref(self, var: Var, context: Context) -> Type:
231231
def visit_call_expr(self, e: CallExpr, allow_none_return: bool = False) -> Type:
232232
"""Type check a call expression."""
233233
if e.analyzed:
234+
if isinstance(e.analyzed, NamedTupleExpr) and not e.analyzed.is_typed:
235+
# Type check the arguments, but ignore the results. This relies
236+
# on the typeshed stubs to type check the arguments.
237+
self.visit_call_expr_inner(e)
234238
# It's really a special form that only looks like a call.
235239
return self.accept(e.analyzed, self.type_context[-1])
240+
return self.visit_call_expr_inner(e, allow_none_return=allow_none_return)
241+
242+
def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> Type:
236243
if isinstance(e.callee, NameExpr) and isinstance(e.callee.node, TypeInfo) and \
237244
e.callee.node.typeddict_type is not None:
238245
# Use named fallback for better error messages.

mypy/nodes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1917,10 +1917,12 @@ class NamedTupleExpr(Expression):
19171917
# The class representation of this named tuple (its tuple_type attribute contains
19181918
# the tuple item types)
19191919
info = None # type: TypeInfo
1920+
is_typed = False # whether this class was created with typing.NamedTuple
19201921

1921-
def __init__(self, info: 'TypeInfo') -> None:
1922+
def __init__(self, info: 'TypeInfo', is_typed: bool = False) -> None:
19221923
super().__init__()
19231924
self.info = info
1925+
self.is_typed = is_typed
19241926

19251927
def accept(self, visitor: ExpressionVisitor[T]) -> T:
19261928
return visitor.visit_namedtuple_expr(self)

mypy/semanal_namedtuple.py

Lines changed: 61 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This is conceptually part of mypy.semanal (semantic analyzer pass 2).
44
"""
55

6-
from typing import Tuple, List, Dict, Optional, cast
6+
from typing import Tuple, List, Dict, Mapping, Optional, cast
77

88
from mypy.types import (
99
Type, TupleType, NoneTyp, AnyType, TypeOfAny, TypeVarType, TypeVarDef, CallableType, TypeType
@@ -14,7 +14,7 @@
1414
AssignmentStmt, PassStmt, Decorator, FuncBase, ClassDef, Expression, RefExpr, TypeInfo,
1515
NamedTupleExpr, CallExpr, Context, TupleExpr, ListExpr, SymbolTableNode, FuncDef, Block,
1616
TempNode,
17-
ARG_POS, ARG_NAMED_OPT, ARG_OPT, MDEF, GDEF
17+
ARG_POS, ARG_NAMED, ARG_NAMED_OPT, ARG_OPT, MDEF, GDEF
1818
)
1919
from mypy.options import Options
2020
from mypy.exprtotype import expr_to_unanalyzed_type, TypeTranslationError
@@ -48,7 +48,7 @@ def analyze_namedtuple_classdef(self, defn: ClassDef) -> Optional[TypeInfo]:
4848
node.node = info
4949
defn.info.replaced = info
5050
defn.info = info
51-
defn.analyzed = NamedTupleExpr(info)
51+
defn.analyzed = NamedTupleExpr(info, is_typed=True)
5252
defn.analyzed.line = defn.line
5353
defn.analyzed.column = defn.column
5454
return info
@@ -142,35 +142,71 @@ def check_namedtuple(self,
142142
if not isinstance(callee, RefExpr):
143143
return None
144144
fullname = callee.fullname
145-
if fullname not in ('collections.namedtuple', 'typing.NamedTuple'):
145+
if fullname == 'collections.namedtuple':
146+
is_typed = False
147+
elif fullname == 'typing.NamedTuple':
148+
is_typed = True
149+
else:
146150
return None
147-
items, types, ok = self.parse_namedtuple_args(call, fullname)
151+
items, types, defaults, ok = self.parse_namedtuple_args(call, fullname)
148152
if not ok:
149153
# Error. Construct dummy return value.
150154
return self.build_namedtuple_typeinfo('namedtuple', [], [], {})
151155
name = cast(StrExpr, call.args[0]).value
152156
if name != var_name or is_func_scope:
153157
# Give it a unique name derived from the line number.
154158
name += '@' + str(call.line)
155-
info = self.build_namedtuple_typeinfo(name, items, types, {})
159+
if len(defaults) > 0:
160+
default_items = {
161+
arg_name: default
162+
for arg_name, default in zip(items[-len(defaults):], defaults)
163+
}
164+
else:
165+
default_items = {}
166+
info = self.build_namedtuple_typeinfo(name, items, types, default_items)
156167
# Store it as a global just in case it would remain anonymous.
157168
# (Or in the nearest class if there is one.)
158169
stnode = SymbolTableNode(GDEF, info)
159170
self.api.add_symbol_table_node(name, stnode)
160-
call.analyzed = NamedTupleExpr(info)
171+
call.analyzed = NamedTupleExpr(info, is_typed=is_typed)
161172
call.analyzed.set_line(call.line, call.column)
162173
return info
163174

164-
def parse_namedtuple_args(self, call: CallExpr,
165-
fullname: str) -> Tuple[List[str], List[Type], bool]:
175+
def parse_namedtuple_args(self, call: CallExpr, fullname: str
176+
) -> Tuple[List[str], List[Type], List[Expression], bool]:
177+
"""Parse a namedtuple() call into data needed to construct a type.
178+
179+
Returns a 4-tuple:
180+
- List of argument names
181+
- List of argument types
182+
- Number of arguments that have a default value
183+
- Whether the definition typechecked.
184+
185+
"""
166186
# TODO: Share code with check_argument_count in checkexpr.py?
167187
args = call.args
168188
if len(args) < 2:
169189
return self.fail_namedtuple_arg("Too few arguments for namedtuple()", call)
190+
defaults = [] # type: List[Expression]
170191
if len(args) > 2:
171-
# FIX incorrect. There are two additional parameters
172-
return self.fail_namedtuple_arg("Too many arguments for namedtuple()", call)
173-
if call.arg_kinds != [ARG_POS, ARG_POS]:
192+
# Typed namedtuple doesn't support additional arguments.
193+
if fullname == 'typing.NamedTuple':
194+
return self.fail_namedtuple_arg("Too many arguments for NamedTuple()", call)
195+
for i, arg_name in enumerate(call.arg_names[2:], 2):
196+
if arg_name == 'defaults':
197+
arg = args[i]
198+
# We don't care what the values are, as long as the argument is an iterable
199+
# and we can count how many defaults there are.
200+
if isinstance(arg, (ListExpr, TupleExpr)):
201+
defaults = list(arg.items)
202+
else:
203+
self.fail(
204+
"List or tuple literal expected as the defaults argument to "
205+
"namedtuple()",
206+
arg
207+
)
208+
break
209+
if call.arg_kinds[:2] != [ARG_POS, ARG_POS]:
174210
return self.fail_namedtuple_arg("Unexpected arguments to namedtuple()", call)
175211
if not isinstance(args[0], (StrExpr, BytesExpr, UnicodeExpr)):
176212
return self.fail_namedtuple_arg(
@@ -196,17 +232,21 @@ def parse_namedtuple_args(self, call: CallExpr,
196232
items = [cast(StrExpr, item).value for item in listexpr.items]
197233
else:
198234
# The fields argument contains (name, type) tuples.
199-
items, types, ok = self.parse_namedtuple_fields_with_types(listexpr.items, call)
235+
items, types, _, ok = self.parse_namedtuple_fields_with_types(listexpr.items, call)
200236
if not types:
201237
types = [AnyType(TypeOfAny.unannotated) for _ in items]
202238
underscore = [item for item in items if item.startswith('_')]
203239
if underscore:
204240
self.fail("namedtuple() field names cannot start with an underscore: "
205241
+ ', '.join(underscore), call)
206-
return items, types, ok
242+
if len(defaults) > len(items):
243+
self.fail("Too many defaults given in call to namedtuple()", call)
244+
defaults = defaults[:len(items)]
245+
return items, types, defaults, ok
207246

208-
def parse_namedtuple_fields_with_types(self, nodes: List[Expression],
209-
context: Context) -> Tuple[List[str], List[Type], bool]:
247+
def parse_namedtuple_fields_with_types(self, nodes: List[Expression], context: Context
248+
) -> Tuple[List[str], List[Type], List[Expression],
249+
bool]:
210250
items = [] # type: List[str]
211251
types = [] # type: List[Type]
212252
for item in nodes:
@@ -226,15 +266,15 @@ def parse_namedtuple_fields_with_types(self, nodes: List[Expression],
226266
types.append(self.api.anal_type(type))
227267
else:
228268
return self.fail_namedtuple_arg("Tuple expected as NamedTuple() field", item)
229-
return items, types, True
269+
return items, types, [], True
230270

231-
def fail_namedtuple_arg(self, message: str,
232-
context: Context) -> Tuple[List[str], List[Type], bool]:
271+
def fail_namedtuple_arg(self, message: str, context: Context
272+
) -> Tuple[List[str], List[Type], List[Expression], bool]:
233273
self.fail(message, context)
234-
return [], [], False
274+
return [], [], [], False
235275

236276
def build_namedtuple_typeinfo(self, name: str, items: List[str], types: List[Type],
237-
default_items: Dict[str, Expression]) -> TypeInfo:
277+
default_items: Mapping[str, Expression]) -> TypeInfo:
238278
strtype = self.api.named_type('__builtins__.str')
239279
implicit_any = AnyType(TypeOfAny.special_form)
240280
basetuple_type = self.api.named_type('__builtins__.tuple', [implicit_any])

test-data/unit/check-namedtuple.test

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[case testNamedTupleUsedAsTuple]
22
from collections import namedtuple
33

4-
X = namedtuple('X', ['x', 'y'])
4+
X = namedtuple('X', 'x y')
55
x = None # type: X
66
a, b = x
77
b = x[0]
@@ -28,7 +28,7 @@ X = namedtuple('X', 'x, _y, _z') # E: namedtuple() field names cannot start wit
2828
[case testNamedTupleAccessingAttributes]
2929
from collections import namedtuple
3030

31-
X = namedtuple('X', ['x', 'y'])
31+
X = namedtuple('X', 'x y')
3232
x = None # type: X
3333
x.x
3434
x.y
@@ -38,7 +38,7 @@ x.z # E: "X" has no attribute "z"
3838
[case testNamedTupleAttributesAreReadOnly]
3939
from collections import namedtuple
4040

41-
X = namedtuple('X', ['x', 'y'])
41+
X = namedtuple('X', 'x y')
4242
x = None # type: X
4343
x.x = 5 # E: Property "x" defined in "X" is read-only
4444
x.y = 5 # E: Property "y" defined in "X" is read-only
@@ -71,7 +71,7 @@ main:10: note: Protocol member HasX.x expected settable variable, got read-only
7171
[case testNamedTupleCreateWithPositionalArguments]
7272
from collections import namedtuple
7373

74-
X = namedtuple('X', ['x', 'y'])
74+
X = namedtuple('X', 'x y')
7575
x = X(1, 'x')
7676
x.x
7777
x.z # E: "X" has no attribute "z"
@@ -81,21 +81,52 @@ x = X(1, 2, 3) # E: Too many arguments for "X"
8181
[case testCreateNamedTupleWithKeywordArguments]
8282
from collections import namedtuple
8383

84-
X = namedtuple('X', ['x', 'y'])
84+
X = namedtuple('X', 'x y')
8585
x = X(x=1, y='x')
8686
x = X(1, y='x')
8787
x = X(x=1, z=1) # E: Unexpected keyword argument "z" for "X"
8888
x = X(y=1) # E: Missing positional argument "x" in call to "X"
8989

90-
9190
[case testNamedTupleCreateAndUseAsTuple]
9291
from collections import namedtuple
9392

94-
X = namedtuple('X', ['x', 'y'])
93+
X = namedtuple('X', 'x y')
9594
x = X(1, 'x')
9695
a, b = x
9796
a, b, c = x # E: Need more than 2 values to unpack (3 expected)
9897

98+
[case testNamedTupleAdditionalArgs]
99+
from collections import namedtuple
100+
101+
A = namedtuple('A', 'a b')
102+
B = namedtuple('B', 'a b', rename=1)
103+
C = namedtuple('C', 'a b', rename='not a bool')
104+
D = namedtuple('D', 'a b', unrecognized_arg=False)
105+
E = namedtuple('E', 'a b', 0)
106+
107+
[builtins fixtures/bool.pyi]
108+
109+
[out]
110+
main:5: error: Argument "rename" to "namedtuple" has incompatible type "str"; expected "int"
111+
main:6: error: Unexpected keyword argument "unrecognized_arg" for "namedtuple"
112+
<ROOT>/test-data/unit/lib-stub/collections.pyi:3: note: "namedtuple" defined here
113+
main:7: error: Too many positional arguments for "namedtuple"
114+
115+
[case testNamedTupleDefaults]
116+
# flags: --python-version 3.7
117+
from collections import namedtuple
118+
119+
X = namedtuple('X', ['x', 'y'], defaults=(1,))
120+
121+
X() # E: Too few arguments for "X"
122+
X(0) # ok
123+
X(0, 1) # ok
124+
X(0, 1, 2) # E: Too many arguments for "X"
125+
126+
Y = namedtuple('Y', ['x', 'y'], defaults=(1, 2, 3)) # E: Too many defaults given in call to namedtuple()
127+
Z = namedtuple('Z', ['x', 'y'], defaults='not a tuple') # E: Argument "defaults" to "namedtuple" has incompatible type "str"; expected "Optional[Iterable[Any]]" # E: List or tuple literal expected as the defaults argument to namedtuple()
128+
129+
[builtins fixtures/list.pyi]
99130

100131
[case testNamedTupleWithItemTypes]
101132
from typing import NamedTuple
@@ -240,6 +271,7 @@ import collections
240271
MyNamedTuple = collections.namedtuple('MyNamedTuple', ['spam', 'eggs'])
241272
MyNamedTuple.x # E: "Type[MyNamedTuple]" has no attribute "x"
242273

274+
[builtins fixtures/list.pyi]
243275

244276
[case testNamedTupleEmptyItems]
245277
from typing import NamedTuple
@@ -280,6 +312,8 @@ x._replace(x=3, y=5)
280312
x._replace(z=5) # E: Unexpected keyword argument "z" for "_replace" of "X"
281313
x._replace(5) # E: Too many positional arguments for "_replace" of "X"
282314

315+
[builtins fixtures/list.pyi]
316+
283317
[case testNamedTupleReplaceAsClass]
284318
from collections import namedtuple
285319

@@ -288,6 +322,7 @@ x = None # type: X
288322
X._replace(x, x=1, y=2)
289323
X._replace(x=1, y=2) # E: Missing positional argument "self" in call to "_replace" of "X"
290324

325+
[builtins fixtures/list.pyi]
291326

292327
[case testNamedTupleReplaceTyped]
293328
from typing import NamedTuple

test-data/unit/check-newtype.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ Point3 = NewType('Point3', Vector3)
123123
p3 = Point3(Vector3(1, 3))
124124
reveal_type(p3.x) # E: Revealed type is 'builtins.int'
125125
reveal_type(p3.y) # E: Revealed type is 'builtins.int'
126+
127+
[builtins fixtures/list.pyi]
128+
126129
[out]
127130

128131
[case testNewTypeWithCasts]

test-data/unit/check-python2.test

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,6 @@ s = b'foo'
1313
from typing import TypeVar
1414
T = TypeVar(u'T')
1515

16-
[case testNamedTupleUnicode]
17-
from typing import NamedTuple
18-
from collections import namedtuple
19-
N = NamedTuple(u'N', [(u'x', int)])
20-
n = namedtuple(u'n', u'x y')
21-
22-
[builtins fixtures/dict.pyi]
23-
2416
[case testPrintStatement]
2517
print ''() # E: "str" not callable
2618
print 1, 1() # E: "int" not callable

test-data/unit/fixtures/args.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ class int:
2727
class str: pass
2828
class bool: pass
2929
class function: pass
30+
class ellipsis: pass

test-data/unit/fixtures/ops.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,5 @@ class float: pass
5555
class BaseException: pass
5656

5757
def __print(a1=None, a2=None, a3=None, a4=None): pass
58+
59+
class ellipsis: pass
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1-
import typing
1+
from typing import Any, Iterable, Union, Optional
22

3-
namedtuple = object()
3+
def namedtuple(
4+
typename: str,
5+
field_names: Union[str, Iterable[str]],
6+
*,
7+
# really bool but many tests don't have bool available
8+
rename: int = ...,
9+
module: Optional[str] = ...,
10+
defaults: Optional[Iterable[Any]] = ...
11+
) -> Any: ...

test-data/unit/python2eval.test

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,17 @@ s = ''.join([u'']) # Error
262262
_program.py:5: error: Incompatible types in assignment (expression has type "str", variable has type "int")
263263
_program.py:6: error: Incompatible types in assignment (expression has type "unicode", variable has type "str")
264264

265-
[case testNamedTupleError_python2]
266-
import typing
265+
[case testNamedTuple_python2]
266+
from typing import NamedTuple
267267
from collections import namedtuple
268268
X = namedtuple('X', ['a', 'b'])
269269
x = X(a=1, b='s')
270270
x.c
271271
x.a
272+
273+
N = NamedTuple(u'N', [(u'x', int)])
274+
n = namedtuple(u'n', u'x y')
275+
272276
[out]
273277
_program.py:5: error: "X" has no attribute "c"
274278

test-data/unit/semanal-namedtuple.test

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,6 @@ MypyFile:1(
138138
from collections import namedtuple
139139
N = namedtuple('N') # E: Too few arguments for namedtuple()
140140

141-
[case testNamedTupleWithTooManyArguments]
142-
from collections import namedtuple
143-
N = namedtuple('N', ['x'], 'y') # E: Too many arguments for namedtuple()
144-
145141
[case testNamedTupleWithInvalidName]
146142
from collections import namedtuple
147143
N = namedtuple(1, ['x']) # E: namedtuple() expects a string literal as the first argument

0 commit comments

Comments
 (0)