Skip to content

Commit c3b3177

Browse files
JelleZijlstraJukkaL
authored andcommitted
Implement default values on NamedTuple (#2719)
This implements python/typing#338 in mypy.
1 parent 89e1286 commit c3b3177

File tree

3 files changed

+156
-31
lines changed

3 files changed

+156
-31
lines changed

mypy/nodes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ class Argument(Node):
427427
variable = None # type: Var
428428
type_annotation = None # type: Optional[mypy.types.Type]
429429
initializer = None # type: Optional[Expression]
430-
kind = None # type: int
430+
kind = None # type: int # must be an ARG_* constant
431431
initialization_statement = None # type: Optional[AssignmentStmt]
432432

433433
def __init__(self, variable: 'Var', type_annotation: 'Optional[mypy.types.Type]',

mypy/semanal.py

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr,
6666
YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, AwaitExpr,
6767
IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr, TempNode,
68-
COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES, nongen_builtins,
68+
COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES, ARG_OPT, nongen_builtins,
6969
collections_type_aliases, get_member_expr_fullname,
7070
)
7171
from mypy.typevars import has_no_typevars, fill_typevars
@@ -588,27 +588,32 @@ def visit_class_def(self, defn: ClassDef) -> None:
588588
if self.analyze_typeddict_classdef(defn):
589589
return
590590
if self.analyze_namedtuple_classdef(defn):
591-
return
592-
self.setup_class_def_analysis(defn)
591+
# just analyze the class body so we catch type errors in default values
592+
self.enter_class(defn)
593+
defn.defs.accept(self)
594+
self.leave_class()
595+
else:
596+
self.setup_class_def_analysis(defn)
593597

594-
self.bind_class_type_vars(defn)
598+
self.bind_class_type_vars(defn)
595599

596-
self.analyze_base_classes(defn)
597-
self.analyze_metaclass(defn)
600+
self.analyze_base_classes(defn)
601+
self.analyze_metaclass(defn)
598602

599-
for decorator in defn.decorators:
600-
self.analyze_class_decorator(defn, decorator)
603+
for decorator in defn.decorators:
604+
self.analyze_class_decorator(defn, decorator)
601605

602-
self.enter_class(defn)
606+
self.enter_class(defn)
603607

604-
# Analyze class body.
605-
defn.defs.accept(self)
608+
# Analyze class body.
609+
defn.defs.accept(self)
606610

607-
self.calculate_abstract_status(defn.info)
608-
self.setup_type_promotion(defn)
611+
self.calculate_abstract_status(defn.info)
612+
self.setup_type_promotion(defn)
609613

610-
self.leave_class()
611-
self.unbind_class_type_vars()
614+
self.leave_class()
615+
616+
self.unbind_class_type_vars()
612617

613618
def enter_class(self, defn: ClassDef) -> None:
614619
# Remember previous active class
@@ -818,21 +823,24 @@ def analyze_namedtuple_classdef(self, defn: ClassDef) -> bool:
818823
node = self.lookup(defn.name, defn)
819824
if node is not None:
820825
node.kind = GDEF # TODO in process_namedtuple_definition also applies here
821-
items, types = self.check_namedtuple_classdef(defn)
822-
node.node = self.build_namedtuple_typeinfo(defn.name, items, types)
826+
items, types, default_items = self.check_namedtuple_classdef(defn)
827+
node.node = self.build_namedtuple_typeinfo(
828+
defn.name, items, types, default_items)
823829
return True
824830
return False
825831

826-
def check_namedtuple_classdef(self, defn: ClassDef) -> Tuple[List[str], List[Type]]:
832+
def check_namedtuple_classdef(
833+
self, defn: ClassDef) -> Tuple[List[str], List[Type], Dict[str, Expression]]:
827834
NAMEDTUP_CLASS_ERROR = ('Invalid statement in NamedTuple definition; '
828835
'expected "field_name: field_type"')
829836
if self.options.python_version < (3, 6):
830837
self.fail('NamedTuple class syntax is only supported in Python 3.6', defn)
831-
return [], []
838+
return [], [], {}
832839
if len(defn.base_type_exprs) > 1:
833840
self.fail('NamedTuple should be a single base', defn)
834841
items = [] # type: List[str]
835842
types = [] # type: List[Type]
843+
default_items = {} # type: Dict[str, Expression]
836844
for stmt in defn.defs.body:
837845
if not isinstance(stmt, AssignmentStmt):
838846
# Still allow pass or ... (for empty namedtuples).
@@ -854,10 +862,14 @@ def check_namedtuple_classdef(self, defn: ClassDef) -> Tuple[List[str], List[Typ
854862
.format(name), stmt)
855863
if stmt.type is None or hasattr(stmt, 'new_syntax') and not stmt.new_syntax:
856864
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
857-
elif not isinstance(stmt.rvalue, TempNode):
865+
elif isinstance(stmt.rvalue, TempNode):
858866
# x: int assigns rvalue to TempNode(AnyType())
859-
self.fail('Right hand side values are not supported in NamedTuple', stmt)
860-
return items, types
867+
if default_items:
868+
self.fail('Non-default NamedTuple fields cannot follow default fields',
869+
stmt)
870+
else:
871+
default_items[name] = stmt.rvalue
872+
return items, types, default_items
861873

862874
def setup_class_def_analysis(self, defn: ClassDef) -> None:
863875
"""Prepare for the analysis of a class definition."""
@@ -1915,12 +1927,12 @@ def check_namedtuple(self, node: Expression, var_name: str = None) -> Optional[T
19151927
items, types, ok = self.parse_namedtuple_args(call, fullname)
19161928
if not ok:
19171929
# Error. Construct dummy return value.
1918-
return self.build_namedtuple_typeinfo('namedtuple', [], [])
1930+
return self.build_namedtuple_typeinfo('namedtuple', [], [], {})
19191931
name = cast(StrExpr, call.args[0]).value
19201932
if name != var_name or self.is_func_scope():
19211933
# Give it a unique name derived from the line number.
19221934
name += '@' + str(call.line)
1923-
info = self.build_namedtuple_typeinfo(name, items, types)
1935+
info = self.build_namedtuple_typeinfo(name, items, types, {})
19241936
# Store it as a global just in case it would remain anonymous.
19251937
# (Or in the nearest class if there is one.)
19261938
stnode = SymbolTableNode(GDEF, info, self.cur_mod_id)
@@ -2013,8 +2025,8 @@ def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance) -> TypeI
20132025
info.bases = [basetype_or_fallback]
20142026
return info
20152027

2016-
def build_namedtuple_typeinfo(self, name: str, items: List[str],
2017-
types: List[Type]) -> TypeInfo:
2028+
def build_namedtuple_typeinfo(self, name: str, items: List[str], types: List[Type],
2029+
default_items: Dict[str, Expression]) -> TypeInfo:
20182030
strtype = self.str_type()
20192031
basetuple_type = self.named_type('__builtins__.tuple', [AnyType()])
20202032
dictype = (self.named_type_or_none('builtins.dict', [strtype, AnyType()])
@@ -2046,6 +2058,7 @@ def add_field(var: Var, is_initialized_in_class: bool = False,
20462058
tuple_of_strings = TupleType([strtype for _ in items], basetuple_type)
20472059
add_field(Var('_fields', tuple_of_strings), is_initialized_in_class=True)
20482060
add_field(Var('_field_types', dictype), is_initialized_in_class=True)
2061+
add_field(Var('_field_defaults', dictype), is_initialized_in_class=True)
20492062
add_field(Var('_source', strtype), is_initialized_in_class=True)
20502063

20512064
tvd = TypeVarDef('NT', 1, [], info.tuple_type)
@@ -2083,8 +2096,14 @@ def add_method(funcname: str,
20832096

20842097
add_method('_replace', ret=selftype,
20852098
args=[Argument(var, var.type, EllipsisExpr(), ARG_NAMED_OPT) for var in vars])
2099+
2100+
def make_init_arg(var: Var) -> Argument:
2101+
default = default_items.get(var.name(), None)
2102+
kind = ARG_POS if default is None else ARG_OPT
2103+
return Argument(var, var.type, default, kind)
2104+
20862105
add_method('__init__', ret=NoneTyp(), name=info.name(),
2087-
args=[Argument(var, var.type, None, ARG_POS) for var in vars])
2106+
args=[make_init_arg(var) for var in vars])
20882107
add_method('_asdict', args=[], ret=ordereddictype)
20892108
add_method('_make', ret=selftype, is_classmethod=True,
20902109
args=[Argument(Var('iterable', iterable_type), iterable_type, None, ARG_POS),

test-data/unit/check-class-namedtuple.test

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ class X(NamedTuple):
294294
y: str
295295

296296
reveal_type(X._fields) # E: Revealed type is 'Tuple[builtins.str, builtins.str]'
297+
reveal_type(X._field_types) # E: Revealed type is 'builtins.dict[builtins.str, Any]'
298+
reveal_type(X._field_defaults) # E: Revealed type is 'builtins.dict[builtins.str, Any]'
299+
300+
[builtins fixtures/dict.pyi]
297301

298302
[case testNewNamedTupleUnit]
299303
# flags: --python-version 3.6
@@ -349,9 +353,17 @@ import typing
349353

350354
class X(typing.NamedTuple):
351355
x: int
352-
y: str = 'y' # E: Right hand side values are not supported in NamedTuple
353-
z = None # type: int # E: Invalid statement in NamedTuple definition; expected "field_name: field_type"
354-
x[0]: int # E: Invalid statement in NamedTuple definition; expected "field_name: field_type"
356+
y = 1
357+
x.x: int
358+
z: str = 'z'
359+
aa: int
360+
361+
[out]
362+
main:6: error: Invalid statement in NamedTuple definition; expected "field_name: field_type"
363+
main:7: error: Invalid statement in NamedTuple definition; expected "field_name: field_type"
364+
main:7: error: Type cannot be declared in assignment to non-self attribute
365+
main:7: error: "int" has no attribute "x"
366+
main:9: error: Non-default NamedTuple fields cannot follow default fields
355367

356368
[builtins fixtures/list.pyi]
357369

@@ -376,3 +388,97 @@ def f(a: Type[N]):
376388
[builtins fixtures/list.pyi]
377389
[out]
378390
main:8: error: Unsupported type Type["N"]
391+
392+
[case testNewNamedTupleWithDefaults]
393+
# flags: --fast-parser --python-version 3.6
394+
from typing import List, NamedTuple, Optional
395+
396+
class X(NamedTuple):
397+
x: int
398+
y: int = 2
399+
400+
reveal_type(X(1)) # E: Revealed type is 'Tuple[builtins.int, builtins.int, fallback=__main__.X]'
401+
reveal_type(X(1, 2)) # E: Revealed type is 'Tuple[builtins.int, builtins.int, fallback=__main__.X]'
402+
403+
X(1, 'a') # E: Argument 2 to "X" has incompatible type "str"; expected "int"
404+
X(1, z=3) # E: Unexpected keyword argument "z" for "X"
405+
406+
class HasNone(NamedTuple):
407+
x: int
408+
y: Optional[int] = None
409+
410+
reveal_type(HasNone(1)) # E: Revealed type is 'Tuple[builtins.int, builtins.int, fallback=__main__.HasNone]'
411+
412+
class Parameterized(NamedTuple):
413+
x: int
414+
y: List[int] = [1] + [2]
415+
z: List[int] = []
416+
417+
reveal_type(Parameterized(1)) # E: Revealed type is 'Tuple[builtins.int, builtins.list[builtins.int], builtins.list[builtins.int], fallback=__main__.Parameterized]'
418+
Parameterized(1, ['not an int']) # E: List item 0 has incompatible type "str"
419+
420+
class Default:
421+
pass
422+
423+
class UserDefined(NamedTuple):
424+
x: Default = Default()
425+
426+
reveal_type(UserDefined()) # E: Revealed type is 'Tuple[__main__.Default, fallback=__main__.UserDefined]'
427+
reveal_type(UserDefined(Default())) # E: Revealed type is 'Tuple[__main__.Default, fallback=__main__.UserDefined]'
428+
UserDefined(1) # E: Argument 1 to "UserDefined" has incompatible type "int"; expected "Default"
429+
430+
[builtins fixtures/list.pyi]
431+
432+
[case testNewNamedTupleWithDefaultsStrictOptional]
433+
# flags: --fast-parser --strict-optional --python-version 3.6
434+
from typing import List, NamedTuple, Optional
435+
436+
class HasNone(NamedTuple):
437+
x: int
438+
y: Optional[int] = None
439+
440+
reveal_type(HasNone(1)) # E: Revealed type is 'Tuple[builtins.int, Union[builtins.int, builtins.None], fallback=__main__.HasNone]'
441+
HasNone(None) # E: Argument 1 to "HasNone" has incompatible type None; expected "int"
442+
HasNone(1, y=None)
443+
HasNone(1, y=2)
444+
445+
class CannotBeNone(NamedTuple):
446+
x: int
447+
y: int = None # E: Incompatible types in assignment (expression has type None, variable has type "int")
448+
449+
[builtins fixtures/list.pyi]
450+
451+
[case testNewNamedTupleWrongType]
452+
# flags: --fast-parser --python-version 3.6
453+
from typing import NamedTuple
454+
455+
class X(NamedTuple):
456+
x: int
457+
y: int = 'not an int' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
458+
459+
[case testNewNamedTupleErrorInDefault]
460+
# flags: --fast-parser --python-version 3.6
461+
from typing import NamedTuple
462+
463+
class X(NamedTuple):
464+
x: int = 1 + '1' # E: Unsupported operand types for + ("int" and "str")
465+
466+
[case testNewNamedTupleInheritance]
467+
# flags: --fast-parser --python-version 3.6
468+
from typing import NamedTuple
469+
470+
class X(NamedTuple):
471+
x: str
472+
y: int = 3
473+
474+
class Y(X):
475+
def method(self) -> str:
476+
self.y
477+
return self.x
478+
479+
reveal_type(Y('a')) # E: Revealed type is 'Tuple[builtins.str, builtins.int, fallback=__main__.Y]'
480+
Y(y=1, x='1').method()
481+
482+
class CallsBaseInit(X):
483+
def __init__(self, x: str) -> None:
484+
super().__init__(x)

0 commit comments

Comments
 (0)