summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorYury Selivanov <yury@magic.io>2016-09-09 03:50:03 (GMT)
committerYury Selivanov <yury@magic.io>2016-09-09 03:50:03 (GMT)
commitf8cb8a16a344ab208fd46876c4b63604987347b8 (patch)
treec44caa48291401d1e1e388004d2762513ac88c93 /Lib
parent09ad17810c38d1aaae02de69084dd2a8ad9f5cdb (diff)
downloadcpython-f8cb8a16a344ab208fd46876c4b63604987347b8.zip
cpython-f8cb8a16a344ab208fd46876c4b63604987347b8.tar.gz
cpython-f8cb8a16a344ab208fd46876c4b63604987347b8.tar.bz2
Issue #27985: Implement PEP 526 -- Syntax for Variable Annotations.
Patch by Ivan Levkivskyi.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/importlib/_bootstrap_external.py4
-rw-r--r--Lib/lib2to3/Grammar.txt5
-rw-r--r--Lib/lib2to3/tests/test_parser.py30
-rw-r--r--Lib/opcode.py3
-rwxr-xr-xLib/symbol.py137
-rw-r--r--Lib/symtable.py5
-rw-r--r--Lib/test/ann_module.py53
-rw-r--r--Lib/test/ann_module2.py36
-rw-r--r--Lib/test/ann_module3.py18
-rw-r--r--Lib/test/pydoc_mod.py2
-rw-r--r--Lib/test/test___all__.py2
-rw-r--r--Lib/test/test_dis.py33
-rw-r--r--Lib/test/test_grammar.py178
-rw-r--r--Lib/test/test_opcodes.py27
-rw-r--r--Lib/test/test_parser.py39
-rw-r--r--Lib/test/test_pydoc.py4
-rw-r--r--Lib/test/test_symtable.py11
-rw-r--r--Lib/test/test_tools/test_com2ann.py260
-rw-r--r--Lib/test/test_typing.py83
-rw-r--r--Lib/typing.py146
20 files changed, 987 insertions, 89 deletions
diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py
index 828246c..ffb9325 100644
--- a/Lib/importlib/_bootstrap_external.py
+++ b/Lib/importlib/_bootstrap_external.py
@@ -234,6 +234,8 @@ _code_type = type(_write_atomic.__code__)
# Python 3.6a1 3372 (MAKE_FUNCTION simplification, remove MAKE_CLOSURE
# #27095)
# Python 3.6b1 3373 (add BUILD_STRING opcode #27078)
+# Python 3.6b1 3375 (add SETUP_ANNOTATIONS and STORE_ANNOTATION opcodes
+# #27985)
#
# MAGIC must change whenever the bytecode emitted by the compiler may no
# longer be understood by older implementations of the eval loop (usually
@@ -242,7 +244,7 @@ _code_type = type(_write_atomic.__code__)
# Whenever MAGIC_NUMBER is changed, the ranges in the magic_values array
# in PC/launcher.c must also be updated.
-MAGIC_NUMBER = (3373).to_bytes(2, 'little') + b'\r\n'
+MAGIC_NUMBER = (3375).to_bytes(2, 'little') + b'\r\n'
_RAW_MAGIC_NUMBER = int.from_bytes(MAGIC_NUMBER, 'little') # For import.c
_PYCACHE = '__pycache__'
diff --git a/Lib/lib2to3/Grammar.txt b/Lib/lib2to3/Grammar.txt
index c954669..abe1268 100644
--- a/Lib/lib2to3/Grammar.txt
+++ b/Lib/lib2to3/Grammar.txt
@@ -54,12 +54,13 @@ stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | print_stmt | del_stmt | pass_stmt | flow_stmt |
import_stmt | global_stmt | exec_stmt | assert_stmt)
-expr_stmt: testlist_star_expr (augassign (yield_expr|testlist) |
+expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
('=' (yield_expr|testlist_star_expr))*)
+annassign: ':' test ['=' test]
testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [',']
augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
'<<=' | '>>=' | '**=' | '//=')
-# For normal assignments, additional restrictions enforced by the interpreter
+# For normal and annotated assignments, additional restrictions enforced by the interpreter
print_stmt: 'print' ( [ test (',' test)* [','] ] |
'>>' test [ (',' test)+ [','] ] )
del_stmt: 'del' exprlist
diff --git a/Lib/lib2to3/tests/test_parser.py b/Lib/lib2to3/tests/test_parser.py
index 9adb031..b378163 100644
--- a/Lib/lib2to3/tests/test_parser.py
+++ b/Lib/lib2to3/tests/test_parser.py
@@ -237,6 +237,36 @@ class TestFunctionAnnotations(GrammarTest):
self.validate(s)
+# Adapted from Python 3's Lib/test/test_grammar.py:GrammarTests.test_var_annot
+class TestFunctionAnnotations(GrammarTest):
+ def test_1(self):
+ self.validate("var1: int = 5")
+
+ def test_2(self):
+ self.validate("var2: [int, str]")
+
+ def test_3(self):
+ self.validate("def f():\n"
+ " st: str = 'Hello'\n"
+ " a.b: int = (1, 2)\n"
+ " return st\n")
+
+ def test_4(self):
+ self.validate("def fbad():\n"
+ " x: int\n"
+ " print(x)\n")
+
+ def test_5(self):
+ self.validate("class C:\n"
+ " x: int\n"
+ " s: str = 'attr'\n"
+ " z = 2\n"
+ " def __init__(self, x):\n"
+ " self.x: int = x\n")
+
+ def test_6(self):
+ self.validate("lst: List[int] = []")
+
class TestExcept(GrammarTest):
def test_new(self):
s = """
diff --git a/Lib/opcode.py b/Lib/opcode.py
index d9202e8..31d1534 100644
--- a/Lib/opcode.py
+++ b/Lib/opcode.py
@@ -119,7 +119,7 @@ def_op('WITH_CLEANUP_FINISH', 82)
def_op('RETURN_VALUE', 83)
def_op('IMPORT_STAR', 84)
-
+def_op('SETUP_ANNOTATIONS', 85)
def_op('YIELD_VALUE', 86)
def_op('POP_BLOCK', 87)
def_op('END_FINALLY', 88)
@@ -169,6 +169,7 @@ def_op('STORE_FAST', 125) # Local variable number
haslocal.append(125)
def_op('DELETE_FAST', 126) # Local variable number
haslocal.append(126)
+name_op('STORE_ANNOTATION', 127) # Index in name list
def_op('RAISE_VARARGS', 130) # Number of raise arguments (1, 2, or 3)
def_op('CALL_FUNCTION', 131) # #args + (#kwargs << 8)
diff --git a/Lib/symbol.py b/Lib/symbol.py
index 7541497..d9f01e0 100755
--- a/Lib/symbol.py
+++ b/Lib/symbol.py
@@ -27,74 +27,75 @@ stmt = 269
simple_stmt = 270
small_stmt = 271
expr_stmt = 272
-testlist_star_expr = 273
-augassign = 274
-del_stmt = 275
-pass_stmt = 276
-flow_stmt = 277
-break_stmt = 278
-continue_stmt = 279
-return_stmt = 280
-yield_stmt = 281
-raise_stmt = 282
-import_stmt = 283
-import_name = 284
-import_from = 285
-import_as_name = 286
-dotted_as_name = 287
-import_as_names = 288
-dotted_as_names = 289
-dotted_name = 290
-global_stmt = 291
-nonlocal_stmt = 292
-assert_stmt = 293
-compound_stmt = 294
-async_stmt = 295
-if_stmt = 296
-while_stmt = 297
-for_stmt = 298
-try_stmt = 299
-with_stmt = 300
-with_item = 301
-except_clause = 302
-suite = 303
-test = 304
-test_nocond = 305
-lambdef = 306
-lambdef_nocond = 307
-or_test = 308
-and_test = 309
-not_test = 310
-comparison = 311
-comp_op = 312
-star_expr = 313
-expr = 314
-xor_expr = 315
-and_expr = 316
-shift_expr = 317
-arith_expr = 318
-term = 319
-factor = 320
-power = 321
-atom_expr = 322
-atom = 323
-testlist_comp = 324
-trailer = 325
-subscriptlist = 326
-subscript = 327
-sliceop = 328
-exprlist = 329
-testlist = 330
-dictorsetmaker = 331
-classdef = 332
-arglist = 333
-argument = 334
-comp_iter = 335
-comp_for = 336
-comp_if = 337
-encoding_decl = 338
-yield_expr = 339
-yield_arg = 340
+annassign = 273
+testlist_star_expr = 274
+augassign = 275
+del_stmt = 276
+pass_stmt = 277
+flow_stmt = 278
+break_stmt = 279
+continue_stmt = 280
+return_stmt = 281
+yield_stmt = 282
+raise_stmt = 283
+import_stmt = 284
+import_name = 285
+import_from = 286
+import_as_name = 287
+dotted_as_name = 288
+import_as_names = 289
+dotted_as_names = 290
+dotted_name = 291
+global_stmt = 292
+nonlocal_stmt = 293
+assert_stmt = 294
+compound_stmt = 295
+async_stmt = 296
+if_stmt = 297
+while_stmt = 298
+for_stmt = 299
+try_stmt = 300
+with_stmt = 301
+with_item = 302
+except_clause = 303
+suite = 304
+test = 305
+test_nocond = 306
+lambdef = 307
+lambdef_nocond = 308
+or_test = 309
+and_test = 310
+not_test = 311
+comparison = 312
+comp_op = 313
+star_expr = 314
+expr = 315
+xor_expr = 316
+and_expr = 317
+shift_expr = 318
+arith_expr = 319
+term = 320
+factor = 321
+power = 322
+atom_expr = 323
+atom = 324
+testlist_comp = 325
+trailer = 326
+subscriptlist = 327
+subscript = 328
+sliceop = 329
+exprlist = 330
+testlist = 331
+dictorsetmaker = 332
+classdef = 333
+arglist = 334
+argument = 335
+comp_iter = 336
+comp_for = 337
+comp_if = 338
+encoding_decl = 339
+yield_expr = 340
+yield_arg = 341
#--end constants--
sym_name = {}
diff --git a/Lib/symtable.py b/Lib/symtable.py
index 84fec4a..b0e5260 100644
--- a/Lib/symtable.py
+++ b/Lib/symtable.py
@@ -2,7 +2,7 @@
import _symtable
from _symtable import (USE, DEF_GLOBAL, DEF_LOCAL, DEF_PARAM,
- DEF_IMPORT, DEF_BOUND, SCOPE_OFF, SCOPE_MASK, FREE,
+ DEF_IMPORT, DEF_BOUND, DEF_ANNOT, SCOPE_OFF, SCOPE_MASK, FREE,
LOCAL, GLOBAL_IMPLICIT, GLOBAL_EXPLICIT, CELL)
import weakref
@@ -190,6 +190,9 @@ class Symbol(object):
def is_local(self):
return bool(self.__flags & DEF_BOUND)
+ def is_annotated(self):
+ return bool(self.__flags & DEF_ANNOT)
+
def is_free(self):
return bool(self.__scope == FREE)
diff --git a/Lib/test/ann_module.py b/Lib/test/ann_module.py
new file mode 100644
index 0000000..9e6b87d
--- /dev/null
+++ b/Lib/test/ann_module.py
@@ -0,0 +1,53 @@
+
+
+"""
+The module for testing variable annotations.
+Empty lines above are for good reason (testing for correct line numbers)
+"""
+
+from typing import Optional
+
+__annotations__[1] = 2
+
+class C:
+
+ x = 5; y: Optional['C'] = None
+
+from typing import Tuple
+x: int = 5; y: str = x; f: Tuple[int, int]
+
+class M(type):
+
+ __annotations__['123'] = 123
+ o: type = object
+
+(pars): bool = True
+
+class D(C):
+ j: str = 'hi'; k: str= 'bye'
+
+from types import new_class
+h_class = new_class('H', (C,))
+j_class = new_class('J')
+
+class F():
+ z: int = 5
+ def __init__(self, x):
+ pass
+
+class Y(F):
+ def __init__(self):
+ super(F, self).__init__(123)
+
+class Meta(type):
+ def __new__(meta, name, bases, namespace):
+ return super().__new__(meta, name, bases, namespace)
+
+class S(metaclass = Meta):
+ x: str = 'something'
+ y: str = 'something else'
+
+def foo(x: int = 10):
+ def bar(y: List[str]):
+ x: str = 'yes'
+ bar()
diff --git a/Lib/test/ann_module2.py b/Lib/test/ann_module2.py
new file mode 100644
index 0000000..76cf5b3
--- /dev/null
+++ b/Lib/test/ann_module2.py
@@ -0,0 +1,36 @@
+"""
+Some correct syntax for variable annotation here.
+More examples are in test_grammar and test_parser.
+"""
+
+from typing import no_type_check, ClassVar
+
+i: int = 1
+j: int
+x: float = i/10
+
+def f():
+ class C: ...
+ return C()
+
+f().new_attr: object = object()
+
+class C:
+ def __init__(self, x: int) -> None:
+ self.x = x
+
+c = C(5)
+c.new_attr: int = 10
+
+__annotations__ = {}
+
+
+@no_type_check
+class NTC:
+ def meth(self, param: complex) -> None:
+ ...
+
+class CV:
+ var: ClassVar['CV']
+
+CV.var = CV()
diff --git a/Lib/test/ann_module3.py b/Lib/test/ann_module3.py
new file mode 100644
index 0000000..eccd7be
--- /dev/null
+++ b/Lib/test/ann_module3.py
@@ -0,0 +1,18 @@
+"""
+Correct syntax for variable annotation that should fail at runtime
+in a certain manner. More examples are in test_grammar and test_parser.
+"""
+
+def f_bad_ann():
+ __annotations__[1] = 2
+
+class C_OK:
+ def __init__(self, x: int) -> None:
+ self.x: no_such_name = x # This one is OK as proposed by Guido
+
+class D_bad_ann:
+ def __init__(self, x: int) -> None:
+ sfel.y: int = 0
+
+def g_bad_ann():
+ no_such_name.attr: int = 0
diff --git a/Lib/test/pydoc_mod.py b/Lib/test/pydoc_mod.py
index cda1c9e..9c1fff5 100644
--- a/Lib/test/pydoc_mod.py
+++ b/Lib/test/pydoc_mod.py
@@ -12,7 +12,7 @@ class A:
pass
class B(object):
- NO_MEANING = "eggs"
+ NO_MEANING: str = "eggs"
pass
class C(object):
diff --git a/Lib/test/test___all__.py b/Lib/test/test___all__.py
index e94d984..ae9114e 100644
--- a/Lib/test/test___all__.py
+++ b/Lib/test/test___all__.py
@@ -38,6 +38,8 @@ class AllTest(unittest.TestCase):
modname, e.__class__.__name__, e))
if "__builtins__" in names:
del names["__builtins__"]
+ if '__annotations__' in names:
+ del names['__annotations__']
keys = set(names)
all_list = sys.modules[modname].__all__
all_set = set(all_list)
diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py
index 09e68ce..6081073 100644
--- a/Lib/test/test_dis.py
+++ b/Lib/test/test_dis.py
@@ -207,6 +207,38 @@ dis_simple_stmt_str = """\
10 RETURN_VALUE
"""
+annot_stmt_str = """\
+
+x: int = 1
+y: fun(1)
+lst[fun(0)]: int = 1
+"""
+# leading newline is for a reason (tests lineno)
+
+dis_annot_stmt_str = """\
+ 2 0 SETUP_ANNOTATIONS
+ 2 LOAD_CONST 0 (1)
+ 4 STORE_NAME 0 (x)
+ 6 LOAD_NAME 1 (int)
+ 8 STORE_ANNOTATION 0 (x)
+
+ 3 10 LOAD_NAME 2 (fun)
+ 12 LOAD_CONST 0 (1)
+ 14 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
+ 16 STORE_ANNOTATION 3 (y)
+
+ 4 18 LOAD_CONST 0 (1)
+ 20 LOAD_NAME 4 (lst)
+ 22 LOAD_NAME 2 (fun)
+ 24 LOAD_CONST 1 (0)
+ 26 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
+ 28 STORE_SUBSCR
+ 30 LOAD_NAME 1 (int)
+ 32 POP_TOP
+ 34 LOAD_CONST 2 (None)
+ 36 RETURN_VALUE
+"""
+
compound_stmt_str = """\
x = 0
while 1:
@@ -345,6 +377,7 @@ class DisTests(unittest.TestCase):
def test_disassemble_str(self):
self.do_disassembly_test(expr_str, dis_expr_str)
self.do_disassembly_test(simple_stmt_str, dis_simple_stmt_str)
+ self.do_disassembly_test(annot_stmt_str, dis_annot_stmt_str)
self.do_disassembly_test(compound_stmt_str, dis_compound_stmt_str)
def test_disassemble_bytes(self):
diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py
index bfe5225..109013f 100644
--- a/Lib/test/test_grammar.py
+++ b/Lib/test/test_grammar.py
@@ -8,6 +8,14 @@ import sys
# testing import *
from sys import *
+# different import patterns to check that __annotations__ does not interfere
+# with import machinery
+import test.ann_module as ann_module
+import typing
+from collections import ChainMap
+from test import ann_module2
+import test
+
class TokenTests(unittest.TestCase):
@@ -139,6 +147,19 @@ the \'lazy\' dog.\n\
compile(s, "<test>", "exec")
self.assertIn("unexpected EOF", str(cm.exception))
+var_annot_global: int # a global annotated is necessary for test_var_annot
+
+# custom namespace for testing __annotations__
+
+class CNS:
+ def __init__(self):
+ self._dct = {}
+ def __setitem__(self, item, value):
+ self._dct[item.lower()] = value
+ def __getitem__(self, item):
+ return self._dct[item]
+
+
class GrammarTests(unittest.TestCase):
# single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
@@ -154,6 +175,163 @@ class GrammarTests(unittest.TestCase):
# testlist ENDMARKER
x = eval('1, 0 or 1')
+ def test_var_annot_basics(self):
+ # all these should be allowed
+ var1: int = 5
+ var2: [int, str]
+ my_lst = [42]
+ def one():
+ return 1
+ int.new_attr: int
+ [list][0]: type
+ my_lst[one()-1]: int = 5
+ self.assertEqual(my_lst, [5])
+
+ def test_var_annot_syntax_errors(self):
+ # parser pass
+ check_syntax_error(self, "def f: int")
+ check_syntax_error(self, "x: int: str")
+ check_syntax_error(self, "def f():\n"
+ " nonlocal x: int\n")
+ # AST pass
+ check_syntax_error(self, "[x, 0]: int\n")
+ check_syntax_error(self, "f(): int\n")
+ check_syntax_error(self, "(x,): int")
+ check_syntax_error(self, "def f():\n"
+ " (x, y): int = (1, 2)\n")
+ # symtable pass
+ check_syntax_error(self, "def f():\n"
+ " x: int\n"
+ " global x\n")
+ check_syntax_error(self, "def f():\n"
+ " global x\n"
+ " x: int\n")
+
+ def test_var_annot_basic_semantics(self):
+ # execution order
+ with self.assertRaises(ZeroDivisionError):
+ no_name[does_not_exist]: no_name_again = 1/0
+ with self.assertRaises(NameError):
+ no_name[does_not_exist]: 1/0 = 0
+ global var_annot_global
+
+ # function semantics
+ def f():
+ st: str = "Hello"
+ a.b: int = (1, 2)
+ return st
+ self.assertEqual(f.__annotations__, {})
+ def f_OK():
+ x: 1/0
+ f_OK()
+ def fbad():
+ x: int
+ print(x)
+ with self.assertRaises(UnboundLocalError):
+ fbad()
+ def f2bad():
+ (no_such_global): int
+ print(no_such_global)
+ try:
+ f2bad()
+ except Exception as e:
+ self.assertIs(type(e), NameError)
+
+ # class semantics
+ class C:
+ x: int
+ s: str = "attr"
+ z = 2
+ def __init__(self, x):
+ self.x: int = x
+ self.assertEqual(C.__annotations__, {'x': int, 's': str})
+ with self.assertRaises(NameError):
+ class CBad:
+ no_such_name_defined.attr: int = 0
+ with self.assertRaises(NameError):
+ class Cbad2(C):
+ x: int
+ x.y: list = []
+
+ def test_var_annot_metaclass_semantics(self):
+ class CMeta(type):
+ @classmethod
+ def __prepare__(metacls, name, bases, **kwds):
+ return {'__annotations__': CNS()}
+ class CC(metaclass=CMeta):
+ XX: 'ANNOT'
+ self.assertEqual(CC.__annotations__['xx'], 'ANNOT')
+
+ def test_var_annot_module_semantics(self):
+ with self.assertRaises(AttributeError):
+ print(test.__annotations__)
+ self.assertEqual(ann_module.__annotations__,
+ {1: 2, 'x': int, 'y': str, 'f': typing.Tuple[int, int]})
+ self.assertEqual(ann_module.M.__annotations__,
+ {'123': 123, 'o': type})
+ self.assertEqual(ann_module2.__annotations__, {})
+ self.assertEqual(typing.get_type_hints(ann_module2.CV,
+ ann_module2.__dict__),
+ ChainMap({'var': typing.ClassVar[ann_module2.CV]}, {}))
+
+ def test_var_annot_in_module(self):
+ # check that functions fail the same way when executed
+ # outside of module where they were defined
+ from test.ann_module3 import f_bad_ann, g_bad_ann, D_bad_ann
+ with self.assertRaises(NameError):
+ f_bad_ann()
+ with self.assertRaises(NameError):
+ g_bad_ann()
+ with self.assertRaises(NameError):
+ D_bad_ann(5)
+
+ def test_var_annot_simple_exec(self):
+ gns = {}; lns= {}
+ exec("'docstring'\n"
+ "__annotations__[1] = 2\n"
+ "x: int = 5\n", gns, lns)
+ self.assertEqual(lns["__annotations__"], {1: 2, 'x': int})
+ with self.assertRaises(KeyError):
+ gns['__annotations__']
+
+ def test_var_annot_custom_maps(self):
+ # tests with custom locals() and __annotations__
+ ns = {'__annotations__': CNS()}
+ exec('X: int; Z: str = "Z"; (w): complex = 1j', ns)
+ self.assertEqual(ns['__annotations__']['x'], int)
+ self.assertEqual(ns['__annotations__']['z'], str)
+ with self.assertRaises(KeyError):
+ ns['__annotations__']['w']
+ nonloc_ns = {}
+ class CNS2:
+ def __init__(self):
+ self._dct = {}
+ def __setitem__(self, item, value):
+ nonlocal nonloc_ns
+ self._dct[item] = value
+ nonloc_ns[item] = value
+ def __getitem__(self, item):
+ return self._dct[item]
+ exec('x: int = 1', {}, CNS2())
+ self.assertEqual(nonloc_ns['__annotations__']['x'], int)
+
+ def test_var_annot_refleak(self):
+ # complex case: custom locals plus custom __annotations__
+ # this was causing refleak
+ cns = CNS()
+ nonloc_ns = {'__annotations__': cns}
+ class CNS2:
+ def __init__(self):
+ self._dct = {'__annotations__': cns}
+ def __setitem__(self, item, value):
+ nonlocal nonloc_ns
+ self._dct[item] = value
+ nonloc_ns[item] = value
+ def __getitem__(self, item):
+ return self._dct[item]
+ exec('X: str', {}, CNS2())
+ self.assertEqual(nonloc_ns['__annotations__']['x'], str)
+
def test_funcdef(self):
### [decorators] 'def' NAME parameters ['->' test] ':' suite
### decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
diff --git a/Lib/test/test_opcodes.py b/Lib/test/test_opcodes.py
index 6ef93d9..6806c61 100644
--- a/Lib/test/test_opcodes.py
+++ b/Lib/test/test_opcodes.py
@@ -1,6 +1,7 @@
# Python test set -- part 2, opcodes
import unittest
+from test import ann_module
class OpcodeTest(unittest.TestCase):
@@ -20,6 +21,32 @@ class OpcodeTest(unittest.TestCase):
if n != 90:
self.fail('try inside for')
+ def test_setup_annotations_line(self):
+ # check that SETUP_ANNOTATIONS does not create spurious line numbers
+ try:
+ with open(ann_module.__file__) as f:
+ txt = f.read()
+ co = compile(txt, ann_module.__file__, 'exec')
+ self.assertEqual(co.co_firstlineno, 6)
+ except OSError:
+ pass
+
+ def test_no_annotations_if_not_needed(self):
+ class C: pass
+ with self.assertRaises(AttributeError):
+ C.__annotations__
+
+ def test_use_existing_annotations(self):
+ ns = {'__annotations__': {1: 2}}
+ exec('x: int', ns)
+ self.assertEqual(ns['__annotations__'], {'x': int, 1: 2})
+
+ def test_do_not_recreate_annotations(self):
+ class C:
+ del __annotations__
+ with self.assertRaises(NameError):
+ x: int
+
def test_raise_class_exceptions(self):
class AClass(Exception): pass
diff --git a/Lib/test/test_parser.py b/Lib/test/test_parser.py
index e2a42f9..d6e6f71 100644
--- a/Lib/test/test_parser.py
+++ b/Lib/test/test_parser.py
@@ -138,6 +138,45 @@ class RoundtripLegalSyntaxTestCase(unittest.TestCase):
self.check_suite("a = b")
self.check_suite("a = b = c = d = e")
+ def test_var_annot(self):
+ self.check_suite("x: int = 5")
+ self.check_suite("y: List[T] = []; z: [list] = fun()")
+ self.check_suite("x: tuple = (1, 2)")
+ self.check_suite("d[f()]: int = 42")
+ self.check_suite("f(d[x]): str = 'abc'")
+ self.check_suite("x.y.z.w: complex = 42j")
+ self.check_suite("x: int")
+ self.check_suite("def f():\n"
+ " x: str\n"
+ " y: int = 5\n")
+ self.check_suite("class C:\n"
+ " x: str\n"
+ " y: int = 5\n")
+ self.check_suite("class C:\n"
+ " def __init__(self, x: int) -> None:\n"
+ " self.x: int = x\n")
+ # double check for nonsense
+ with self.assertRaises(SyntaxError):
+ exec("2+2: int", {}, {})
+ with self.assertRaises(SyntaxError):
+ exec("[]: int = 5", {}, {})
+ with self.assertRaises(SyntaxError):
+ exec("x, *y, z: int = range(5)", {}, {})
+ with self.assertRaises(SyntaxError):
+ exec("t: tuple = 1, 2", {}, {})
+ with self.assertRaises(SyntaxError):
+ exec("u = v: int", {}, {})
+ with self.assertRaises(SyntaxError):
+ exec("False: int", {}, {})
+ with self.assertRaises(SyntaxError):
+ exec("x.False: int", {}, {})
+ with self.assertRaises(SyntaxError):
+ exec("x.y,: int", {}, {})
+ with self.assertRaises(SyntaxError):
+ exec("[0]: int", {}, {})
+ with self.assertRaises(SyntaxError):
+ exec("f(): int", {}, {})
+
def test_simple_augmented_assignments(self):
self.check_suite("a += b")
self.check_suite("a -= b")
diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py
index 17a82c2..5174d56 100644
--- a/Lib/test/test_pydoc.py
+++ b/Lib/test/test_pydoc.py
@@ -83,6 +83,8 @@ CLASSES
| Data and other attributes defined here:
|\x20\x20
| NO_MEANING = 'eggs'
+ |\x20\x20
+ | __annotations__ = {'NO_MEANING': <class 'str'>}
\x20\x20\x20\x20
class C(builtins.object)
| Methods defined here:
@@ -195,6 +197,8 @@ Data descriptors defined here:<br>
Data and other attributes defined here:<br>
<dl><dt><strong>NO_MEANING</strong> = 'eggs'</dl>
+<dl><dt><strong>__annotations__</strong> = {'NO_MEANING': &lt;class 'str'&gt;}</dl>
+
</td></tr></table> <p>
<table width="100%%" cellspacing=0 cellpadding=2 border=0 summary="section">
<tr bgcolor="#ffc8d8">
diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py
index bf99505..3047165 100644
--- a/Lib/test/test_symtable.py
+++ b/Lib/test/test_symtable.py
@@ -133,6 +133,17 @@ class SymtableTest(unittest.TestCase):
self.assertTrue(self.Mine.lookup("a_method").is_assigned())
self.assertFalse(self.internal.lookup("x").is_assigned())
+ def test_annotated(self):
+ st1 = symtable.symtable('def f():\n x: int\n', 'test', 'exec')
+ st2 = st1.get_children()[0]
+ self.assertTrue(st2.lookup('x').is_local())
+ self.assertTrue(st2.lookup('x').is_annotated())
+ self.assertFalse(st2.lookup('x').is_global())
+ st3 = symtable.symtable('def f():\n x = 1\n', 'test', 'exec')
+ st4 = st3.get_children()[0]
+ self.assertTrue(st4.lookup('x').is_local())
+ self.assertFalse(st4.lookup('x').is_annotated())
+
def test_imported(self):
self.assertTrue(self.top.lookup("sys").is_imported())
diff --git a/Lib/test/test_tools/test_com2ann.py b/Lib/test/test_tools/test_com2ann.py
new file mode 100644
index 0000000..2731f82
--- /dev/null
+++ b/Lib/test/test_tools/test_com2ann.py
@@ -0,0 +1,260 @@
+"""Tests for the com2ann.py script in the Tools/parser directory."""
+
+import unittest
+import test.support
+import os
+import re
+
+from test.test_tools import basepath, toolsdir, skip_if_missing
+
+skip_if_missing()
+
+parser_path = os.path.join(toolsdir, "parser")
+
+with test.support.DirsOnSysPath(parser_path):
+ from com2ann import *
+
+class BaseTestCase(unittest.TestCase):
+
+ def check(self, code, expected, n=False, e=False):
+ self.assertEqual(com2ann(code,
+ drop_None=n, drop_Ellipsis=e, silent=True),
+ expected)
+
+class SimpleTestCase(BaseTestCase):
+ # Tests for basic conversions
+
+ def test_basics(self):
+ self.check("z = 5", "z = 5")
+ self.check("z: int = 5", "z: int = 5")
+ self.check("z = 5 # type: int", "z: int = 5")
+ self.check("z = 5 # type: int # comment",
+ "z: int = 5 # comment")
+
+ def test_type_ignore(self):
+ self.check("foobar = foobaz() #type: ignore",
+ "foobar = foobaz() #type: ignore")
+ self.check("a = 42 #type: ignore #comment",
+ "a = 42 #type: ignore #comment")
+
+ def test_complete_tuple(self):
+ self.check("t = 1, 2, 3 # type: Tuple[int, ...]",
+ "t: Tuple[int, ...] = (1, 2, 3)")
+ self.check("t = 1, # type: Tuple[int]",
+ "t: Tuple[int] = (1,)")
+ self.check("t = (1, 2, 3) # type: Tuple[int, ...]",
+ "t: Tuple[int, ...] = (1, 2, 3)")
+
+ def test_drop_None(self):
+ self.check("x = None # type: int",
+ "x: int", True)
+ self.check("x = None # type: int # another",
+ "x: int # another", True)
+ self.check("x = None # type: int # None",
+ "x: int # None", True)
+
+ def test_drop_Ellipsis(self):
+ self.check("x = ... # type: int",
+ "x: int", False, True)
+ self.check("x = ... # type: int # another",
+ "x: int # another", False, True)
+ self.check("x = ... # type: int # ...",
+ "x: int # ...", False, True)
+
+ def test_newline(self):
+ self.check("z = 5 # type: int\r\n", "z: int = 5\r\n")
+ self.check("z = 5 # type: int # comment\x85",
+ "z: int = 5 # comment\x85")
+
+ def test_wrong(self):
+ self.check("#type : str", "#type : str")
+ self.check("x==y #type: bool", "x==y #type: bool")
+
+ def test_pattern(self):
+ for line in ["#type: int", " # type: str[:] # com"]:
+ self.assertTrue(re.search(TYPE_COM, line))
+ for line in ["", "#", "# comment", "#type", "type int:"]:
+ self.assertFalse(re.search(TYPE_COM, line))
+
+class BigTestCase(BaseTestCase):
+ # Tests for really crazy formatting, to be sure
+ # that script works reasonably in extreme situations
+
+ def test_crazy(self):
+ self.maxDiff = None
+ self.check(crazy_code, big_result, False, False)
+ self.check(crazy_code, big_result_ne, True, True)
+
+crazy_code = """\
+# -*- coding: utf-8 -*- # this should not be spoiled
+'''
+Docstring here
+'''
+
+import testmod
+x = 5 #type : int # this one is OK
+ttt \\
+ = \\
+ 1.0, \\
+ 2.0, \\
+ 3.0, #type: Tuple[float, float, float]
+with foo(x==1) as f: #type: str
+ print(f)
+
+for i, j in my_inter(x=1): # type: ignore
+ i + j # type: int # what about this
+
+x = y = z = 1 # type: int
+x, y, z = [], [], [] # type: (List[int], List[int], List[str])
+class C:
+
+
+ l[f(x
+ =1)] = [
+
+ 1,
+ 2,
+ ] # type: List[int]
+
+
+ (C.x[1]) = \\
+ 42 == 5# type: bool
+lst[...] = \\
+ ((\\
+...)) # type: int # comment ..
+
+y = ... # type: int # comment ...
+z = ...
+#type: int
+
+
+#DONE placement of annotation after target rather than before =
+
+TD.x[1] \\
+ = 0 == 5# type: bool
+
+TD.y[1] =5 == 5# type: bool # one more here
+F[G(x == y,
+
+# hm...
+
+ z)]\\
+ = None # type: OMG[int] # comment: None
+x = None#type:int #comment : None"""
+
+big_result = """\
+# -*- coding: utf-8 -*- # this should not be spoiled
+'''
+Docstring here
+'''
+
+import testmod
+x: int = 5 # this one is OK
+ttt: Tuple[float, float, float] \\
+ = \\
+ (1.0, \\
+ 2.0, \\
+ 3.0,)
+with foo(x==1) as f: #type: str
+ print(f)
+
+for i, j in my_inter(x=1): # type: ignore
+ i + j # type: int # what about this
+
+x = y = z = 1 # type: int
+x, y, z = [], [], [] # type: (List[int], List[int], List[str])
+class C:
+
+
+ l[f(x
+ =1)]: List[int] = [
+
+ 1,
+ 2,
+ ]
+
+
+ (C.x[1]): bool = \\
+ 42 == 5
+lst[...]: int = \\
+ ((\\
+...)) # comment ..
+
+y: int = ... # comment ...
+z = ...
+#type: int
+
+
+#DONE placement of annotation after target rather than before =
+
+TD.x[1]: bool \\
+ = 0 == 5
+
+TD.y[1]: bool =5 == 5 # one more here
+F[G(x == y,
+
+# hm...
+
+ z)]: OMG[int]\\
+ = None # comment: None
+x: int = None #comment : None"""
+
+big_result_ne = """\
+# -*- coding: utf-8 -*- # this should not be spoiled
+'''
+Docstring here
+'''
+
+import testmod
+x: int = 5 # this one is OK
+ttt: Tuple[float, float, float] \\
+ = \\
+ (1.0, \\
+ 2.0, \\
+ 3.0,)
+with foo(x==1) as f: #type: str
+ print(f)
+
+for i, j in my_inter(x=1): # type: ignore
+ i + j # type: int # what about this
+
+x = y = z = 1 # type: int
+x, y, z = [], [], [] # type: (List[int], List[int], List[str])
+class C:
+
+
+ l[f(x
+ =1)]: List[int] = [
+
+ 1,
+ 2,
+ ]
+
+
+ (C.x[1]): bool = \\
+ 42 == 5
+lst[...]: int \\
+ \\
+ # comment ..
+
+y: int # comment ...
+z = ...
+#type: int
+
+
+#DONE placement of annotation after target rather than before =
+
+TD.x[1]: bool \\
+ = 0 == 5
+
+TD.y[1]: bool =5 == 5 # one more here
+F[G(x == y,
+
+# hm...
+
+ z)]: OMG[int]\\
+ # comment: None
+x: int #comment : None"""
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index 72afe67..e3904b1 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -4,14 +4,16 @@ import pickle
import re
import sys
from unittest import TestCase, main, skipUnless, SkipTest
+from collections import ChainMap
+from test import ann_module, ann_module2, ann_module3
from typing import Any
from typing import TypeVar, AnyStr
from typing import T, KT, VT # Not in __all__.
from typing import Union, Optional
-from typing import Tuple
+from typing import Tuple, List
from typing import Callable
-from typing import Generic
+from typing import Generic, ClassVar
from typing import cast
from typing import get_type_hints
from typing import no_type_check, no_type_check_decorator
@@ -827,6 +829,43 @@ class GenericTests(BaseTestCase):
with self.assertRaises(Exception):
D[T]
+class ClassVarTests(BaseTestCase):
+
+ def test_basics(self):
+ with self.assertRaises(TypeError):
+ ClassVar[1]
+ with self.assertRaises(TypeError):
+ ClassVar[int, str]
+ with self.assertRaises(TypeError):
+ ClassVar[int][str]
+
+ def test_repr(self):
+ self.assertEqual(repr(ClassVar), 'typing.ClassVar')
+ cv = ClassVar[int]
+ self.assertEqual(repr(cv), 'typing.ClassVar[int]')
+ cv = ClassVar[Employee]
+ self.assertEqual(repr(cv), 'typing.ClassVar[%s.Employee]' % __name__)
+
+ def test_cannot_subclass(self):
+ with self.assertRaises(TypeError):
+ class C(type(ClassVar)):
+ pass
+ with self.assertRaises(TypeError):
+ class C(type(ClassVar[int])):
+ pass
+
+ def test_cannot_init(self):
+ with self.assertRaises(TypeError):
+ type(ClassVar)()
+ with self.assertRaises(TypeError):
+ type(ClassVar[Optional[int]])()
+
+ def test_no_isinstance(self):
+ with self.assertRaises(TypeError):
+ isinstance(1, ClassVar[int])
+ with self.assertRaises(TypeError):
+ issubclass(int, ClassVar)
+
class VarianceTests(BaseTestCase):
@@ -930,6 +969,46 @@ class ForwardRefTests(BaseTestCase):
right_hints = get_type_hints(t.add_right, globals(), locals())
self.assertEqual(right_hints['node'], Optional[Node[T]])
+ def test_get_type_hints(self):
+ gth = get_type_hints
+ self.assertEqual(gth(ann_module), {'x': int, 'y': str})
+ self.assertEqual(gth(ann_module.C, ann_module.__dict__),
+ ChainMap({'y': Optional[ann_module.C]}, {}))
+ self.assertEqual(gth(ann_module2), {})
+ self.assertEqual(gth(ann_module3), {})
+ self.assertEqual(repr(gth(ann_module.j_class)), 'ChainMap({}, {})')
+ self.assertEqual(gth(ann_module.M), ChainMap({'123': 123, 'o': type},
+ {}, {}))
+ self.assertEqual(gth(ann_module.D),
+ ChainMap({'j': str, 'k': str,
+ 'y': Optional[ann_module.C]}, {}))
+ self.assertEqual(gth(ann_module.Y), ChainMap({'z': int}, {}))
+ self.assertEqual(gth(ann_module.h_class),
+ ChainMap({}, {'y': Optional[ann_module.C]}, {}))
+ self.assertEqual(gth(ann_module.S), ChainMap({'x': str, 'y': str},
+ {}))
+ self.assertEqual(gth(ann_module.foo), {'x': int})
+
+ def testf(x, y): ...
+ testf.__annotations__['x'] = 'int'
+ self.assertEqual(gth(testf), {'x': int})
+ self.assertEqual(gth(ann_module2.NTC.meth), {})
+
+ # interactions with ClassVar
+ class B:
+ x: ClassVar[Optional['B']] = None
+ y: int
+ class C(B):
+ z: ClassVar['C'] = B()
+ class G(Generic[T]):
+ lst: ClassVar[List[T]] = []
+ self.assertEqual(gth(B, locals()),
+ ChainMap({'y': int, 'x': ClassVar[Optional[B]]}, {}))
+ self.assertEqual(gth(C, locals()),
+ ChainMap({'z': ClassVar[C]},
+ {'y': int, 'x': ClassVar[Optional[B]]}, {}))
+ self.assertEqual(gth(G), ChainMap({'lst': ClassVar[List[T]]},{},{}))
+
def test_forwardref_instance_type_error(self):
fr = typing._ForwardRef('int')
with self.assertRaises(TypeError):
diff --git a/Lib/typing.py b/Lib/typing.py
index 5573a1f..6ce74fc 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -6,6 +6,7 @@ import functools
import re as stdlib_re # Avoid confusion with the re we export.
import sys
import types
+
try:
import collections.abc as collections_abc
except ImportError:
@@ -17,6 +18,7 @@ __all__ = [
# Super-special typing primitives.
'Any',
'Callable',
+ 'ClassVar',
'Generic',
'Optional',
'Tuple',
@@ -270,7 +272,7 @@ class _TypeAlias:
def _get_type_vars(types, tvars):
for t in types:
- if isinstance(t, TypingMeta):
+ if isinstance(t, TypingMeta) or isinstance(t, _ClassVar):
t._get_type_vars(tvars)
@@ -281,7 +283,7 @@ def _type_vars(types):
def _eval_type(t, globalns, localns):
- if isinstance(t, TypingMeta):
+ if isinstance(t, TypingMeta) or isinstance(t, _ClassVar):
return t._eval_type(globalns, localns)
else:
return t
@@ -1114,6 +1116,67 @@ class Generic(metaclass=GenericMeta):
return obj
+class _ClassVar(metaclass=TypingMeta, _root=True):
+ """Special type construct to mark class variables.
+
+ An annotation wrapped in ClassVar indicates that a given
+ attribute is intended to be used as a class variable and
+ should not be set on instances of that class. Usage::
+
+ class Starship:
+ stats: ClassVar[Dict[str, int]] = {} # class variable
+ damage: int = 10 # instance variable
+
+ ClassVar accepts only types and cannot be further subscribed.
+
+ Note that ClassVar is not a class itself, and should not
+ be used with isinstance() or issubclass().
+ """
+
+ def __init__(self, tp=None, _root=False):
+ cls = type(self)
+ if _root:
+ self.__type__ = tp
+ else:
+ raise TypeError('Cannot initialize {}'.format(cls.__name__[1:]))
+
+ def __getitem__(self, item):
+ cls = type(self)
+ if self.__type__ is None:
+ return cls(_type_check(item,
+ '{} accepts only types.'.format(cls.__name__[1:])),
+ _root=True)
+ raise TypeError('{} cannot be further subscripted'
+ .format(cls.__name__[1:]))
+
+ def _eval_type(self, globalns, localns):
+ return type(self)(_eval_type(self.__type__, globalns, localns),
+ _root=True)
+
+ def _get_type_vars(self, tvars):
+ if self.__type__:
+ _get_type_vars(self.__type__, tvars)
+
+ def __repr__(self):
+ cls = type(self)
+ if not self.__type__:
+ return '{}.{}'.format(cls.__module__, cls.__name__[1:])
+ return '{}.{}[{}]'.format(cls.__module__, cls.__name__[1:],
+ _type_repr(self.__type__))
+
+ def __hash__(self):
+ return hash((type(self).__name__, self.__type__))
+
+ def __eq__(self, other):
+ if not isinstance(other, _ClassVar):
+ return NotImplemented
+ if self.__type__ is not None:
+ return self.__type__ == other.__type__
+ return self is other
+
+ClassVar = _ClassVar(_root=True)
+
+
def cast(typ, val):
"""Cast a value to a type.
@@ -1142,12 +1205,20 @@ def _get_defaults(func):
def get_type_hints(obj, globalns=None, localns=None):
- """Return type hints for a function or method object.
+ """Return type hints for an object.
This is often the same as obj.__annotations__, but it handles
forward references encoded as string literals, and if necessary
adds Optional[t] if a default value equal to None is set.
+ The argument may be a module, class, method, or function. The annotations
+ are returned as a dictionary, or in the case of a class, a ChainMap of
+ dictionaries.
+
+ TypeError is raised if the argument is not of a type that can contain
+ annotations, and an empty dictionary is returned if no annotations are
+ present.
+
BEWARE -- the behavior of globalns and localns is counterintuitive
(unless you are familiar with how eval() and exec() work). The
search order is locals first, then globals.
@@ -1162,6 +1233,7 @@ def get_type_hints(obj, globalns=None, localns=None):
- If two dict arguments are passed, they specify globals and
locals, respectively.
"""
+
if getattr(obj, '__no_type_check__', None):
return {}
if globalns is None:
@@ -1170,16 +1242,62 @@ def get_type_hints(obj, globalns=None, localns=None):
localns = globalns
elif localns is None:
localns = globalns
- defaults = _get_defaults(obj)
- hints = dict(obj.__annotations__)
- for name, value in hints.items():
- if isinstance(value, str):
- value = _ForwardRef(value)
- value = _eval_type(value, globalns, localns)
- if name in defaults and defaults[name] is None:
- value = Optional[value]
- hints[name] = value
- return hints
+
+ if (isinstance(obj, types.FunctionType) or
+ isinstance(obj, types.BuiltinFunctionType) or
+ isinstance(obj, types.MethodType)):
+ defaults = _get_defaults(obj)
+ hints = obj.__annotations__
+ for name, value in hints.items():
+ if value is None:
+ value = type(None)
+ if isinstance(value, str):
+ value = _ForwardRef(value)
+ value = _eval_type(value, globalns, localns)
+ if name in defaults and defaults[name] is None:
+ value = Optional[value]
+ hints[name] = value
+ return hints
+
+ if isinstance(obj, types.ModuleType):
+ try:
+ hints = obj.__annotations__
+ except AttributeError:
+ return {}
+ # we keep only those annotations that can be accessed on module
+ members = obj.__dict__
+ hints = {name: value for name, value in hints.items()
+ if name in members}
+ for name, value in hints.items():
+ if value is None:
+ value = type(None)
+ if isinstance(value, str):
+ value = _ForwardRef(value)
+ value = _eval_type(value, globalns, localns)
+ hints[name] = value
+ return hints
+
+ if isinstance(object, type):
+ cmap = None
+ for base in reversed(obj.__mro__):
+ new_map = collections.ChainMap if cmap is None else cmap.new_child
+ try:
+ hints = base.__dict__['__annotations__']
+ except KeyError:
+ cmap = new_map()
+ else:
+ for name, value in hints.items():
+ if value is None:
+ value = type(None)
+ if isinstance(value, str):
+ value = _ForwardRef(value)
+ value = _eval_type(value, globalns, localns)
+ hints[name] = value
+ cmap = new_map(hints)
+ return cmap
+
+ raise TypeError('{!r} is not a module, class, method, '
+ 'or function.'.format(obj))
def no_type_check(arg):
@@ -1300,6 +1418,8 @@ class _ProtocolMeta(GenericMeta):
else:
if (not attr.startswith('_abc_') and
attr != '__abstractmethods__' and
+ attr != '__annotations__' and
+ attr != '__weakref__' and
attr != '_is_protocol' and
attr != '__dict__' and
attr != '__args__' and