Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ class Argument(Node):
variable = None # type: Var
type_annotation = None # type: Optional[mypy.types.Type]
initializer = None # type: Optional[Expression]
kind = None # type: int
kind = None # type: int # must be an ARG_* constant
initialization_statement = None # type: Optional[AssignmentStmt]

def __init__(self, variable: 'Var', type_annotation: 'Optional[mypy.types.Type]',
Expand Down
66 changes: 42 additions & 24 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr,
YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, AwaitExpr,
IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr, TempNode,
COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES,
COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES, ARG_OPT,
)
from mypy.visitor import NodeVisitor
from mypy.traverser import TraverserVisitor
Expand Down Expand Up @@ -564,28 +564,32 @@ def check_function_signature(self, fdef: FuncItem) -> None:

def visit_class_def(self, defn: ClassDef) -> None:
self.clean_up_bases_and_infer_type_variables(defn)
if self.analyze_namedtuple_classdef(defn):
return
self.setup_class_def_analysis(defn)
is_named_tuple = self.analyze_namedtuple_classdef(defn)

self.bind_class_type_vars(defn)
if not is_named_tuple:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for coming up with new comments, but I think that this will be cleaner if you refactor this as:

if self.analyze_namedtuple_classdef(defn):
    # quick check
    self.enter_class(defn)
    defn.defs.accept(self)
    self.leave_class()
else:
    # full story
    ...

self.setup_class_def_analysis(defn)

self.analyze_base_classes(defn)
self.analyze_metaclass(defn)
self.bind_class_type_vars(defn)

for decorator in defn.decorators:
self.analyze_class_decorator(defn, decorator)
self.analyze_base_classes(defn)
self.analyze_metaclass(defn)

for decorator in defn.decorators:
self.analyze_class_decorator(defn, decorator)

self.enter_class(defn)

# Analyze class body.
defn.defs.accept(self)

self.calculate_abstract_status(defn.info)
self.setup_type_promotion(defn)
if not is_named_tuple:
self.calculate_abstract_status(defn.info)
self.setup_type_promotion(defn)

self.leave_class()
self.unbind_class_type_vars()

if not is_named_tuple:
self.unbind_class_type_vars()

def enter_class(self, defn: ClassDef) -> None:
# Remember previous active class
Expand Down Expand Up @@ -742,21 +746,24 @@ def analyze_namedtuple_classdef(self, defn: ClassDef) -> bool:
node = self.lookup(defn.name, defn)
if node is not None:
node.kind = GDEF # TODO in process_namedtuple_definition also applies here
items, types = self.check_namedtuple_classdef(defn)
node.node = self.build_namedtuple_typeinfo(defn.name, items, types)
items, types, default_items = self.check_namedtuple_classdef(defn)
node.node = self.build_namedtuple_typeinfo(
defn.name, items, types, default_items)
return True
return False

