diff options
author | Yury Selivanov <yury@magic.io> | 2016-09-09 03:50:03 (GMT) |
---|---|---|
committer | Yury Selivanov <yury@magic.io> | 2016-09-09 03:50:03 (GMT) |
commit | f8cb8a16a344ab208fd46876c4b63604987347b8 (patch) | |
tree | c44caa48291401d1e1e388004d2762513ac88c93 /Lib | |
parent | 09ad17810c38d1aaae02de69084dd2a8ad9f5cdb (diff) | |
download | cpython-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.py | 4 | ||||
-rw-r--r-- | Lib/lib2to3/Grammar.txt | 5 | ||||
-rw-r--r-- | Lib/lib2to3/tests/test_parser.py | 30 | ||||
-rw-r--r-- | Lib/opcode.py | 3 | ||||
-rwxr-xr-x | Lib/symbol.py | 137 | ||||
-rw-r--r-- | Lib/symtable.py | 5 | ||||
-rw-r--r-- | Lib/test/ann_module.py | 53 | ||||
-rw-r--r-- | Lib/test/ann_module2.py | 36 | ||||
-rw-r--r-- | Lib/test/ann_module3.py | 18 | ||||
-rw-r--r-- | Lib/test/pydoc_mod.py | 2 | ||||
-rw-r--r-- | Lib/test/test___all__.py | 2 | ||||
-rw-r--r-- | Lib/test/test_dis.py | 33 | ||||
-rw-r--r-- | Lib/test/test_grammar.py | 178 | ||||
-rw-r--r-- | Lib/test/test_opcodes.py | 27 | ||||
-rw-r--r-- | Lib/test/test_parser.py | 39 | ||||
-rw-r--r-- | Lib/test/test_pydoc.py | 4 | ||||
-rw-r--r-- | Lib/test/test_symtable.py | 11 | ||||
-rw-r--r-- | Lib/test/test_tools/test_com2ann.py | 260 | ||||
-rw-r--r-- | Lib/test/test_typing.py | 83 | ||||
-rw-r--r-- | Lib/typing.py | 146 |
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': <class 'str'>}</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 |