Skip to content

Commit 5f0d02c

Browse files
ilevkivskyiddfisher
authored andcommitted
Implement PEP 526 Variable Annotations Syntax (#2131)
Depends on python/typed_ast#16 for the new syntax, but is backwards compatible otherwise.
1 parent c752b42 commit 5f0d02c

File tree

5 files changed

+129
-11
lines changed

5 files changed

+129
-11
lines changed

mypy/checker.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1030,7 +1030,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> Type:
10301030
10311031
Handle all kinds of assignment statements (simple, indexed, multiple).
10321032
"""
1033-
self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None)
1033+
self.check_assignment(s.lvalues[-1], s.rvalue, s.type is None, s.new_syntax)
10341034

10351035
if len(s.lvalues) > 1:
10361036
# Chained assignment (e.g. x = y = ...).
@@ -1041,7 +1041,8 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> Type:
10411041
for lv in s.lvalues[:-1]:
10421042
self.check_assignment(lv, rvalue, s.type is None)
10431043

1044-
def check_assignment(self, lvalue: Node, rvalue: Node, infer_lvalue_type: bool = True) -> None:
1044+
def check_assignment(self, lvalue: Node, rvalue: Node, infer_lvalue_type: bool = True,
1045+
new_syntax: bool = False) -> None:
10451046
"""Type check a single assignment: lvalue = rvalue."""
10461047
if isinstance(lvalue, TupleExpr) or isinstance(lvalue, ListExpr):
10471048
self.check_assignment_to_multiple_lvalues(lvalue.items, rvalue, lvalue,
@@ -1078,7 +1079,8 @@ def check_assignment(self, lvalue: Node, rvalue: Node, infer_lvalue_type: bool =
10781079
elif (is_literal_none(rvalue) and
10791080
isinstance(lvalue, NameExpr) and
10801081
isinstance(lvalue.node, Var) and
1081-
lvalue.node.is_initialized_in_class):
1082+
lvalue.node.is_initialized_in_class and
1083+
not new_syntax):
10821084
# Allow None's to be assigned to class variables with non-Optional types.
10831085
rvalue_type = lvalue_type
10841086
else:

mypy/fastparse.py

+22-7
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
UnaryExpr, FuncExpr, ComparisonExpr,
1515
StarExpr, YieldFromExpr, NonlocalDecl, DictionaryComprehension,
1616
SetComprehension, ComplexExpr, EllipsisExpr, YieldExpr, Argument,
17-
AwaitExpr,
17+
AwaitExpr, TempNode,
1818
ARG_POS, ARG_OPT, ARG_STAR, ARG_NAMED, ARG_STAR2
1919
)
2020
from mypy.types import (
@@ -403,16 +403,31 @@ def visit_Delete(self, n: ast35.Delete) -> Node:
403403
else:
404404
return DelStmt(self.visit(n.targets[0]))
405405

406-
# Assign(expr* targets, expr value, string? type_comment)
406+
# Assign(expr* targets, expr? value, string? type_comment, expr? annotation)
407407
@with_line
408408
def visit_Assign(self, n: ast35.Assign) -> Node:
409409
typ = None
410-
if n.type_comment:
410+
if hasattr(n, 'annotation') and n.annotation is not None: # type: ignore
411+
new_syntax = True
412+
else:
413+
new_syntax = False
414+
if new_syntax and self.pyversion < (3, 6):
415+
raise TypeCommentParseError('Variable annotation syntax is only '
416+
'suppoted in Python 3.6, use type '
417+
'comment instead', n.lineno, n.col_offset)
418+
# typed_ast prevents having both type_comment and annotation.
419+
if n.type_comment is not None:
411420
typ = parse_type_comment(n.type_comment, n.lineno)
412-
413-
return AssignmentStmt(self.visit_list(n.targets),
414-
self.visit(n.value),
415-
type=typ)
421+
elif new_syntax:
422+
typ = TypeConverter(line=n.lineno).visit(n.annotation) # type: ignore
423+
if n.value is None: # always allow 'x: int'
424+
rvalue = TempNode(AnyType()) # type: Node
425+
else:
426+
rvalue = self.visit(n.value)
427+
lvalues = self.visit_list(n.targets)
428+
return AssignmentStmt(lvalues,
429+
rvalue,
430+
type=typ, new_syntax=new_syntax)
416431

417432
# AugAssign(expr target, operator op, expr value)
418433
@with_line

mypy/nodes.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -779,12 +779,15 @@ class AssignmentStmt(Statement):
779779
rvalue = None # type: Expression
780780
# Declared type in a comment, may be None.
781781
type = None # type: mypy.types.Type
782+
# This indicates usage of PEP 526 type annotation syntax in assignment.
783+
new_syntax = False # type: bool
782784

783785
def __init__(self, lvalues: List[Expression], rvalue: Expression,
784-
type: 'mypy.types.Type' = None) -> None:
786+
type: 'mypy.types.Type' = None, new_syntax: bool = False) -> None:
785787
self.lvalues = lvalues
786788
self.rvalue = rvalue
787789
self.type = type
790+
self.new_syntax = new_syntax
788791

789792
def accept(self, visitor: NodeVisitor[T]) -> T:
790793
return visitor.visit_assignment_stmt(self)
@@ -1786,6 +1789,9 @@ class TempNode(Expression):
17861789
def __init__(self, typ: 'mypy.types.Type') -> None:
17871790
self.type = typ
17881791

1792+
def __repr__(self):
1793+
return 'TempNode(%s)' % str(self.type)
1794+
17891795
def accept(self, visitor: NodeVisitor[T]) -> T:
17901796
return visitor.visit_temp_node(self)
17911797

mypy/test/testcheck.py

+5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import shutil
66
import sys
77
import time
8+
import typed_ast
9+
import typed_ast.ast35
810

911
from typing import Tuple, List, Dict, Set
1012

@@ -68,6 +70,9 @@
6870
'check-columns.test',
6971
]
7072

73+
if 'annotation' in typed_ast.ast35.Assign._fields:
74+
files.append('check-newsyntax.test')
75+
7176

7277
class TypeCheckSuite(DataSuite):
7378
def __init__(self, *, update_data=False):

test-data/unit/check-newsyntax.test

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
[case testNewSyntaxRequire36]
2+
# flags: --fast-parser --python-version 3.5
3+
x: int = 5 # E: Variable annotation syntax is only suppoted in Python 3.6, use type comment instead
4+
[out]
5+
6+
[case testNewSyntaxSyntaxError]
7+
# flags: --fast-parser --python-version 3.6
8+
x: int: int # E: invalid syntax
9+
[out]
10+
11+
[case testNewSyntaxBasics]
12+
# flags: --fast-parser --python-version 3.6
13+
x: int
14+
x = 5
15+
y: int = 5
16+
17+
a: str
18+
a = 5 # E: Incompatible types in assignment (expression has type "int", variable has type "str")
19+
b: str = 5 # E: Incompatible types in assignment (expression has type "int", variable has type "str")
20+
21+
zzz: int
22+
zzz: str # E: Name 'zzz' already defined
23+
[out]
24+
25+
[case testNewSyntaxWithDict]
26+
# flags: --fast-parser --python-version 3.6
27+
from typing import Dict, Any
28+
29+
d: Dict[int, str] = {}
30+
d[42] = 'ab'
31+
d[42] = 42 # E: Incompatible types in assignment (expression has type "int", target has type "str")
32+
d['ab'] = 'ab' # E: Invalid index type "str" for "dict"
33+
[builtins fixtures/dict.pyi]
34+
[out]
35+
36+
[case testNewSyntaxWithRevealType]
37+
# flags: --fast-parser --python-version 3.6
38+
from typing import Dict
39+
40+
def tst_local(dct: Dict[int, T]) -> Dict[T, int]:
41+
ret: Dict[T, int] = {}
42+
return ret
43+
44+
reveal_type(tst_local({1: 'a'})) # E: Revealed type is 'builtins.dict[builtins.str*, builtins.int]'
45+
[builtins fixtures/dict.pyi]
46+
[out]
47+
48+
[case testNewSyntaxWithInstanceVars]
49+
# flags: --fast-parser --python-version 3.6
50+
class TstInstance:
51+
a: str
52+
def __init__(self) -> None:
53+
self.x: int
54+
55+
TstInstance().x = 5
56+
TstInstance().x = 'ab' # E: Incompatible types in assignment (expression has type "str", variable has type "int")
57+
TstInstance().a = 5 # E: Incompatible types in assignment (expression has type "int", variable has type "str")
58+
TstInstance().a = 'ab'
59+
[out]
60+
61+
[case testNewSyntaxWithClassVars]
62+
# flags: --fast-parser --strict-optional --python-version 3.6
63+
class CCC:
64+
a: str = None # E: Incompatible types in assignment (expression has type None, variable has type "str")
65+
[out]
66+
main: note: In class "CCC":
67+
68+
[case testNewSyntaxWithStrictOptional]
69+
# flags: --fast-parser --strict-optional --python-version 3.6
70+
strict: int
71+
strict = None # E: Incompatible types in assignment (expression has type None, variable has type "int")
72+
strict2: int = None # E: Incompatible types in assignment (expression has type None, variable has type "int")
73+
[out]
74+
75+
[case testNewSyntaxWithStrictOptionalFunctions]
76+
# flags: --fast-parser --strict-optional --python-version 3.6
77+
def f() -> None:
78+
x: int
79+
x = None # E: Incompatible types in assignment (expression has type None, variable has type "int")
80+
[out]
81+
main: note: In function "f":
82+
83+
[case testNewSyntaxWithStrictOptionalClasses]
84+
# flags: --fast-parser --strict-optional --python-version 3.6
85+
class C:
86+
def meth(self) -> None:
87+
x: int = None # E: Incompatible types in assignment (expression has type None, variable has type "int")
88+
self.x: int = None # E: Incompatible types in assignment (expression has type None, variable has type "int")
89+
[out]
90+
main: note: In member "meth" of class "C":

0 commit comments

Comments
 (0)