Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Implement new NamedTuple syntax
  • Loading branch information
ilevkivskyi committed Oct 13, 2016
commit 409abb668c5a2966a15790bfa7867b725e2a895f
51 changes: 49 additions & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
ImportFrom, ImportAll, Block, LDEF, NameExpr, MemberExpr,
IndexExpr, TupleExpr, ListExpr, ExpressionStmt, ReturnStmt,
RaiseStmt, AssertStmt, OperatorAssignmentStmt, WhileStmt,
ForStmt, BreakStmt, ContinueStmt, IfStmt, TryStmt, WithStmt, DelStmt,
ForStmt, BreakStmt, ContinueStmt, IfStmt, TryStmt, WithStmt, DelStmt, PassStmt,
GlobalDecl, SuperExpr, DictExpr, CallExpr, RefExpr, OpExpr, UnaryExpr,
SliceExpr, CastExpr, RevealTypeExpr, TypeApplication, Context, SymbolTable,
SymbolTableNode, BOUND_TVAR, UNBOUND_TVAR, ListComprehension, GeneratorExpr,
Expand All @@ -63,7 +63,7 @@
YieldFromExpr, NamedTupleExpr, NonlocalDecl, SymbolNode,
SetComprehension, DictionaryComprehension, TYPE_ALIAS, TypeAliasExpr,
YieldExpr, ExecStmt, Argument, BackquoteExpr, ImportBase, AwaitExpr,
IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr,
IntExpr, FloatExpr, UnicodeExpr, EllipsisExpr, TempNode,
COVARIANT, CONTRAVARIANT, INVARIANT, UNBOUND_IMPORTED, LITERAL_YES,
)
from mypy.visitor import NodeVisitor
Expand Down Expand Up @@ -545,7 +545,54 @@ def check_function_signature(self, fdef: FuncItem) -> None:
elif len(sig.arg_types) > len(fdef.arguments):
self.fail('Type signature has too many arguments', fdef, blocker=True)

def check_namedtuple_classdef(self, defn: ClassDef) -> Tuple[List[str], List[Type]]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Believe it or not, mypy's (loose) convention for helper functions is that they follow the main (or only) call site.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Talking about placement, it would be nice to have it closer to other namedtuple-handling functions (~ 1600)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@elazarg 1600 is probably too far. I put it in the same place where other ClassDef helpers are (~ 750).

if self.options.python_version < (3, 6):
self.fail('NamedTuple class syntax is only supported in Python 3.6', defn)
return [], []
if len(defn.base_type_exprs) > 1:
self.fail('NamedTuple should be a single base', defn)
return [], []
items = [] # type: List[str]
types = [] # type: List[Type]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It looks hard to prove (and preserve in future edits) that these have the same length. Maybe make a list of tuples and separate them at the end using list(zip(*x))?

@elazarg elazarg Oct 15, 2016 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree, and this should be uniform in other places (e.g. parse_namedtuple_fields_with_types). In my PR I did not refactor it, trying to stay somewhat closer to the original implementation, A namedtuple Field would add to readability: instead of returning Tuple[List[str], List[Type], bool] we can return Tuple[List[Field], bool]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

list(zip(*x)) will require a special-casing for empty namedtuples, also mypy complains about its own code (probably because of not very precise stub for zip). I refactored this part, so that it is clear that the two lists have always equal length (there are only two appends now that stay next to each other) and added comments.

for stmt in defn.defs.body:
if isinstance(stmt, AssignmentStmt):
if len(stmt.lvalues) > 1 or not isinstance(stmt.lvalues[0], NameExpr):
self.fail('NamedTuple supports only simple fields', stmt)
return [], []
name = stmt.lvalues[0].name
if not isinstance(stmt.rvalue, TempNode):
# x: int assigns rvalue to TempNode(AnyType())
self.fail('NamedTuple does not support initial values', stmt)
if stmt.type is None:
self.fail('NamedTuple field type must be specified', stmt)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When you get this the previous fail() also triggers, right? I think that if someone writes x = 1 they deserve to get a single error that explains why that's not allowed -- they may not even realize you see this as an "initial value".