def check_namedtuple_classdef(self, defn: ClassDef) -> Tuple[List[str], List[Type]]:
def check_namedtuple_classdef(
self, defn: ClassDef) -> Tuple[List[str], List[Type], Dict[str, Expression]]:
NAMEDTUP_CLASS_ERROR = ('Invalid statement in NamedTuple definition; '
'expected "field_name: field_type"')
if self.options.python_version < (3, 6):
self.fail('NamedTuple class syntax is only supported in Python 3.6', defn)
return [], []
return [], [], {}
if len(defn.base_type_exprs) > 1:
self.fail('NamedTuple should be a single base', defn)
items = [] # type: List[str]
types = [] # type: List[Type]
default_items = {} # type: Dict[str, Expression]
for stmt in defn.defs.body:
if not isinstance(stmt, AssignmentStmt):
# Still allow pass or ... (for empty namedtuples).
Expand All @@ -778,10 +785,14 @@ def check_namedtuple_classdef(self, defn: ClassDef) -> Tuple[List[str], List[Typ
.format(name), stmt)
if stmt.type is None or hasattr(stmt, 'new_syntax') and not stmt.new_syntax:
self.fail(NAMEDTUP_CLASS_ERROR, stmt)
elif not isinstance(stmt.rvalue, TempNode):
elif isinstance(stmt.rvalue, TempNode):
# x: int assigns rvalue to TempNode(AnyType())
self.fail('Right hand side values are not supported in NamedTuple', stmt)
return items, types
if default_items:
self.fail('Non-default NamedTuple fields cannot follow default fields',
stmt)
else:
default_items[name] = stmt.rvalue
return items, types, default_items

def setup_class_def_analysis(self, defn: ClassDef) -> None:
"""Prepare for the analysis of a class definition."""
Expand Down Expand Up @@ -1687,12 +1698,12 @@ def check_namedtuple(self, node: Expression, var_name: str = None) -> Optional[T
items, types, ok = self.parse_namedtuple_args(call, fullname)
if not ok:
# Error. Construct dummy return value.
return self.build_namedtuple_typeinfo('namedtuple', [], [])
return self.build_namedtuple_typeinfo('namedtuple', [], [], {})
name = cast(StrExpr, call.args[0]).value
if name != var_name or self.is_func_scope():
# Give it a unique name derived from the line number.
name += '@' + str(call.line)
info = self.build_namedtuple_typeinfo(name, items, types)
info = self.build_namedtuple_typeinfo(name, items, types, {})
# Store it as a global just in case it would remain anonymous.
# (Or in the nearest class if there is one.)
stnode = SymbolTableNode(GDEF, info, self.cur_mod_id)
Expand Down Expand Up @@ -1785,8 +1796,8 @@ def basic_new_typeinfo(self, name: str, basetype_or_fallback: Instance) -> TypeI
info.bases = [basetype_or_fallback]
return info

def build_namedtuple_typeinfo(self, name: str, items: List[str],
types: List[Type]) -> TypeInfo:
def build_namedtuple_typeinfo(self, name: str, items: List[str], types: List[Type],
default_items: Dict[str, Expression]) -> TypeInfo:
strtype = self.str_type()
basetuple_type = self.named_type('__builtins__.tuple', [AnyType()])
dictype = (self.named_type_or_none('builtins.dict', [strtype, AnyType()])
Expand Down Expand Up @@ -1818,6 +1829,7 @@ def add_field(var: Var, is_initialized_in_class: bool = False,
tuple_of_strings = TupleType([strtype for _ in items], basetuple_type)
add_field(Var('_fields', tuple_of_strings), is_initialized_in_class=True)
add_field(Var('_field_types', dictype), is_initialized_in_class=True)
add_field(Var('_field_defaults', dictype), is_initialized_in_class=True)
add_field(Var('_source', strtype), is_initialized_in_class=True)

tvd = TypeVarDef('NT', 1, [], info.tuple_type)
Expand Down Expand Up @@ -1855,8 +1867,14 @@ def add_method(funcname: str,

add_method('_replace', ret=selftype,
args=[Argument(var, var.type, EllipsisExpr(), ARG_NAMED_OPT) for var in vars])

def make_init_arg(var: Var) -> Argument:
default = default_items.get(var.name(), None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default value could be itself None. So that I would use sentinel = object() and then default_items.get(var.name(), sentinel) and below if default is sentinel.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Python default value could be None, but as I understand it mypy would use a value of type mypy.types.NoneTyp internally. The previous code also passes None to indicate that the Argument had no default, so I think this is fine.

kind = ARG_POS if default is None else ARG_OPT
return Argument(var, var.type, default, kind)

add_method('__init__', ret=NoneTyp(), name=info.name(),
args=[Argument(var, var.type, None, ARG_POS) for var in vars])
args=[make_init_arg(var) for var in vars])
add_method('_asdict', args=[], ret=ordereddictype)
add_method('_make', ret=selftype, is_classmethod=True,
args=[Argument(Var('iterable', iterable_type), iterable_type, None, ARG_POS),
Expand Down
106 changes: 103 additions & 3 deletions test-data/unit/check-class-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ class X(NamedTuple):
y: str

reveal_type(X._fields) # E: Revealed type is 'Tuple[builtins.str, builtins.str]'
reveal_type(X._field_types) # E: Revealed type is 'builtins.dict[builtins.str, Any]'
reveal_type(X._field_defaults) # E: Revealed type is 'builtins.dict[builtins.str, Any]'

[builtins fixtures/dict.pyi]

[case testNewNamedTupleUnit]
# flags: --fast-parser --python-version 3.6
Expand Down Expand Up @@ -349,9 +353,17 @@ import typing

class X(typing.NamedTuple):
x: int
y: str = 'y' # E: Right hand side values are not supported in NamedTuple
z = None # type: int # E: Invalid statement in NamedTuple definition; expected "field_name: field_type"
x[0]: int # E: Invalid statement in NamedTuple definition; expected "field_name: field_type"
y = 1
x.x: int
z: str = 'z'
aa: int

[out]
main:6: error: Invalid statement in NamedTuple definition; expected "field_name: field_type"
main:7: error: Invalid statement in NamedTuple definition; expected "field_name: field_type"
main:7: error: Type cannot be declared in assignment to non-self attribute
main:7: error: "int" has no attribute "x"
main:9: error: Non-default NamedTuple fields cannot follow default fields

[builtins fixtures/list.pyi]

Expand All @@ -376,3 +388,91 @@ def f(a: Type[N]):
[builtins fixtures/list.pyi]
[out]
main:8: error: Unsupported type Type["N"]

[case testNewNamedTupleWithDefaults]
# flags: --fast-parser --python-version 3.6
from typing import List, NamedTuple, Optional

class X(NamedTuple):
x: int
y: int = 2

reveal_type(X(1)) # E: Revealed type is 'Tuple[builtins.int, builtins.int, fallback=__main__.X]'
reveal_type(X(1, 2)) # E: Revealed type is 'Tuple[builtins.int, builtins.int, fallback=__main__.X]'

X(1, 'a') # E: Argument 2 to "X" has incompatible type "str"; expected "int"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add few more test cases. For example, the None default value mentioned above, user defined classes as field types, wrong types like x: str = 5, etc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it makes sense to add tests with classes inheriting form a named tuple with default values.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for these suggestions! The x: str = 5 case actually wasn't checked, and I had some trouble coming up with a way to make that throw an error, because the body of the NamedTuple is currently almost completely ignored. I managed to fix it by typechecking the body like a normal class. This has the side effect that some errors (stuff like x[0]: int) produce multiple errors on the same line, but that doesn't seem like a big deal.

X(1, z=3) # E: Unexpected keyword argument "z" for "X"

class HasNone(NamedTuple):
x: int
y: Optional[int] = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for writing more tests. I will be even more happy if you add few more tests for these (also with --strict-optional)


reveal_type(HasNone(1)) # E: Revealed type is 'Tuple[builtins.int, builtins.int, fallback=__main__.HasNone]'

class Parameterized(NamedTuple):
x: int
y: List[int] = [1] + [2]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add test case where the default value if [] and the type is List[int]. This will verify that the attribute type is used as type context.


reveal_type(Parameterized(1)) # E: Revealed type is 'Tuple[builtins.int, builtins.list[builtins.int], fallback=__main__.Parameterized]'
Parameterized(1, ['not an int']) # E: List item 0 has incompatible type "str"

class Default:
pass

class UserDefined(NamedTuple):
x: Default = Default()

reveal_type(UserDefined()) # E: Revealed type is 'Tuple[__main__.Default, fallback=__main__.UserDefined]'
reveal_type(UserDefined(Default())) # E: Revealed type is 'Tuple[__main__.Default, fallback=__main__.UserDefined]'
UserDefined(1) # E: Argument 1 to "UserDefined" has incompatible type "int"; expected "Default"

[builtins fixtures/list.pyi]

[case testNewNamedTupleWithDefaultsStrictOptional]
# flags: --fast-parser --strict-optional --python-version 3.6
from typing import List, NamedTuple, Optional

class HasNone(NamedTuple):
x: int
y: Optional[int] = None

reveal_type(HasNone(1)) # E: Revealed type is 'Tuple[builtins.int, Union[builtins.int, builtins.None], fallback=__main__.HasNone]'
HasNone(None) # E: Argument 1 to "HasNone" has incompatible type None; expected "int"
HasNone(1, y=None)
HasNone(1, y=2)

class CannotBeNone(NamedTuple):
x: int
y: int = None # E: Incompatible types in assignment (expression has type None, variable has type "int")

[builtins fixtures/list.pyi]

[case testNewNamedTupleWrongType]
# flags: --fast-parser --python-version 3.6
from typing import NamedTuple

class X(NamedTuple):
x: int
y: int = 'not an int' # E: Incompatible types in assignment (expression has type "str", variable has type "int")

[case testNewNamedTupleErrorInDefault]
# flags: --fast-parser --python-version 3.6
from typing import NamedTuple

class X(NamedTuple):
x: int = 1 + '1' # E: Unsupported operand types for + ("int" and "str")

[case testNewNamedTupleInheritance]
# flags: --fast-parser --python-version 3.6
from typing import cast, NamedTuple

class X(NamedTuple):
x: str
y: int = 3

class Y(X):
def method(self) -> str:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test calling base class __init__ in a subclass of a named tuple.

return self.x + cast(str, self.y)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I don't like casts of unsafe expressions in tests, but formally this test works as it should.
Maybe you could modify it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll just change it to access self.y without using it. I did str(self.y) first but that doesn't work because the fixture doesn't have it.


reveal_type(Y('a')) # E: Revealed type is 'Tuple[builtins.str, builtins.int, fallback=__main__.Y]'
Y(y=1, x='1').method()