(Also, what happens when they write x = 1 # type: int ?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

OK. I changed this so that a single "generic" ... expected "field_name: field_type" error is displayed (also for x = 1 # type: int, since this will not work in typing.py). A more specific error Right hand side values are not supported in NamedTuple is displayed for x: int = 1 (the generic error could be not completely clear in this case).

types.append(AnyType())
else:
types.append(self.anal_type(stmt.type))
if name.startswith('_'):
self.fail('NamedTuple field name cannot start with an underscore: {}'
.format(name), stmt)
items.append(name)
else:
if not isinstance(stmt, PassStmt):
self.fail('Invalid definition in NamedTuple', stmt)
return items, types

def analyze_namedtuple_classdef(self, defn: ClassDef) -> None:
node = self.lookup(defn.name, defn)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Careful, this may return None.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed.

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)

def visit_class_def(self, defn: ClassDef) -> None:
# special case for NamedTuple

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd refactor this into a helper function. Also it probably should be called after clean_up_bases_and_infer_type_variables().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed.

for base_expr in defn.base_type_exprs:
if isinstance(base_expr, NameExpr):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hm, this should probably also check for MemberExpr (similar to what's happening in check_namedtuple()).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed.

sym = self.lookup_qualified(base_expr.name, base_expr)
if sym is None or sym.node is None:
continue
if sym.node.fullname() == 'typing.NamedTuple':
self.analyze_namedtuple_classdef(defn)
return
self.clean_up_bases_and_infer_type_variables(defn)
self.setup_class_def_analysis(defn)

Expand Down
36 changes: 15 additions & 21 deletions test-data/unit/check-class-namedtuple.test
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@
# flags: --fast-parser --python-version 3.5
from typing import NamedTuple

class E(NamedTuple): # E: NamedTuple class syntax is only supported in Python 3.6+
x: int
y: str
class E(NamedTuple): # E: NamedTuple class syntax is only supported in Python 3.6
pass

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

class X(NamedTuple): # E: NamedTuple field names cannot start with an underscore: _y, _z
class X(NamedTuple):
x: int
_y: int
_z: int
_y: int # E: NamedTuple field name cannot start with an underscore: _y
_z: int # E: NamedTuple field name cannot start with an underscore: _z

[case testNewNamedTupleAccessingAttributes]
# flags: --fast-parser --python-version 3.6
Expand All @@ -34,18 +33,14 @@ from typing import NamedTuple

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

x: X
x.x = 5 # E: Property "x" defined in "X" is read-only
x.y = 5 # E: Property "y" defined in "X" is read-only
x.z = 5 # E: "X" has no attribute "z"
x.y = 5 # E: "X" has no attribute "y"

class A(X): pass
a: A
a.x = 5 # E: Property "x" defined in "A" is read-only
a.y = 5 # E: Property "y" defined in "A" is read-only
-- a.z = 5 # not supported yet

[case testNewNamedTupleCreateWithPositionalArguments]
# flags: --fast-parser --python-version 3.6
Expand All @@ -55,11 +50,11 @@ class X(NamedTuple):
x: int
y: str

x = X(1, 'x')
x = X(1, '2')
x.x
x.z # E: "X" has no attribute "z"
x = X(1) # E: Too few arguments for "X"
x = X(1, 2, 3) # E: Too many arguments for "X"
x = X(1, '2', 3) # E: Too many arguments for "X"

[case testCreateNewNamedTupleWithKeywordArguments]
# flags: --fast-parser --python-version 3.6
Expand All @@ -72,7 +67,7 @@ class X(NamedTuple):
x = X(x=1, y='x')
x = X(1, y='x')
x = X(x=1, z=1) # E: Unexpected keyword argument "z" for "X"
x = X(y=1) # E: Missing positional argument "x" in call to "X"
x = X(y='x') # E: Missing positional argument "x" in call to "X"

[case testNewNamedTupleCreateAndUseAsTuple]
# flags: --fast-parser --python-version 3.6
Expand Down Expand Up @@ -227,15 +222,14 @@ MyNamedTuple.x # E: "MyNamedTuple" has no attribute "x"
from typing import NamedTuple

class A(NamedTuple):
...
pass

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

class A(NamedTuple):
a: int
b: str

class B(A):
@property
Expand Down Expand Up @@ -288,7 +282,7 @@ reveal_type(X._fields) # E: Revealed type is 'Tuple[builtins.str, builtins.str]
from typing import NamedTuple

class X(NamedTuple):
...
pass

x: X = X()
x._replace()
Expand Down Expand Up @@ -329,7 +323,7 @@ from typing import NamedTuple
class X(NamedTuple):
x: int
y: str
def f(self): pass # E: NamedTuple only allows names as fields
def f(self): pass # E: Invalid definition in NamedTuple

[case testNewNamedTupleWithInvalidItems2]
# flags: --fast-parser --python-version 3.6
Expand All @@ -338,13 +332,13 @@ from typing import NamedTuple
class X(NamedTuple):
x: int
y: str
[0][0]: int # E: NamedTuple only allows names as fields
x[0]: int # E: NamedTuple supports only simple fields

[builtins fixtures/list.pyi]

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

class N(NamedTuple):
x: int
Expand All @@ -355,4 +349,4 @@ def f(a: Type[N]):
[builtins fixtures/list.pyi]
[out]
main: note: In function "f":
main:3: error: Unsupported type Type["N"]
main:8: error: Unsupported type Type["N"]