From 4c5b3143b5af03266357ab2de7f24fb12ceda113 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 27 May 2020 14:25:03 +0300 Subject: [PATCH 01/18] make future annotations default --- Python/ast_opt.c | 24 ------------------------ Python/compile.c | 14 ++------------ Python/future.c | 2 +- 3 files changed, 3 insertions(+), 37 deletions(-) diff --git a/Python/ast_opt.c b/Python/ast_opt.c index 5efaac4c8925a9..22ca6f23aefa30 100644 --- a/Python/ast_opt.c +++ b/Python/ast_opt.c @@ -392,7 +392,6 @@ static int astfold_expr(expr_ty node_, PyArena *ctx_, _PyASTOptimizeState *state static int astfold_arguments(arguments_ty node_, PyArena *ctx_, _PyASTOptimizeState *state); static int astfold_comprehension(comprehension_ty node_, PyArena *ctx_, _PyASTOptimizeState *state); static int astfold_keyword(keyword_ty node_, PyArena *ctx_, _PyASTOptimizeState *state); -static int astfold_arg(arg_ty node_, PyArena *ctx_, _PyASTOptimizeState *state); static int astfold_withitem(withitem_ty node_, PyArena *ctx_, _PyASTOptimizeState *state); static int astfold_excepthandler(excepthandler_ty node_, PyArena *ctx_, _PyASTOptimizeState *state); #define CALL(FUNC, TYPE, ARG) \ @@ -595,25 +594,11 @@ astfold_comprehension(comprehension_ty node_, PyArena *ctx_, _PyASTOptimizeState static int astfold_arguments(arguments_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) { - CALL_SEQ(astfold_arg, arg, node_->posonlyargs); - CALL_SEQ(astfold_arg, arg, node_->args); - CALL_OPT(astfold_arg, arg_ty, node_->vararg); - CALL_SEQ(astfold_arg, arg, node_->kwonlyargs); CALL_SEQ(astfold_expr, expr, node_->kw_defaults); - CALL_OPT(astfold_arg, arg_ty, node_->kwarg); CALL_SEQ(astfold_expr, expr, node_->defaults); return 1; } -static int -astfold_arg(arg_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) -{ - if (!(state->ff_features & CO_FUTURE_ANNOTATIONS)) { - CALL_OPT(astfold_expr, expr_ty, node_->annotation); - } - return 1; -} - static int astfold_stmt(stmt_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) { @@ -622,17 +607,11 @@ astfold_stmt(stmt_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) CALL(astfold_arguments, arguments_ty, node_->v.FunctionDef.args); CALL(astfold_body, asdl_seq, node_->v.FunctionDef.body); CALL_SEQ(astfold_expr, expr, node_->v.FunctionDef.decorator_list); - if (!(state->ff_features & CO_FUTURE_ANNOTATIONS)) { - CALL_OPT(astfold_expr, expr_ty, node_->v.FunctionDef.returns); - } break; case AsyncFunctionDef_kind: CALL(astfold_arguments, arguments_ty, node_->v.AsyncFunctionDef.args); CALL(astfold_body, asdl_seq, node_->v.AsyncFunctionDef.body); CALL_SEQ(astfold_expr, expr, node_->v.AsyncFunctionDef.decorator_list); - if (!(state->ff_features & CO_FUTURE_ANNOTATIONS)) { - CALL_OPT(astfold_expr, expr_ty, node_->v.AsyncFunctionDef.returns); - } break; case ClassDef_kind: CALL_SEQ(astfold_expr, expr, node_->v.ClassDef.bases); @@ -656,9 +635,6 @@ astfold_stmt(stmt_ty node_, PyArena *ctx_, _PyASTOptimizeState *state) break; case AnnAssign_kind: CALL(astfold_expr, expr_ty, node_->v.AnnAssign.target); - if (!(state->ff_features & CO_FUTURE_ANNOTATIONS)) { - CALL(astfold_expr, expr_ty, node_->v.AnnAssign.annotation); - } CALL_OPT(astfold_expr, expr_ty, node_->v.AnnAssign.value); break; case For_kind: diff --git a/Python/compile.c b/Python/compile.c index f2563d7f7a411c..85729c8d13bd69 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -2026,12 +2026,7 @@ compiler_visit_argannotation(struct compiler *c, identifier id, { if (annotation) { PyObject *mangled; - if (c->c_future->ff_features & CO_FUTURE_ANNOTATIONS) { - VISIT(c, annexpr, annotation) - } - else { - VISIT(c, expr, annotation); - } + VISIT(c, annexpr, annotation) mangled = _Py_Mangle(c->u->u_private, id); if (!mangled) return 0; @@ -5261,12 +5256,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) if (s->v.AnnAssign.simple && (c->u->u_scope_type == COMPILER_SCOPE_MODULE || c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { - if (c->c_future->ff_features & CO_FUTURE_ANNOTATIONS) { - VISIT(c, annexpr, s->v.AnnAssign.annotation) - } - else { - VISIT(c, expr, s->v.AnnAssign.annotation); - } + VISIT(c, annexpr, s->v.AnnAssign.annotation) ADDOP_NAME(c, LOAD_NAME, __annotations__, names); mangled = _Py_Mangle(c->u->u_private, targ->v.Name.id); ADDOP_LOAD_CONST_NEW(c, mangled); diff --git a/Python/future.c b/Python/future.c index 3cea4fee78085c..4b73eb64129052 100644 --- a/Python/future.c +++ b/Python/future.c @@ -41,7 +41,7 @@ future_check_features(PyFutureFeatures *ff, stmt_ty s, PyObject *filename) } else if (strcmp(feature, FUTURE_GENERATOR_STOP) == 0) { continue; } else if (strcmp(feature, FUTURE_ANNOTATIONS) == 0) { - ff->ff_features |= CO_FUTURE_ANNOTATIONS; + continue; } else if (strcmp(feature, "braces") == 0) { PyErr_SetString(PyExc_SyntaxError, "not a chance"); From 3402877a0e5c69d5d526dcaef7366596c3da81a2 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 27 May 2020 14:31:23 +0300 Subject: [PATCH 02/18] Fix test_coroutines --- Lib/test/test_coroutines.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/Lib/test/test_coroutines.py b/Lib/test/test_coroutines.py index 145adb67781701..40c2eb8d232dd9 100644 --- a/Lib/test/test_coroutines.py +++ b/Lib/test/test_coroutines.py @@ -91,10 +91,6 @@ def test_badsyntax_1(self): pass """, - """async def foo(a:await something()): - pass - """, - """async def foo(): def bar(): [i async for i in els] @@ -299,10 +295,6 @@ def bar(): pass """, - """async def foo(a:await b): - pass - """, - """def baz(): async def foo(a=await b): pass From 9a131a25826c33f7aacfcf4c9810f7216776395b Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 27 May 2020 14:32:18 +0300 Subject: [PATCH 03/18] Regenerate bytecode for test_dis --- Lib/test/test_dis.py | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 4533a016a2fab5..bbaddd57d29189 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -227,28 +227,26 @@ def bug1333982(x=[]): 2 0 SETUP_ANNOTATIONS 2 LOAD_CONST 0 (1) 4 STORE_NAME 0 (x) - 6 LOAD_NAME 1 (int) - 8 LOAD_NAME 2 (__annotations__) - 10 LOAD_CONST 1 ('x') + 6 LOAD_CONST 1 ('int') + 8 LOAD_NAME 1 (__annotations__) + 10 LOAD_CONST 2 ('x') 12 STORE_SUBSCR - 3 14 LOAD_NAME 3 (fun) - 16 LOAD_CONST 0 (1) - 18 CALL_FUNCTION 1 - 20 LOAD_NAME 2 (__annotations__) - 22 LOAD_CONST 2 ('y') - 24 STORE_SUBSCR - - 4 26 LOAD_CONST 0 (1) - 28 LOAD_NAME 4 (lst) - 30 LOAD_NAME 3 (fun) - 32 LOAD_CONST 3 (0) - 34 CALL_FUNCTION 1 - 36 STORE_SUBSCR - 38 LOAD_NAME 1 (int) - 40 POP_TOP - 42 LOAD_CONST 4 (None) - 44 RETURN_VALUE + 3 14 LOAD_CONST 3 ('fun(1)') + 16 LOAD_NAME 1 (__annotations__) + 18 LOAD_CONST 4 ('y') + 20 STORE_SUBSCR + + 4 22 LOAD_CONST 0 (1) + 24 LOAD_NAME 2 (lst) + 26 LOAD_NAME 3 (fun) + 28 LOAD_CONST 5 (0) + 30 CALL_FUNCTION 1 + 32 STORE_SUBSCR + 34 LOAD_NAME 4 (int) + 36 POP_TOP + 38 LOAD_CONST 6 (None) + 40 RETURN_VALUE """ compound_stmt_str = """\ From fdcea4db7abd2be8829bf88c35dade5cb6000355 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 27 May 2020 14:37:41 +0300 Subject: [PATCH 04/18] Regenerate static strings for docxmlrpc and pydoc tests --- Lib/test/test_docxmlrpc.py | 4 ++-- Lib/test/test_pydoc.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_docxmlrpc.py b/Lib/test/test_docxmlrpc.py index 7d3e30cbee964a..69d52427354a1e 100644 --- a/Lib/test/test_docxmlrpc.py +++ b/Lib/test/test_docxmlrpc.py @@ -188,9 +188,9 @@ def test_annotations(self): b'
Use function annotations.
') self.assertIn( (b'
annotation' - b'(x: int)
' + docstring + b'
\n' + b'(x: \'int\')' + docstring + b'\n' b'
' - b'method_annotation(x: bytes)
'), + b'method_annotation(x: \'bytes\')'), response.read()) def test_server_title_escape(self): diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index 76d2af8e461ed1..aae81e62cbaeba 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -81,7 +81,7 @@ class B(builtins.object) |\x20\x20 | NO_MEANING = 'eggs' |\x20\x20 - | __annotations__ = {'NO_MEANING': } + | __annotations__ = {'NO_MEANING': 'str'} \x20\x20\x20\x20 class C(builtins.object) | Methods defined here: @@ -194,7 +194,7 @@ class C(builtins.object) Data and other attributes defined here:
NO_MEANING = 'eggs'
-
__annotations__ = {'NO_MEANING': <class 'str'>}
+
__annotations__ = {'NO_MEANING': 'str'}

@@ -1055,8 +1055,8 @@ def foo(data: typing.List[typing.Any], T = typing.TypeVar('T') class C(typing.Generic[T], typing.Mapping[int, str]): ... self.assertEqual(pydoc.render_doc(foo).splitlines()[-1], - 'f\x08fo\x08oo\x08o(data: List[Any], x: int)' - ' -> Iterator[Tuple[int, Any]]') + 'f\x08fo\x08oo\x08o(data: \'typing.List[typing.Any]\', x: \'int\')' + ' -> \'typing.Iterator[typing.Tuple[int, typing.Any]]\'') self.assertEqual(pydoc.render_doc(C).splitlines()[2], 'class C\x08C(collections.abc.Mapping, typing.Generic)') From 14e7c2d82ed15570fda18cf2563869ae2be11d7d Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 27 May 2020 14:40:27 +0300 Subject: [PATCH 05/18] Update tests for: syntax, opcodes, posonly, grammar... --- Lib/test/test_grammar.py | 56 ++++++++++++++-------------- Lib/test/test_opcodes.py | 2 +- Lib/test/test_positional_only_arg.py | 17 ++------- Lib/test/test_syntax.py | 8 ---- 4 files changed, 31 insertions(+), 52 deletions(-) diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py index 5235fa2c783f04..441b0b2a1630e4 100644 --- a/Lib/test/test_grammar.py +++ b/Lib/test/test_grammar.py @@ -362,7 +362,7 @@ class C: z = 2 def __init__(self, x): self.x: int = x - self.assertEqual(C.__annotations__, {'_C__foo': int, 's': str}) + self.assertEqual(C.__annotations__, {'_C__foo': 'int', 's': 'str'}) with self.assertRaises(NameError): class CBad: no_such_name_defined.attr: int = 0 @@ -378,15 +378,15 @@ def __prepare__(metacls, name, bases, **kwds): return {'__annotations__': CNS()} class CC(metaclass=CMeta): XX: 'ANNOT' - self.assertEqual(CC.__annotations__['xx'], 'ANNOT') + self.assertEqual(CC.__annotations__['xx'], repr('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]}) + {1: 2, 'x': 'int', 'y': 'str', 'f': 'Tuple[int, int]'}) self.assertEqual(ann_module.M.__annotations__, - {'123': 123, 'o': type}) + {'123': 123, 'o': 'type'}) self.assertEqual(ann_module2.__annotations__, {}) def test_var_annot_in_module(self): @@ -405,7 +405,7 @@ def test_var_annot_simple_exec(self): exec("'docstring'\n" "__annotations__[1] = 2\n" "x: int = 5\n", gns, lns) - self.assertEqual(lns["__annotations__"], {1: 2, 'x': int}) + self.assertEqual(lns["__annotations__"], {1: 2, 'x': 'int'}) with self.assertRaises(KeyError): gns['__annotations__'] @@ -413,8 +413,8 @@ 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) + self.assertEqual(ns['__annotations__']['x'], 'int') + self.assertEqual(ns['__annotations__']['z'], 'str') with self.assertRaises(KeyError): ns['__annotations__']['w'] nonloc_ns = {} @@ -428,7 +428,7 @@ def __setitem__(self, item, value): def __getitem__(self, item): return self._dct[item] exec('x: int = 1', {}, CNS2()) - self.assertEqual(nonloc_ns['__annotations__']['x'], int) + self.assertEqual(nonloc_ns['__annotations__']['x'], 'int') def test_var_annot_refleak(self): # complex case: custom locals plus custom __annotations__ @@ -445,7 +445,7 @@ def __setitem__(self, item, value): def __getitem__(self, item): return self._dct[item] exec('X: str', {}, CNS2()) - self.assertEqual(nonloc_ns['__annotations__']['x'], str) + self.assertEqual(nonloc_ns['__annotations__']['x'], 'str') def test_var_annot_rhs(self): ns = {} @@ -625,50 +625,50 @@ def f(*args, **kwargs): # argument annotation tests def f(x) -> list: pass - self.assertEqual(f.__annotations__, {'return': list}) + self.assertEqual(f.__annotations__, {'return': 'list'}) def f(x: int): pass - self.assertEqual(f.__annotations__, {'x': int}) + self.assertEqual(f.__annotations__, {'x': 'int'}) def f(x: int, /): pass - self.assertEqual(f.__annotations__, {'x': int}) + self.assertEqual(f.__annotations__, {'x': 'int'}) def f(x: int = 34, /): pass - self.assertEqual(f.__annotations__, {'x': int}) + self.assertEqual(f.__annotations__, {'x': 'int'}) def f(*x: str): pass - self.assertEqual(f.__annotations__, {'x': str}) + self.assertEqual(f.__annotations__, {'x': 'str'}) def f(**x: float): pass - self.assertEqual(f.__annotations__, {'x': float}) + self.assertEqual(f.__annotations__, {'x': 'float'}) def f(x, y: 1+2): pass - self.assertEqual(f.__annotations__, {'y': 3}) + self.assertEqual(f.__annotations__, {'y': '1 + 2'}) def f(x, y: 1+2, /): pass - self.assertEqual(f.__annotations__, {'y': 3}) + self.assertEqual(f.__annotations__, {'y': '1 + 2'}) def f(a, b: 1, c: 2, d): pass - self.assertEqual(f.__annotations__, {'b': 1, 'c': 2}) + self.assertEqual(f.__annotations__, {'b': '1', 'c': '2'}) def f(a, b: 1, /, c: 2, d): pass - self.assertEqual(f.__annotations__, {'b': 1, 'c': 2}) + self.assertEqual(f.__annotations__, {'b': '1', 'c': '2'}) def f(a, b: 1, c: 2, d, e: 3 = 4, f=5, *g: 6): pass self.assertEqual(f.__annotations__, - {'b': 1, 'c': 2, 'e': 3, 'g': 6}) + {'b': '1', 'c': '2', 'e': '3', 'g': '6'}) def f(a, b: 1, c: 2, d, e: 3 = 4, f=5, *g: 6, h: 7, i=8, j: 9 = 10, **k: 11) -> 12: pass self.assertEqual(f.__annotations__, - {'b': 1, 'c': 2, 'e': 3, 'g': 6, 'h': 7, 'j': 9, - 'k': 11, 'return': 12}) + {'b': '1', 'c': '2', 'e': '3', 'g': '6', 'h': '7', 'j': '9', + 'k': '11', 'return': '12'}) def f(a, b: 1, c: 2, d, e: 3 = 4, f: int = 5, /, *g: 6, h: 7, i=8, j: 9 = 10, **k: 11) -> 12: pass self.assertEqual(f.__annotations__, - {'b': 1, 'c': 2, 'e': 3, 'f': int, 'g': 6, 'h': 7, 'j': 9, - 'k': 11, 'return': 12}) + {'b': '1', 'c': '2', 'e': '3', 'f': 'int', 'g': '6', 'h': '7', 'j': '9', + 'k': '11', 'return': '12'}) # Check for issue #20625 -- annotations mangling class Spam: def f(self, *, __kw: 1): pass class Ham(Spam): pass - self.assertEqual(Spam.f.__annotations__, {'_Spam__kw': 1}) - self.assertEqual(Ham.f.__annotations__, {'_Spam__kw': 1}) + self.assertEqual(Spam.f.__annotations__, {'_Spam__kw': '1'}) + self.assertEqual(Ham.f.__annotations__, {'_Spam__kw': '1'}) # Check for SF Bug #1697248 - mixing decorators and a return annotation def null(x): return x @null def f(x) -> list: pass - self.assertEqual(f.__annotations__, {'return': list}) + self.assertEqual(f.__annotations__, {'return': 'list'}) # Test expressions as decorators (PEP 614): @False or null @@ -1116,8 +1116,6 @@ def g(): rest = 4, 5, 6; yield 1, 2, 3, *rest # Not allowed at class scope check_syntax_error(self, "class foo:yield 1") check_syntax_error(self, "class foo:yield from ()") - # Check annotation refleak on SyntaxError - check_syntax_error(self, "def g(a:(yield)): pass") def test_yield_in_comprehensions(self): # Check yield in comprehensions diff --git a/Lib/test/test_opcodes.py b/Lib/test/test_opcodes.py index 527aca664d38e8..1152eb65bb2c3d 100644 --- a/Lib/test/test_opcodes.py +++ b/Lib/test/test_opcodes.py @@ -39,7 +39,7 @@ class C: pass def test_use_existing_annotations(self): ns = {'__annotations__': {1: 2}} exec('x: int', ns) - self.assertEqual(ns['__annotations__'], {'x': int, 1: 2}) + self.assertEqual(ns['__annotations__'], {'x': 'int', 1: 2}) def test_do_not_recreate_annotations(self): # Don't rely on the existence of the '__annotations__' global. diff --git a/Lib/test/test_positional_only_arg.py b/Lib/test/test_positional_only_arg.py index 0a9503e2025d6b..1fe8256d46ea45 100644 --- a/Lib/test/test_positional_only_arg.py +++ b/Lib/test/test_positional_only_arg.py @@ -302,14 +302,14 @@ def inner_has_pos_only(): def f(x: int, /): ... return f - assert inner_has_pos_only().__annotations__ == {'x': int} + assert inner_has_pos_only().__annotations__ == {'x': 'int'} class Something: def method(self): def f(x: int, /): ... return f - assert Something().method().__annotations__ == {'x': int} + assert Something().method().__annotations__ == {'x': 'int'} def multiple_levels(): def inner_has_pos_only(): @@ -317,7 +317,7 @@ def f(x: int, /): ... return f return inner_has_pos_only() - assert multiple_levels().__annotations__ == {'x': int} + assert multiple_levels().__annotations__ == {'x': 'int'} def test_same_keyword_as_positional_with_kwargs(self): def f(something,/,**kwargs): @@ -429,17 +429,6 @@ def method(self, /): self.assertEqual(C().method(), sentinel) - def test_annotations_constant_fold(self): - def g(): - def f(x: not (int is int), /): ... - - # without constant folding we end up with - # COMPARE_OP(is), IS_OP (0) - # with constant folding we should expect a IS_OP (1) - codes = [(i.opname, i.argval) for i in dis.get_instructions(g)] - self.assertNotIn(('UNARY_NOT', None), codes) - self.assertIn(('IS_OP', 1), codes) - if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 09c6eb3375409b..7c3302c1d46aeb 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -752,14 +752,6 @@ Traceback (most recent call last): SyntaxError: cannot assign to __debug__ - >>> def f(*args:(lambda __debug__:0)): pass - Traceback (most recent call last): - SyntaxError: cannot assign to __debug__ - - >>> def f(**kwargs:(lambda __debug__:0)): pass - Traceback (most recent call last): - SyntaxError: cannot assign to __debug__ - >>> with (lambda *:0): pass Traceback (most recent call last): SyntaxError: named arguments must follow bare * From f217d4880aaa3572ce600628e23c7f708265ac73 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 27 May 2020 14:47:50 +0300 Subject: [PATCH 06/18] Refactor inspect tests(+doctests) with string annotations --- Doc/library/inspect.rst | 2 +- Lib/test/test_inspect.py | 56 +++++++++++++++++++--------------------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index d00a30ff004063..8fe8fad62f7345 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -1005,7 +1005,7 @@ Classes and functions ... pass ... >>> formatargspec(*getfullargspec(f)) - '(a: int, b: float)' + "(a: 'int', b: 'float')" .. deprecated:: 3.5 Use :func:`signature` and diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 6667dc91edbcec..82564644737dc3 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -861,8 +861,8 @@ def test_getfullargspec(self): formatted='(*arg1, arg2=1)') self.assertFullArgSpecEquals(mod2.annotated, ['arg1'], - ann_e={'arg1' : list}, - formatted='(arg1: list)') + ann_e={'arg1' : 'list'}, + formatted="(arg1: 'list')") self.assertFullArgSpecEquals(mod2.keyword_only_arg, [], kwonlyargs_e=['arg'], formatted='(*, arg)') @@ -2211,27 +2211,27 @@ def test(a, b:'foo') -> 123: pass self.assertEqual(self.signature(test), ((('a', ..., ..., "positional_or_keyword"), - ('b', ..., 'foo', "positional_or_keyword")), - 123)) + ('b', ..., repr('foo'), "positional_or_keyword")), + '123')) def test_signature_on_wkwonly(self): def test(*, a:float, b:str) -> int: pass self.assertEqual(self.signature(test), - ((('a', ..., float, "keyword_only"), - ('b', ..., str, "keyword_only")), - int)) + ((('a', ..., 'float', "keyword_only"), + ('b', ..., 'str', "keyword_only")), + 'int')) def test_signature_on_complex_args(self): def test(a, b:'foo'=10, *args:'bar', spam:'baz', ham=123, **kwargs:int): pass self.assertEqual(self.signature(test), ((('a', ..., ..., "positional_or_keyword"), - ('b', 10, 'foo', "positional_or_keyword"), - ('args', ..., 'bar', "var_positional"), - ('spam', ..., 'baz', "keyword_only"), + ('b', 10, repr('foo'), "positional_or_keyword"), + ('args', ..., repr('bar'), "var_positional"), + ('spam', ..., repr('baz'), "keyword_only"), ('ham', 123, ..., "keyword_only"), - ('kwargs', ..., int, "var_keyword")), + ('kwargs', ..., 'int', "var_keyword")), ...)) def test_signature_without_self(self): @@ -2466,7 +2466,7 @@ def __call__(*, a): self.assertEqual(self.signature(Test().m1), ((('arg1', ..., ..., "positional_or_keyword"), ('arg2', 1, ..., "positional_or_keyword")), - int)) + 'int')) self.assertEqual(self.signature(Test().m2), ((('args', ..., ..., "var_positional"),), @@ -2490,7 +2490,7 @@ def m1d(*args, **kwargs): self.assertEqual(self.signature(m1d), ((('arg1', ..., ..., "positional_or_keyword"), ('arg2', 1, ..., "positional_or_keyword")), - int)) + 'int')) def test_signature_on_classmethod(self): class Test: @@ -2640,12 +2640,12 @@ def test(a, b, c:int) -> 42: self.assertEqual(self.signature(partial(partial(test, 1))), ((('b', ..., ..., "positional_or_keyword"), - ('c', ..., int, "positional_or_keyword")), - 42)) + ('c', ..., 'int', "positional_or_keyword")), + '42')) self.assertEqual(self.signature(partial(partial(test, 1), 2)), - ((('c', ..., int, "positional_or_keyword"),), - 42)) + ((('c', ..., 'int', "positional_or_keyword"),), + '42')) psig = inspect.signature(partial(partial(test, 1), 2)) @@ -2764,12 +2764,12 @@ def test(it, a, *, c) -> 'spam': ((('it', ..., ..., 'positional_or_keyword'), ('a', ..., ..., 'positional_or_keyword'), ('c', 1, ..., 'keyword_only')), - 'spam')) + repr('spam'))) self.assertEqual(self.signature(Spam().ham), ((('a', ..., ..., 'positional_or_keyword'), ('c', 1, ..., 'keyword_only')), - 'spam')) + repr('spam'))) class Spam: def test(self: 'anno', x): @@ -2778,7 +2778,7 @@ def test(self: 'anno', x): g = partialmethod(test, 1) self.assertEqual(self.signature(Spam.g), - ((('self', ..., 'anno', 'positional_or_keyword'),), + ((('self', ..., repr('anno'), 'positional_or_keyword'),), ...)) def test_signature_on_fake_partialmethod(self): @@ -3116,20 +3116,16 @@ def foo(a={}): pass with self.assertRaisesRegex(TypeError, 'unhashable type'): hash(inspect.signature(foo)) - def foo(a) -> {}: pass - with self.assertRaisesRegex(TypeError, 'unhashable type'): - hash(inspect.signature(foo)) - def test_signature_str(self): def foo(a:int=1, *, b, c=None, **kwargs) -> 42: pass self.assertEqual(str(inspect.signature(foo)), - '(a: int = 1, *, b, c=None, **kwargs) -> 42') + '(a: \'int\' = 1, *, b, c=None, **kwargs) -> \'42\'') def foo(a:int=1, *args, b, c=None, **kwargs) -> 42: pass self.assertEqual(str(inspect.signature(foo)), - '(a: int = 1, *args, b, c=None, **kwargs) -> 42') + '(a: \'int\' = 1, *args, b, c=None, **kwargs) -> \'42\'') def foo(): pass @@ -3172,8 +3168,8 @@ def test() -> 42: self.assertIs(sig.return_annotation, None) sig = sig.replace(return_annotation=sig.empty) self.assertIs(sig.return_annotation, sig.empty) - sig = sig.replace(return_annotation=42) - self.assertEqual(sig.return_annotation, 42) + sig = sig.replace(return_annotation='42') + self.assertEqual(sig.return_annotation, '42') self.assertEqual(sig, inspect.signature(test)) def test_signature_on_mangled_parameters(self): @@ -3185,8 +3181,8 @@ class Ham(Spam): self.assertEqual(self.signature(Spam.foo), ((('self', ..., ..., "positional_or_keyword"), - ('_Spam__p1', 2, 1, "positional_or_keyword"), - ('_Spam__p2', 3, 2, "keyword_only")), + ('_Spam__p1', 2, '1', "positional_or_keyword"), + ('_Spam__p2', 3, '2', "keyword_only")), ...)) self.assertEqual(self.signature(Spam.foo), From 6209fa021a194ca9beca184fef904d03f6dc27a7 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 27 May 2020 16:10:18 +0300 Subject: [PATCH 07/18] Add documentation, tests for annotations --- Doc/reference/compound_stmts.rst | 9 +- Lib/test/test_annotations.py | 228 ++++++++++++++++++ .../2020-05-27-16-08-16.bpo-38605.rcs2uK.rst | 1 + 3 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 Lib/test/test_annotations.py create mode 100644 Misc/NEWS.d/next/Core and Builtins/2020-05-27-16-08-16.bpo-38605.rcs2uK.rst diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index 158d6a8f164e23..37e98970f0ca76 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -612,11 +612,10 @@ the form "``-> expression``" after the parameter list. These annotations can be any valid Python expression. The presence of annotations does not change the semantics of a function. The annotation values are available as values of a dictionary keyed by the parameters' names in the :attr:`__annotations__` -attribute of the function object. If the ``annotations`` import from -:mod:`__future__` is used, annotations are preserved as strings at runtime which -enables postponed evaluation. Otherwise, they are evaluated when the function -definition is executed. In this case annotations may be evaluated in -a different order than they appear in the source code. +attribute of the function object. Used annnotations are preserved as strings at +runtime which enables postponed evaluation. In this case annotations may be +evaluated in In this case annotations may be evaluated in a different order +than they appear in the source code. .. index:: pair: lambda; expression diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py new file mode 100644 index 00000000000000..fec54c8b4206a6 --- /dev/null +++ b/Lib/test/test_annotations.py @@ -0,0 +1,228 @@ +import unittest +from textwrap import dedent +import sys + +class AnnotationsFutureTestCase(unittest.TestCase): + template = dedent( + """ + from __future__ import annotations + def f() -> {ann}: + ... + def g(arg: {ann}) -> None: + ... + async def f2() -> {ann}: + ... + async def g2(arg: {ann}) -> None: + ... + var: {ann} + var2: {ann} = None + """ + ) + + def getActual(self, annotation): + scope = {} + exec(self.template.format(ann=annotation), {}, scope) + func_ret_ann = scope['f'].__annotations__['return'] + func_arg_ann = scope['g'].__annotations__['arg'] + async_func_ret_ann = scope['f2'].__annotations__['return'] + async_func_arg_ann = scope['g2'].__annotations__['arg'] + var_ann1 = scope['__annotations__']['var'] + var_ann2 = scope['__annotations__']['var2'] + self.assertEqual(func_ret_ann, func_arg_ann) + self.assertEqual(func_ret_ann, async_func_ret_ann) + self.assertEqual(func_ret_ann, async_func_arg_ann) + self.assertEqual(func_ret_ann, var_ann1) + self.assertEqual(func_ret_ann, var_ann2) + return func_ret_ann + + def assertAnnotationEqual( + self, annotation, expected=None, drop_parens=False, is_tuple=False, + ): + actual = self.getActual(annotation) + if expected is None: + expected = annotation if not is_tuple else annotation[1:-1] + if drop_parens: + self.assertNotEqual(actual, expected) + actual = actual.replace("(", "").replace(")", "") + + self.assertEqual(actual, expected) + + def test_annotations(self): + eq = self.assertAnnotationEqual + eq('...') + eq("'some_string'") + eq("u'some_string'") + eq("b'\\xa3'") + eq('Name') + eq('None') + eq('True') + eq('False') + eq('1') + eq('1.0') + eq('1j') + eq('True or False') + eq('True or False or None') + eq('True and False') + eq('True and False and None') + eq('Name1 and Name2 or Name3') + eq('Name1 and (Name2 or Name3)') + eq('Name1 or Name2 and Name3') + eq('(Name1 or Name2) and Name3') + eq('Name1 and Name2 or Name3 and Name4') + eq('Name1 or Name2 and Name3 or Name4') + eq('a + b + (c + d)') + eq('a * b * (c * d)') + eq('(a ** b) ** c ** d') + eq('v1 << 2') + eq('1 >> v2') + eq('1 % finished') + eq('1 + v2 - v3 * 4 ^ 5 ** v6 / 7 // 8') + eq('not great') + eq('not not great') + eq('~great') + eq('+value') + eq('++value') + eq('-1') + eq('~int and not v1 ^ 123 + v2 | True') + eq('a + (not b)') + eq('lambda: None') + eq('lambda arg: None') + eq('lambda a=True: a') + eq('lambda a, b, c=True: a') + eq("lambda a, b, c=True, *, d=1 << v2, e='str': a") + eq("lambda a, b, c=True, *vararg, d, e='str', **kwargs: a + b") + eq("lambda a, /, b, c=True, *vararg, d, e='str', **kwargs: a + b") + eq('lambda x, /: x') + eq('lambda x=1, /: x') + eq('lambda x, /, y: x + y') + eq('lambda x=1, /, y=2: x + y') + eq('lambda x, /, y=1: x + y') + eq('lambda x, /, y=1, *, z=3: x + y + z') + eq('lambda x=1, /, y=2, *, z=3: x + y + z') + eq('lambda x=1, /, y=2, *, z: x + y + z') + eq('lambda x=1, y=2, z=3, /, w=4, *, l, l2: x + y + z + w + l + l2') + eq('lambda x=1, y=2, z=3, /, w=4, *, l, l2, **kwargs: x + y + z + w + l + l2') + eq('lambda x, /, y=1, *, z: x + y + z') + eq('lambda x: lambda y: x + y') + eq('1 if True else 2') + eq('str or None if int or True else str or bytes or None') + eq('str or None if (1 if True else 2) else str or bytes or None') + eq("0 if not x else 1 if x > 0 else -1") + eq("(1 if x > 0 else -1) if x else 0") + eq("{'2.7': dead, '3.7': long_live or die_hard}") + eq("{'2.7': dead, '3.7': long_live or die_hard, **{'3.6': verygood}}") + eq("{**a, **b, **c}") + eq("{'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'}") + eq("{*a, *b, *c}") + eq("({'a': 'b'}, True or False, +value, 'string', b'bytes') or None") + eq("()") + eq("(a,)") + eq("(a, b)") + eq("(a, b, c)") + eq("(*a, *b, *c)") + eq("[]") + eq("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C]") + eq("[*a, *b, *c]") + eq("{i for i in (1, 2, 3)}") + eq("{i ** 2 for i in (1, 2, 3)}") + eq("{i ** 2 for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))}") + eq("{i ** 2 + j for i in (1, 2, 3) for j in (1, 2, 3)}") + eq("[i for i in (1, 2, 3)]") + eq("[i ** 2 for i in (1, 2, 3)]") + eq("[i ** 2 for i, _ in ((1, 'a'), (2, 'b'), (3, 'c'))]") + eq("[i ** 2 + j for i in (1, 2, 3) for j in (1, 2, 3)]") + eq("(i for i in (1, 2, 3))") + eq("(i ** 2 for i in (1, 2, 3))") + eq("(i ** 2 for i, _ in ((1, 'a'), (2, 'b'), (3, 'c')))") + eq("(i ** 2 + j for i in (1, 2, 3) for j in (1, 2, 3))") + eq("{i: 0 for i in (1, 2, 3)}") + eq("{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}") + eq("[(x, y) for x, y in (a, b)]") + eq("[(x,) for x, in (a,)]") + eq("Python3 > Python2 > COBOL") + eq("Life is Life") + eq("call()") + eq("call(arg)") + eq("call(kwarg='hey')") + eq("call(arg, kwarg='hey')") + eq("call(arg, *args, another, kwarg='hey')") + eq("call(arg, another, kwarg='hey', **kwargs, kwarg2='ho')") + eq("lukasz.langa.pl") + eq("call.me(maybe)") + eq("1 .real") + eq("1.0.real") + eq("....__class__") + eq("list[str]") + eq("dict[str, int]") + eq("set[str,]") + eq("tuple[str, ...]") + eq("tuple[(str, *types)]") + eq("tuple[str, int, (str, int)]") + eq("tuple[(*int, str, str, (str, int))]") + eq("tuple[str, int, float, dict[str, int]]") + eq("slice[0]") + eq("slice[0:1]") + eq("slice[0:1:2]") + eq("slice[:]") + eq("slice[:-1]") + eq("slice[1:]") + eq("slice[::-1]") + eq("slice[:,]") + eq("slice[1:2,]") + eq("slice[1:2:3,]") + eq("slice[1:2, 1]") + eq("slice[1:2, 2, 3]") + eq("slice[()]") + eq("slice[a, b:c, d:e:f]") + eq("slice[(x for x in a)]") + eq('str or None if sys.version_info[0] > (3,) else str or bytes or None') + eq("f'f-string without formatted values is just a string'") + eq("f'{{NOT a formatted value}}'") + eq("f'some f-string with {a} {few():.2f} {formatted.values!r}'") + eq('''f"{f'{nested} inner'} outer"''') + eq("f'space between opening braces: { {a for a in (1, 2, 3)}}'") + eq("f'{(lambda x: x)}'") + eq("f'{(None if a else lambda x: x)}'") + eq("f'{x}'") + eq("f'{x!r}'") + eq("f'{x!a}'") + eq('(yield from outside_of_generator)') + eq('(yield)') + eq('(yield a + b)') + eq('await some.complicated[0].call(with_args=True or 1 is not 1)') + eq('[x for x in (a if b else c)]') + eq('[x for x in a if (b if c else d)]') + eq('f(x for x in a)') + eq('f(1, (x for x in a))') + eq('f((x for x in a), 2)') + eq('(((a)))', 'a') + eq('(((a, b)))', '(a, b)') + eq("(x := 10)") + eq("f'{(x := 10):=10}'") + eq("1 + 2 + 3") + + def test_fstring_debug_annotations(self): + # f-strings with '=' don't round trip very well, so set the expected + # result explicitely. + self.assertAnnotationEqual("f'{x=!r}'", expected="f'x={x!r}'") + self.assertAnnotationEqual("f'{x=:}'", expected="f'x={x:}'") + self.assertAnnotationEqual("f'{x=:.2f}'", expected="f'x={x:.2f}'") + self.assertAnnotationEqual("f'{x=!r}'", expected="f'x={x!r}'") + self.assertAnnotationEqual("f'{x=!a}'", expected="f'x={x!a}'") + self.assertAnnotationEqual("f'{x=!s:*^20}'", expected="f'x={x!s:*^20}'") + + def test_infinity_numbers(self): + inf = "1e" + repr(sys.float_info.max_10_exp + 1) + infj = f"{inf}j" + self.assertAnnotationEqual("1e1000", expected=inf) + self.assertAnnotationEqual("1e1000j", expected=infj) + self.assertAnnotationEqual("-1e1000", expected=f"-{inf}") + self.assertAnnotationEqual("3+1e1000j", expected=f"3 + {infj}") + self.assertAnnotationEqual("(1e1000, 1e1000j)", expected=f"({inf}, {infj})") + self.assertAnnotationEqual("'inf'") + self.assertAnnotationEqual("('inf', 1e1000, 'infxxx', 1e1000j)", expected=f"('inf', {inf}, 'infxxx', {infj})") + self.assertAnnotationEqual("(1e1000, (1e1000j,))", expected=f"({inf}, ({infj},))") + + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS.d/next/Core and Builtins/2020-05-27-16-08-16.bpo-38605.rcs2uK.rst b/Misc/NEWS.d/next/Core and Builtins/2020-05-27-16-08-16.bpo-38605.rcs2uK.rst new file mode 100644 index 00000000000000..43d9a402bf70cf --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2020-05-27-16-08-16.bpo-38605.rcs2uK.rst @@ -0,0 +1 @@ +Enable postponed evaluation of annotations (:pep:`563`) by default. From bbf01bba8d6992cff42d20a4de273b4acbeebb7e Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 27 May 2020 17:37:25 +0300 Subject: [PATCH 08/18] Escape double stringified annotations on typing.ForwardRef --- Lib/test/test_functools.py | 2 +- Lib/typing.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index edd5773e13d549..13303e8004a376 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -635,7 +635,7 @@ def test_default_update(self): self.assertEqual(wrapper.__name__, 'f') self.assertEqual(wrapper.__qualname__, f.__qualname__) self.assertEqual(wrapper.attr, 'This is also a test') - self.assertEqual(wrapper.__annotations__['a'], 'This is a new annotation') + self.assertEqual(wrapper.__annotations__['a'], repr('This is a new annotation')) self.assertNotIn('b', wrapper.__annotations__) @unittest.skipIf(sys.flags.optimize >= 2, diff --git a/Lib/typing.py b/Lib/typing.py index 8c61bd8e084a85..8e51e423b1d952 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -469,6 +469,11 @@ class ForwardRef(_Final, _root=True): def __init__(self, arg, is_argument=True): if not isinstance(arg, str): raise TypeError(f"Forward reference must be a string -- got {arg!r}") + # since annotations feature is now default, stringified annotations + # should be escaped from quotes, or this will result with double + # forward refs. + if arg[0] in "'\"" and arg[-1] in "'\"": + arg = arg[1:-1] try: code = compile(arg, '', 'eval') except SyntaxError: From 246577df8fbe54fe9ddccc3c7243377e9c5b8d5a Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Wed, 27 May 2020 18:34:24 +0300 Subject: [PATCH 09/18] Fix test_dataclassess --- Lib/dataclasses.py | 6 +- Lib/test/dataclass_module_1.py | 6 -- Lib/test/dataclass_module_1_str.py | 32 ---------- Lib/test/dataclass_module_2.py | 6 -- Lib/test/dataclass_module_2_str.py | 32 ---------- Lib/test/dataclass_textanno.py | 2 - Lib/test/test_dataclasses.py | 94 +++++++++++------------------- 7 files changed, 38 insertions(+), 140 deletions(-) delete mode 100644 Lib/test/dataclass_module_1_str.py delete mode 100644 Lib/test/dataclass_module_2_str.py diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 530d3e99574e8e..c06aad26b6bb8a 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -398,8 +398,10 @@ def _create_fn(name, args, body, *, globals=None, locals=None, ns = {} exec(txt, globals, ns) - return ns['__create_fn__'](**locals) - + func = ns['__create_fn__'](**locals) + for arg, annotation in func.__annotations__.copy().items(): + func.__annotations__[arg] = locals[annotation] + return func def _field_assign(frozen, name, value, self_name): # If we're a frozen class, then assign to our fields in __init__ diff --git a/Lib/test/dataclass_module_1.py b/Lib/test/dataclass_module_1.py index 87a33f8191d3da..9f0aeda67f9abb 100644 --- a/Lib/test/dataclass_module_1.py +++ b/Lib/test/dataclass_module_1.py @@ -1,9 +1,3 @@ -#from __future__ import annotations -USING_STRINGS = False - -# dataclass_module_1.py and dataclass_module_1_str.py are identical -# except only the latter uses string annotations. - import dataclasses import typing diff --git a/Lib/test/dataclass_module_1_str.py b/Lib/test/dataclass_module_1_str.py deleted file mode 100644 index 6de490b7ad7841..00000000000000 --- a/Lib/test/dataclass_module_1_str.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations -USING_STRINGS = True - -# dataclass_module_1.py and dataclass_module_1_str.py are identical -# except only the latter uses string annotations. - -import dataclasses -import typing - -T_CV2 = typing.ClassVar[int] -T_CV3 = typing.ClassVar - -T_IV2 = dataclasses.InitVar[int] -T_IV3 = dataclasses.InitVar - -@dataclasses.dataclass -class CV: - T_CV4 = typing.ClassVar - cv0: typing.ClassVar[int] = 20 - cv1: typing.ClassVar = 30 - cv2: T_CV2 - cv3: T_CV3 - not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. - -@dataclasses.dataclass -class IV: - T_IV4 = dataclasses.InitVar - iv0: dataclasses.InitVar[int] - iv1: dataclasses.InitVar - iv2: T_IV2 - iv3: T_IV3 - not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/dataclass_module_2.py b/Lib/test/dataclass_module_2.py index 68fb733e29925d..8d120d181bd3d5 100644 --- a/Lib/test/dataclass_module_2.py +++ b/Lib/test/dataclass_module_2.py @@ -1,9 +1,3 @@ -#from __future__ import annotations -USING_STRINGS = False - -# dataclass_module_2.py and dataclass_module_2_str.py are identical -# except only the latter uses string annotations. - from dataclasses import dataclass, InitVar from typing import ClassVar diff --git a/Lib/test/dataclass_module_2_str.py b/Lib/test/dataclass_module_2_str.py deleted file mode 100644 index b363d17c176c22..00000000000000 --- a/Lib/test/dataclass_module_2_str.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations -USING_STRINGS = True - -# dataclass_module_2.py and dataclass_module_2_str.py are identical -# except only the latter uses string annotations. - -from dataclasses import dataclass, InitVar -from typing import ClassVar - -T_CV2 = ClassVar[int] -T_CV3 = ClassVar - -T_IV2 = InitVar[int] -T_IV3 = InitVar - -@dataclass -class CV: - T_CV4 = ClassVar - cv0: ClassVar[int] = 20 - cv1: ClassVar = 30 - cv2: T_CV2 - cv3: T_CV3 - not_cv4: T_CV4 # When using string annotations, this field is not recognized as a ClassVar. - -@dataclass -class IV: - T_IV4 = InitVar - iv0: InitVar[int] - iv1: InitVar - iv2: T_IV2 - iv3: T_IV3 - not_iv4: T_IV4 # When using string annotations, this field is not recognized as an InitVar. diff --git a/Lib/test/dataclass_textanno.py b/Lib/test/dataclass_textanno.py index 3eb6c943d4c434..589b60f0cd61d4 100644 --- a/Lib/test/dataclass_textanno.py +++ b/Lib/test/dataclass_textanno.py @@ -1,5 +1,3 @@ -from __future__ import annotations - import dataclasses diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index b20103bdce51cb..27c0ca186e32a8 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -8,6 +8,7 @@ import inspect import builtins import unittest +from textwrap import dedent from unittest.mock import Mock from typing import ClassVar, Any, List, Union, Tuple, Dict, Generic, TypeVar, Optional from typing import get_type_hints @@ -561,17 +562,17 @@ class C: self.assertEqual(len(the_fields), 3) self.assertEqual(the_fields[0].name, 'x') - self.assertEqual(the_fields[0].type, int) + self.assertEqual(the_fields[0].type, 'int') self.assertFalse(hasattr(C, 'x')) self.assertTrue (the_fields[0].init) self.assertTrue (the_fields[0].repr) self.assertEqual(the_fields[1].name, 'y') - self.assertEqual(the_fields[1].type, str) + self.assertEqual(the_fields[1].type, 'str') self.assertIsNone(getattr(C, 'y')) self.assertFalse(the_fields[1].init) self.assertTrue (the_fields[1].repr) self.assertEqual(the_fields[2].name, 'z') - self.assertEqual(the_fields[2].type, str) + self.assertEqual(the_fields[2].type, 'str') self.assertFalse(hasattr(C, 'z')) self.assertTrue (the_fields[2].init) self.assertFalse(the_fields[2].repr) @@ -757,11 +758,11 @@ class F: def validate_class(cls): # First, check __annotations__, even though they're not # function annotations. - self.assertEqual(cls.__annotations__['i'], int) - self.assertEqual(cls.__annotations__['j'], str) - self.assertEqual(cls.__annotations__['k'], F) - self.assertEqual(cls.__annotations__['l'], float) - self.assertEqual(cls.__annotations__['z'], complex) + self.assertEqual(cls.__annotations__['i'], 'int') + self.assertEqual(cls.__annotations__['j'], 'str') + self.assertEqual(cls.__annotations__['k'], 'F') + self.assertEqual(cls.__annotations__['l'], 'float') + self.assertEqual(cls.__annotations__['z'], 'complex') # Verify __init__. @@ -776,22 +777,22 @@ def validate_class(cls): self.assertEqual(param.name, 'self') param = next(params) self.assertEqual(param.name, 'i') - self.assertIs (param.annotation, int) + self.assertIs (param.annotation, 'int') self.assertEqual(param.default, inspect.Parameter.empty) self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_OR_KEYWORD) param = next(params) self.assertEqual(param.name, 'j') - self.assertIs (param.annotation, str) + self.assertIs (param.annotation, 'str') self.assertEqual(param.default, inspect.Parameter.empty) self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_OR_KEYWORD) param = next(params) self.assertEqual(param.name, 'k') - self.assertIs (param.annotation, F) + self.assertIs (param.annotation, 'F') # Don't test for the default, since it's set to MISSING. self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_OR_KEYWORD) param = next(params) self.assertEqual(param.name, 'l') - self.assertIs (param.annotation, float) + self.assertIs (param.annotation, 'float') # Don't test for the default, since it's set to MISSING. self.assertEqual(param.kind, inspect.Parameter.POSITIONAL_OR_KEYWORD) self.assertRaises(StopIteration, next, params) @@ -1997,7 +1998,7 @@ def test_docstring_one_field(self): class C: x: int - self.assertDocStrEqual(C.__doc__, "C(x:int)") + self.assertDocStrEqual(C.__doc__, "C(x:'int')") def test_docstring_two_fields(self): @dataclass @@ -2005,7 +2006,7 @@ class C: x: int y: int - self.assertDocStrEqual(C.__doc__, "C(x:int, y:int)") + self.assertDocStrEqual(C.__doc__, "C(x:'int', y:'int')") def test_docstring_three_fields(self): @dataclass @@ -2014,49 +2015,49 @@ class C: y: int z: str - self.assertDocStrEqual(C.__doc__, "C(x:int, y:int, z:str)") + self.assertDocStrEqual(C.__doc__, "C(x:'int', y:'int', z:'str')") def test_docstring_one_field_with_default(self): @dataclass class C: x: int = 3 - self.assertDocStrEqual(C.__doc__, "C(x:int=3)") + self.assertDocStrEqual(C.__doc__, "C(x:'int'=3)") def test_docstring_one_field_with_default_none(self): @dataclass class C: x: Union[int, type(None)] = None - self.assertDocStrEqual(C.__doc__, "C(x:Optional[int]=None)") + self.assertDocStrEqual(C.__doc__, "C(x:'Union[int, type(None)]'=None)") def test_docstring_list_field(self): @dataclass class C: x: List[int] - self.assertDocStrEqual(C.__doc__, "C(x:List[int])") + self.assertDocStrEqual(C.__doc__, "C(x:'List[int]')") def test_docstring_list_field_with_default_factory(self): @dataclass class C: x: List[int] = field(default_factory=list) - self.assertDocStrEqual(C.__doc__, "C(x:List[int]=)") + self.assertDocStrEqual(C.__doc__, "C(x:'List[int]'=)") def test_docstring_deque_field(self): @dataclass class C: x: deque - self.assertDocStrEqual(C.__doc__, "C(x:collections.deque)") + self.assertDocStrEqual(C.__doc__, "C(x:'deque')") def test_docstring_deque_field_with_default_factory(self): @dataclass class C: x: deque = field(default_factory=deque) - self.assertDocStrEqual(C.__doc__, "C(x:collections.deque=)") + self.assertDocStrEqual(C.__doc__, "C(x:'deque'=)") class TestInit(unittest.TestCase): @@ -2805,13 +2806,10 @@ class C: class TestStringAnnotations(unittest.TestCase): def test_classvar(self): - # Some expressions recognized as ClassVar really aren't. But - # if you're using string annotations, it's not an exact - # science. # These tests assume that both "import typing" and "from # typing import *" have been run in this file. for typestr in ('ClassVar[int]', - 'ClassVar [int]' + 'ClassVar [int]', ' ClassVar [int]', 'ClassVar', ' ClassVar ', @@ -2822,17 +2820,9 @@ def test_classvar(self): 'typing. ClassVar[str]', 'typing.ClassVar [str]', 'typing.ClassVar [ str]', - - # Not syntactically valid, but these will - # be treated as ClassVars. - 'typing.ClassVar.[int]', - 'typing.ClassVar+', ): with self.subTest(typestr=typestr): - @dataclass - class C: - x: typestr - + C = dataclass(type("C", (), {"__annotations__": {"x": typestr}})) # x is a ClassVar, so C() takes no args. C() @@ -2853,9 +2843,7 @@ def test_isnt_classvar(self): 'typingxClassVar[str]', ): with self.subTest(typestr=typestr): - @dataclass - class C: - x: typestr + C = dataclass(type("C", (), {"__annotations__": {"x": typestr}})) # x is not a ClassVar, so C() takes one arg. self.assertEqual(C(10).x, 10) @@ -2882,9 +2870,8 @@ def test_initvar(self): 'dataclasses.InitVar+', ): with self.subTest(typestr=typestr): - @dataclass - class C: - x: typestr + C = dataclass(type("C", (), {"__annotations__": {"x": typestr}})) + # x is an InitVar, so doesn't create a member. with self.assertRaisesRegex(AttributeError, @@ -2898,30 +2885,22 @@ def test_isnt_initvar(self): 'typing.xInitVar[int]', ): with self.subTest(typestr=typestr): - @dataclass - class C: - x: typestr + C = dataclass(type("C", (), {"__annotations__": {"x": typestr}})) # x is not an InitVar, so there will be a member x. self.assertEqual(C(10).x, 10) def test_classvar_module_level_import(self): from test import dataclass_module_1 - from test import dataclass_module_1_str from test import dataclass_module_2 - from test import dataclass_module_2_str - for m in (dataclass_module_1, dataclass_module_1_str, - dataclass_module_2, dataclass_module_2_str, - ): + for m in (dataclass_module_1, + dataclass_module_2): with self.subTest(m=m): # There's a difference in how the ClassVars are # interpreted when using string annotations or # not. See the imported modules for details. - if m.USING_STRINGS: - c = m.CV(10) - else: - c = m.CV() + c = m.CV(10) self.assertEqual(c.cv0, 20) @@ -2937,14 +2916,9 @@ def test_classvar_module_level_import(self): # not an instance field. getattr(c, field_name) - if m.USING_STRINGS: - # iv4 is interpreted as a normal field. - self.assertIn('not_iv4', c.__dict__) - self.assertEqual(c.not_iv4, 4) - else: - # iv4 is interpreted as an InitVar, so it - # won't exist on the instance. - self.assertNotIn('not_iv4', c.__dict__) + # iv4 is interpreted as a normal field. + self.assertIn('not_iv4', c.__dict__) + self.assertEqual(c.not_iv4, 4) def test_text_annotations(self): from test import dataclass_textanno From 1d77917f4397a0ec5f7f2f80e52158dc4ba1b2ec Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 29 May 2020 12:01:30 +0300 Subject: [PATCH 10/18] Initial attempt for typing fix --- Lib/test/test_typing.py | 22 +++++++++++----------- Lib/typing.py | 9 +++++++-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 42aa430c5e107e..20628bf37afe62 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -349,7 +349,7 @@ def test_empty(self): def test_no_eval_union(self): u = Union[int, str] def f(x: u): ... - self.assertIs(get_type_hints(f)['x'], u) + self.assertIs(get_type_hints(f, globals(), locals())['x'], u) def test_function_repr_union(self): def fun() -> int: ... @@ -2853,7 +2853,7 @@ def test_get_type_hints_classes(self): {'x': int, 'y': int}) self.assertEqual(gth(mod_generics_cache.B), {'my_inner_a1': mod_generics_cache.B.A, - 'my_inner_a2': mod_generics_cache.B.A, + 'my_inner_a2': mod_generics_cache.A, 'my_outer_a': mod_generics_cache.A}) def test_respect_no_type_check(self): @@ -3641,7 +3641,7 @@ def test_annotation_usage(self): self.assertEqual(tim.cool, 9000) self.assertEqual(CoolEmployee.__name__, 'CoolEmployee') self.assertEqual(CoolEmployee._fields, ('name', 'cool')) - self.assertEqual(CoolEmployee.__annotations__, + self.assertEqual(gth(CoolEmployee), collections.OrderedDict(name=str, cool=int)) def test_annotation_usage_with_default(self): @@ -3655,7 +3655,7 @@ def test_annotation_usage_with_default(self): self.assertEqual(CoolEmployeeWithDefault.__name__, 'CoolEmployeeWithDefault') self.assertEqual(CoolEmployeeWithDefault._fields, ('name', 'cool')) - self.assertEqual(CoolEmployeeWithDefault.__annotations__, + self.assertEqual(gth(CoolEmployeeWithDefault), dict(name=str, cool=int)) self.assertEqual(CoolEmployeeWithDefault._field_defaults, dict(cool=0)) @@ -3823,7 +3823,7 @@ def test_typeddict_errors(self): def test_py36_class_syntax_usage(self): self.assertEqual(LabelPoint2D.__name__, 'LabelPoint2D') self.assertEqual(LabelPoint2D.__module__, __name__) - self.assertEqual(LabelPoint2D.__annotations__, {'x': int, 'y': int, 'label': str}) + self.assertEqual(gth(LabelPoint2D), {'x': int, 'y': int, 'label': str}) self.assertEqual(LabelPoint2D.__bases__, (dict,)) self.assertEqual(LabelPoint2D.__total__, True) self.assertNotIsSubclass(LabelPoint2D, typing.Sequence) @@ -3882,11 +3882,11 @@ class Cat(Animal): assert BaseAnimal.__required_keys__ == frozenset(['name']) assert BaseAnimal.__optional_keys__ == frozenset([]) - assert BaseAnimal.__annotations__ == {'name': str} + assert gth(BaseAnimal) == {'name': str} assert Animal.__required_keys__ == frozenset(['name']) assert Animal.__optional_keys__ == frozenset(['tail', 'voice']) - assert Animal.__annotations__ == { + assert gth(Animal) == { 'name': str, 'tail': bool, 'voice': str, @@ -3894,7 +3894,7 @@ class Cat(Animal): assert Cat.__required_keys__ == frozenset(['name', 'fur_color']) assert Cat.__optional_keys__ == frozenset(['tail', 'voice']) - assert Cat.__annotations__ == { + assert gth(Cat) == { 'fur_color': str, 'name': str, 'tail': bool, @@ -3915,7 +3915,7 @@ def test_io(self): def stuff(a: IO) -> AnyStr: return a.readline() - a = stuff.__annotations__['a'] + a = gth(stuff)['a'] self.assertEqual(a.__parameters__, (AnyStr,)) def test_textio(self): @@ -3923,7 +3923,7 @@ def test_textio(self): def stuff(a: TextIO) -> str: return a.readline() - a = stuff.__annotations__['a'] + a = gth(stuff)['a'] self.assertEqual(a.__parameters__, ()) def test_binaryio(self): @@ -3931,7 +3931,7 @@ def test_binaryio(self): def stuff(a: BinaryIO) -> bytes: return a.readline() - a = stuff.__annotations__['a'] + a = gth(stuff)['a'] self.assertEqual(a.__parameters__, ()) def test_io_submodule(self): diff --git a/Lib/typing.py b/Lib/typing.py index 8e51e423b1d952..60e084907f7e27 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -1366,6 +1366,11 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): locals, respectively. """ + def resolve(value, globalns, localns=localns): + # double resolve forward refs + value = _eval_type(value, globalns, localns) + return _eval_type(value, globalns, localns) + if getattr(obj, '__no_type_check__', None): return {} # Classes require a special treatment. @@ -1382,7 +1387,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): value = type(None) if isinstance(value, str): value = ForwardRef(value, is_argument=False) - value = _eval_type(value, base_globals, localns) + value = resolve(value, base_globals) hints[name] = value return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} @@ -1414,7 +1419,7 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): value = type(None) if isinstance(value, str): value = ForwardRef(value) - value = _eval_type(value, globalns, localns) + value = resolve(value, globalns) if name in defaults and defaults[name] is None: value = Optional[value] hints[name] = value From 6bd5feeb2040ea1be97313f27d0df4c03163470b Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 29 May 2020 15:48:22 +0300 Subject: [PATCH 11/18] Rename test, apply doc suggestion --- Doc/reference/compound_stmts.rst | 5 ++--- Lib/test/test_annotations.py | 5 ++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index 37e98970f0ca76..47e956655eb963 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -613,9 +613,8 @@ any valid Python expression. The presence of annotations does not change the semantics of a function. The annotation values are available as values of a dictionary keyed by the parameters' names in the :attr:`__annotations__` attribute of the function object. Used annnotations are preserved as strings at -runtime which enables postponed evaluation. In this case annotations may be -evaluated in In this case annotations may be evaluated in a different order -than they appear in the source code. +runtime which enables postponed evaluation (annotations may be evaluated in a +different order than they appear in the source code). .. index:: pair: lambda; expression diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index fec54c8b4206a6..261366eeaa54de 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -1,11 +1,10 @@ import unittest -from textwrap import dedent import sys +from textwrap import dedent -class AnnotationsFutureTestCase(unittest.TestCase): +class PostponedAnnotationsTestCase(unittest.TestCase): template = dedent( """ - from __future__ import annotations def f() -> {ann}: ... def g(arg: {ann}) -> None: From 1e2c3f826e9196ddc2fc457017cbaa8b1a42552d Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Fri, 2 Oct 2020 02:32:21 +0300 Subject: [PATCH 12/18] Adapt code into the latest changes --- Doc/reference/compound_stmts.rst | 3 +-- Lib/test/test_annotations.py | 1 + Lib/test/test_functools.py | 4 ++-- Lib/test/test_grammar.py | 4 ---- Lib/test/test_types.py | 4 ++-- Lib/test/test_typing.py | 2 +- Lib/typing.py | 20 +++++++++----------- 7 files changed, 16 insertions(+), 22 deletions(-) diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index 47e956655eb963..80c7abc8187ec1 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -613,8 +613,7 @@ any valid Python expression. The presence of annotations does not change the semantics of a function. The annotation values are available as values of a dictionary keyed by the parameters' names in the :attr:`__annotations__` attribute of the function object. Used annnotations are preserved as strings at -runtime which enables postponed evaluation (annotations may be evaluated in a -different order than they appear in the source code). +runtime which enables postponed evaluation. .. index:: pair: lambda; expression diff --git a/Lib/test/test_annotations.py b/Lib/test/test_annotations.py index 261366eeaa54de..3e6b709fb4f1e3 100644 --- a/Lib/test/test_annotations.py +++ b/Lib/test/test_annotations.py @@ -198,6 +198,7 @@ def test_annotations(self): eq('(((a, b)))', '(a, b)') eq("(x := 10)") eq("f'{(x := 10):=10}'") + eq("1 + 2") eq("1 + 2 + 3") def test_fstring_debug_annotations(self): diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 13303e8004a376..bee9f9112bf183 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -618,7 +618,7 @@ def check_wrapper(self, wrapper, wrapped, def _default_update(self): - def f(a:'This is a new annotation'): + def f(a: int): """This is a test""" pass f.attr = 'This is also a test' @@ -635,7 +635,7 @@ def test_default_update(self): self.assertEqual(wrapper.__name__, 'f') self.assertEqual(wrapper.__qualname__, f.__qualname__) self.assertEqual(wrapper.attr, 'This is also a test') - self.assertEqual(wrapper.__annotations__['a'], repr('This is a new annotation')) + self.assertEqual(wrapper.__annotations__['a'], 'int') self.assertNotIn('b', wrapper.__annotations__) @unittest.skipIf(sys.flags.optimize >= 2, diff --git a/Lib/test/test_grammar.py b/Lib/test/test_grammar.py index 441b0b2a1630e4..2f6716dfc9a130 100644 --- a/Lib/test/test_grammar.py +++ b/Lib/test/test_grammar.py @@ -636,10 +636,6 @@ def f(*x: str): pass self.assertEqual(f.__annotations__, {'x': 'str'}) def f(**x: float): pass self.assertEqual(f.__annotations__, {'x': 'float'}) - def f(x, y: 1+2): pass - self.assertEqual(f.__annotations__, {'y': '1 + 2'}) - def f(x, y: 1+2, /): pass - self.assertEqual(f.__annotations__, {'y': '1 + 2'}) def f(a, b: 1, c: 2, d): pass self.assertEqual(f.__annotations__, {'b': '1', 'c': '2'}) def f(a, b: 1, /, c: 2, d): pass diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 52a59d54f044d9..75c5eee42dc543 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -671,8 +671,8 @@ def test_or_type_operator_with_forward(self): ForwardBefore = 'Forward' | T def forward_after(x: ForwardAfter[int]) -> None: ... def forward_before(x: ForwardBefore[int]) -> None: ... - assert typing.get_args(typing.get_type_hints(forward_after)['x']) == (int, Forward) - assert typing.get_args(typing.get_type_hints(forward_before)['x']) == (int, Forward) + assert typing.get_args(typing.get_type_hints(forward_after, localns=locals())['x']) == (int, Forward) + assert typing.get_args(typing.get_type_hints(forward_before, localns=locals())['x']) == (int, Forward) def test_or_type_operator_with_Protocol(self): class Proto(typing.Protocol): diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 20628bf37afe62..4bef42f4f32fc7 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -2849,7 +2849,7 @@ def test_get_type_hints_classes(self): self.assertEqual(gth(HasForeignBaseClass), {'some_xrepr': XRepr, 'other_a': mod_generics_cache.A, 'some_b': mod_generics_cache.B}) - self.assertEqual(gth(XRepr.__new__), + self.assertEqual(gth(XRepr), {'x': int, 'y': int}) self.assertEqual(gth(mod_generics_cache.B), {'my_inner_a1': mod_generics_cache.B.A, diff --git a/Lib/typing.py b/Lib/typing.py index 60e084907f7e27..95c412caff6820 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -18,6 +18,7 @@ """ from abc import abstractmethod, ABCMeta +import ast import collections import collections.abc import contextlib @@ -469,11 +470,13 @@ class ForwardRef(_Final, _root=True): def __init__(self, arg, is_argument=True): if not isinstance(arg, str): raise TypeError(f"Forward reference must be a string -- got {arg!r}") - # since annotations feature is now default, stringified annotations - # should be escaped from quotes, or this will result with double - # forward refs. - if arg[0] in "'\"" and arg[-1] in "'\"": + + # Double-stringified forward references is a result of activating + # 'annotations' future by default. This way, we eliminate them on + # the runtime. + if arg.startswith(("'", '\"')) and arg.endswith(("'", '"')): arg = arg[1:-1] + try: code = compile(arg, '', 'eval') except SyntaxError: @@ -1366,11 +1369,6 @@ def get_type_hints(obj, globalns=None, localns=None, include_extras=False): locals, respectively. """ - def resolve(value, globalns, localns=localns): - # double resolve forward refs - value = _eval_type(value, globalns, localns) - return _eval_type(value, globalns, localns) - if getattr(obj, '__no_type_check__', None): return {} # Classes require a special treatment. @@ -1387,7 +1385,7 @@ def resolve(value, globalns, localns=localns): value = type(None) if isinstance(value, str): value = ForwardRef(value, is_argument=False) - value = resolve(value, base_globals) + value = _eval_type(value, base_globals, localns) hints[name] = value return hints if include_extras else {k: _strip_annotations(t) for k, t in hints.items()} @@ -1419,7 +1417,7 @@ def resolve(value, globalns, localns=localns): value = type(None) if isinstance(value, str): value = ForwardRef(value) - value = resolve(value, globalns) + value = _eval_type(value, globalns, localns) if name in defaults and defaults[name] is None: value = Optional[value] hints[name] = value From 0a04da8a9d4bac80e0e912bbf8c62599dc3e65d1 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Sat, 3 Oct 2020 03:10:36 +0300 Subject: [PATCH 13/18] Support double-stringified annotations in dataclasses --- Lib/dataclasses.py | 6 ++++++ Lib/test/test_dataclasses.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index c06aad26b6bb8a..4ab7d515795473 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -652,6 +652,12 @@ def _is_type(annotation, cls, a_module, a_type, is_type_predicate): # a eval() penalty for every single field of every dataclass # that's defined. It was judged not worth it. + # Strip away the extra quotes as a result of double-stringifying when the + # 'annotations' feature became default. + if annotation.startswith(("'", '"')) and annotation.endswith(("'", '"')): + annotation = annotation[1:-1] + + match = _MODULE_IDENTIFIER_RE.match(annotation) if match: ns = None diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 27c0ca186e32a8..d3525b15b6e9ff 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -2805,6 +2805,13 @@ class C: class TestStringAnnotations(unittest.TestCase): + def test_double_stringification(self): + @dataclass + class T: + a: "typing.ClassVar[int]" + + T() + def test_classvar(self): # These tests assume that both "import typing" and "from # typing import *" have been run in this file. From 6d1feb50c8b2c7266f7dde704ac994d1ab0a64d2 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Sun, 4 Oct 2020 12:24:38 +0300 Subject: [PATCH 14/18] Use get_type_hints in inspect.signature --- Doc/library/inspect.rst | 2 +- Doc/reference/compound_stmts.rst | 7 +++-- Lib/dataclasses.py | 3 +-- Lib/inspect.py | 19 +++++++++++-- Lib/test/test_dataclasses.py | 27 ++++++++++++------- Lib/test/test_docxmlrpc.py | 4 +-- Lib/test/test_inspect.py | 14 +++++----- Lib/test/test_pydoc.py | 4 +-- Lib/typing.py | 2 +- .../2020-05-27-16-08-16.bpo-38605.rcs2uK.rst | 4 ++- Python/compile.c | 4 +-- 11 files changed, 56 insertions(+), 34 deletions(-) diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index 8fe8fad62f7345..d00a30ff004063 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -1005,7 +1005,7 @@ Classes and functions ... pass ... >>> formatargspec(*getfullargspec(f)) - "(a: 'int', b: 'float')" + '(a: int, b: float)' .. deprecated:: 3.5 Use :func:`signature` and diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index 80c7abc8187ec1..04a3948d0c9dcf 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -610,10 +610,9 @@ following the parameter name. Any parameter may have an annotation, even those ``*identifier`` or ``**identifier``. Functions may have "return" annotation of the form "``-> expression``" after the parameter list. These annotations can be any valid Python expression. The presence of annotations does not change the -semantics of a function. The annotation values are available as values of -a dictionary keyed by the parameters' names in the :attr:`__annotations__` -attribute of the function object. Used annnotations are preserved as strings at -runtime which enables postponed evaluation. +semantics of a function. The annotation values are available as string values +in a dictionary keyed by the parameters' names in the :attr:`__annotations__` +attribute of the function object. .. index:: pair: lambda; expression diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py index 4ab7d515795473..20d86191cd8866 100644 --- a/Lib/dataclasses.py +++ b/Lib/dataclasses.py @@ -657,7 +657,6 @@ def _is_type(annotation, cls, a_module, a_type, is_type_predicate): if annotation.startswith(("'", '"')) and annotation.endswith(("'", '"')): annotation = annotation[1:-1] - match = _MODULE_IDENTIFIER_RE.match(annotation) if match: ns = None @@ -998,7 +997,7 @@ def _process_class(cls, init, repr, eq, order, unsafe_hash, frozen): if not getattr(cls, '__doc__'): # Create a class doc-string. cls.__doc__ = (cls.__name__ + - str(inspect.signature(cls)).replace(' -> None', '')) + str(inspect.signature(cls)).replace(' -> NoneType', '')) return cls diff --git a/Lib/inspect.py b/Lib/inspect.py index 887a3424057b6e..ac127cbd725b9b 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -45,6 +45,7 @@ import tokenize import token import types +import typing import warnings import functools import builtins @@ -1877,7 +1878,10 @@ def _signature_is_functionlike(obj): code = getattr(obj, '__code__', None) defaults = getattr(obj, '__defaults__', _void) # Important to use _void ... kwdefaults = getattr(obj, '__kwdefaults__', _void) # ... and not None here - annotations = getattr(obj, '__annotations__', None) + try: + annotations = _get_type_hints(obj) + except AttributeError: + annotations = None return (isinstance(code, types.CodeType) and isinstance(name, str) and @@ -2118,6 +2122,16 @@ def p(name_node, default_node, default=empty): return cls(parameters, return_annotation=cls.empty) +def _get_type_hints(func): + try: + return typing.get_type_hints(func) + except Exception: + # First, try to use the get_type_hints to resolve + # annotations. But for keeping the behavior intact + # if there was a problem with that (like the namespace + # can't resolve some annotation) continue to use + # string annotations + return func.__annotations__ def _signature_from_builtin(cls, func, skip_bound_arg=True): """Private helper function to get signature for @@ -2161,7 +2175,8 @@ def _signature_from_function(cls, func, skip_bound_arg=True): positional = arg_names[:pos_count] keyword_only_count = func_code.co_kwonlyargcount keyword_only = arg_names[pos_count:pos_count + keyword_only_count] - annotations = func.__annotations__ + annotations = _get_type_hints(func) + defaults = func.__defaults__ kwdefaults = func.__kwdefaults__ diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index d3525b15b6e9ff..87cae2a25c6ec7 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -1998,7 +1998,7 @@ def test_docstring_one_field(self): class C: x: int - self.assertDocStrEqual(C.__doc__, "C(x:'int')") + self.assertDocStrEqual(C.__doc__, "C(x:int)") def test_docstring_two_fields(self): @dataclass @@ -2006,7 +2006,7 @@ class C: x: int y: int - self.assertDocStrEqual(C.__doc__, "C(x:'int', y:'int')") + self.assertDocStrEqual(C.__doc__, "C(x:int, y:int)") def test_docstring_three_fields(self): @dataclass @@ -2015,49 +2015,49 @@ class C: y: int z: str - self.assertDocStrEqual(C.__doc__, "C(x:'int', y:'int', z:'str')") + self.assertDocStrEqual(C.__doc__, "C(x:int, y:int, z:str)") def test_docstring_one_field_with_default(self): @dataclass class C: x: int = 3 - self.assertDocStrEqual(C.__doc__, "C(x:'int'=3)") + self.assertDocStrEqual(C.__doc__, "C(x:int=3)") def test_docstring_one_field_with_default_none(self): @dataclass class C: x: Union[int, type(None)] = None - self.assertDocStrEqual(C.__doc__, "C(x:'Union[int, type(None)]'=None)") + self.assertDocStrEqual(C.__doc__, "C(x:Optional[int]=None)") def test_docstring_list_field(self): @dataclass class C: x: List[int] - self.assertDocStrEqual(C.__doc__, "C(x:'List[int]')") + self.assertDocStrEqual(C.__doc__, "C(x:List[int])") def test_docstring_list_field_with_default_factory(self): @dataclass class C: x: List[int] = field(default_factory=list) - self.assertDocStrEqual(C.__doc__, "C(x:'List[int]'=)") + self.assertDocStrEqual(C.__doc__, "C(x:List[int]=)") def test_docstring_deque_field(self): @dataclass class C: x: deque - self.assertDocStrEqual(C.__doc__, "C(x:'deque')") + self.assertDocStrEqual(C.__doc__, "C(x:collections.deque)") def test_docstring_deque_field_with_default_factory(self): @dataclass class C: x: deque = field(default_factory=deque) - self.assertDocStrEqual(C.__doc__, "C(x:'deque'=)") + self.assertDocStrEqual(C.__doc__, "C(x:collections.deque=)") class TestInit(unittest.TestCase): @@ -2827,6 +2827,12 @@ def test_classvar(self): 'typing. ClassVar[str]', 'typing.ClassVar [str]', 'typing.ClassVar [ str]', + # Double stringified + '"typing.ClassVar[int]"', + # Not syntactically valid, but these will + # be treated as ClassVars. + 'typing.ClassVar.[int]', + 'typing.ClassVar+', ): with self.subTest(typestr=typestr): C = dataclass(type("C", (), {"__annotations__": {"x": typestr}})) @@ -2870,7 +2876,8 @@ def test_initvar(self): 'dataclasses. InitVar[str]', 'dataclasses.InitVar [str]', 'dataclasses.InitVar [ str]', - + # Double stringified + '"dataclasses.InitVar[int]"', # Not syntactically valid, but these will # be treated as InitVars. 'dataclasses.InitVar.[int]', diff --git a/Lib/test/test_docxmlrpc.py b/Lib/test/test_docxmlrpc.py index 69d52427354a1e..7d3e30cbee964a 100644 --- a/Lib/test/test_docxmlrpc.py +++ b/Lib/test/test_docxmlrpc.py @@ -188,9 +188,9 @@ def test_annotations(self): b'
Use function annotations.
') self.assertIn( (b'
annotation' - b'(x: \'int\')
' + docstring + b'
\n' + b'(x: int)' + docstring + b'\n' b'
' - b'method_annotation(x: \'bytes\')
'), + b'method_annotation(x: bytes)'), response.read()) def test_server_title_escape(self): diff --git a/Lib/test/test_inspect.py b/Lib/test/test_inspect.py index 82564644737dc3..71c4f27d27b982 100644 --- a/Lib/test/test_inspect.py +++ b/Lib/test/test_inspect.py @@ -861,8 +861,8 @@ def test_getfullargspec(self): formatted='(*arg1, arg2=1)') self.assertFullArgSpecEquals(mod2.annotated, ['arg1'], - ann_e={'arg1' : 'list'}, - formatted="(arg1: 'list')") + ann_e={'arg1' : list}, + formatted="(arg1: list)") self.assertFullArgSpecEquals(mod2.keyword_only_arg, [], kwonlyargs_e=['arg'], formatted='(*, arg)') @@ -2218,9 +2218,9 @@ def test_signature_on_wkwonly(self): def test(*, a:float, b:str) -> int: pass self.assertEqual(self.signature(test), - ((('a', ..., 'float', "keyword_only"), - ('b', ..., 'str', "keyword_only")), - 'int')) + ((('a', ..., float, "keyword_only"), + ('b', ..., str, "keyword_only")), + int)) def test_signature_on_complex_args(self): def test(a, b:'foo'=10, *args:'bar', spam:'baz', ham=123, **kwargs:int): @@ -2466,7 +2466,7 @@ def __call__(*, a): self.assertEqual(self.signature(Test().m1), ((('arg1', ..., ..., "positional_or_keyword"), ('arg2', 1, ..., "positional_or_keyword")), - 'int')) + int)) self.assertEqual(self.signature(Test().m2), ((('args', ..., ..., "var_positional"),), @@ -2490,7 +2490,7 @@ def m1d(*args, **kwargs): self.assertEqual(self.signature(m1d), ((('arg1', ..., ..., "positional_or_keyword"), ('arg2', 1, ..., "positional_or_keyword")), - 'int')) + int)) def test_signature_on_classmethod(self): class Test: diff --git a/Lib/test/test_pydoc.py b/Lib/test/test_pydoc.py index aae81e62cbaeba..2f502627f4d0a2 100644 --- a/Lib/test/test_pydoc.py +++ b/Lib/test/test_pydoc.py @@ -1055,8 +1055,8 @@ def foo(data: typing.List[typing.Any], T = typing.TypeVar('T') class C(typing.Generic[T], typing.Mapping[int, str]): ... self.assertEqual(pydoc.render_doc(foo).splitlines()[-1], - 'f\x08fo\x08oo\x08o(data: \'typing.List[typing.Any]\', x: \'int\')' - ' -> \'typing.Iterator[typing.Tuple[int, typing.Any]]\'') + 'f\x08fo\x08oo\x08o(data: List[Any], x: int)' + ' -> Iterator[Tuple[int, Any]]') self.assertEqual(pydoc.render_doc(C).splitlines()[2], 'class C\x08C(collections.abc.Mapping, typing.Generic)') diff --git a/Lib/typing.py b/Lib/typing.py index 95c412caff6820..4cf33c1ae92659 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -472,7 +472,7 @@ def __init__(self, arg, is_argument=True): raise TypeError(f"Forward reference must be a string -- got {arg!r}") # Double-stringified forward references is a result of activating - # 'annotations' future by default. This way, we eliminate them on + # the 'annotations' future by default. This way, we eliminate them in # the runtime. if arg.startswith(("'", '\"')) and arg.endswith(("'", '"')): arg = arg[1:-1] diff --git a/Misc/NEWS.d/next/Core and Builtins/2020-05-27-16-08-16.bpo-38605.rcs2uK.rst b/Misc/NEWS.d/next/Core and Builtins/2020-05-27-16-08-16.bpo-38605.rcs2uK.rst index 43d9a402bf70cf..cbfe6e23523bbe 100644 --- a/Misc/NEWS.d/next/Core and Builtins/2020-05-27-16-08-16.bpo-38605.rcs2uK.rst +++ b/Misc/NEWS.d/next/Core and Builtins/2020-05-27-16-08-16.bpo-38605.rcs2uK.rst @@ -1 +1,3 @@ -Enable postponed evaluation of annotations (:pep:`563`) by default. +Enable ``from __future__ import annotations`` (:pep:`563`) by default. +The values found in :attr:`__annotations__` dicts are now strings, e.g. +``{"x": "int"}`` instead of ``{"x": int}``. diff --git a/Python/compile.c b/Python/compile.c index 85729c8d13bd69..ddd2a049629c1f 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -2026,7 +2026,7 @@ compiler_visit_argannotation(struct compiler *c, identifier id, { if (annotation) { PyObject *mangled; - VISIT(c, annexpr, annotation) + VISIT(c, annexpr, annotation); mangled = _Py_Mangle(c->u->u_private, id); if (!mangled) return 0; @@ -5256,7 +5256,7 @@ compiler_annassign(struct compiler *c, stmt_ty s) if (s->v.AnnAssign.simple && (c->u->u_scope_type == COMPILER_SCOPE_MODULE || c->u->u_scope_type == COMPILER_SCOPE_CLASS)) { - VISIT(c, annexpr, s->v.AnnAssign.annotation) + VISIT(c, annexpr, s->v.AnnAssign.annotation); ADDOP_NAME(c, LOAD_NAME, __annotations__, names); mangled = _Py_Mangle(c->u->u_private, targ->v.Name.id); ADDOP_LOAD_CONST_NEW(c, mangled); From 86181e41c167b2325219aeda96647f54c4102277 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Sun, 4 Oct 2020 13:01:06 +0300 Subject: [PATCH 15/18] remove reduntant test --- Lib/test/test_dataclasses.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Lib/test/test_dataclasses.py b/Lib/test/test_dataclasses.py index 87cae2a25c6ec7..92d5569cce5f62 100644 --- a/Lib/test/test_dataclasses.py +++ b/Lib/test/test_dataclasses.py @@ -2805,13 +2805,6 @@ class C: class TestStringAnnotations(unittest.TestCase): - def test_double_stringification(self): - @dataclass - class T: - a: "typing.ClassVar[int]" - - T() - def test_classvar(self): # These tests assume that both "import typing" and "from # typing import *" have been run in this file. From 427628456aee18c67e24978494e715b8a7e0ef8f Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 6 Oct 2020 01:23:33 +0300 Subject: [PATCH 16/18] Add what's new entry --- Doc/whatsnew/3.10.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index f74dd1aa247a34..b652af7c9e4420 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -70,6 +70,19 @@ Summary -- Release highlights New Features ============ +.. _whatsnew310-pep563: + +PEP 563: Postponed Evaluation of Annotations Becomes Default +------------------------------------------------------------ + +In Python 3.7, postponed evaluation of annotations added, to be enabled with +``from __future__ import annotations`` directive. In 3.10 this became the +default behavior, even without that future directive. With this being default +all annotations stored in :attr:`__annotations__` will be strings. If needed, +annotations can be resolved at runtime using :func:`typing.get_type_hints`. +See :pep:`563` for a full description. (Contributed by Batuhan Taskaya in +:issue:`38605`.) + * The :class:`int` type has a new method :meth:`int.bit_count`, returning the number of ones in the binary expansion of a given integer, also known as the population count. (Contributed by Niklas Fiekas in :issue:`29882`.) From 5ae4bd15733860892dd8ec9f612b216e94135440 Mon Sep 17 00:00:00 2001 From: Batuhan Taskaya Date: Tue, 6 Oct 2020 01:41:33 +0300 Subject: [PATCH 17/18] mention about inspect.signature --- Doc/whatsnew/3.10.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index b652af7c9e4420..46889181cdf279 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -80,7 +80,9 @@ In Python 3.7, postponed evaluation of annotations added, to be enabled with default behavior, even without that future directive. With this being default all annotations stored in :attr:`__annotations__` will be strings. If needed, annotations can be resolved at runtime using :func:`typing.get_type_hints`. -See :pep:`563` for a full description. (Contributed by Batuhan Taskaya in +Also, the :func:`inspect.signature` will try to resolve types from now on, +and when it fails it will fall back to showing the string annotations. See +:pep:`563` for a full description. (Contributed by Batuhan Taskaya in :issue:`38605`.) * The :class:`int` type has a new method :meth:`int.bit_count`, returning the From 1e73aa72331cc6be5ec31d7dd93784072b8360bb Mon Sep 17 00:00:00 2001 From: Guido van Rossum Date: Tue, 6 Oct 2020 11:22:34 -0700 Subject: [PATCH 18/18] Tweak what's new words --- Doc/whatsnew/3.10.rst | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index 46889181cdf279..ad30d8aed21745 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -75,15 +75,17 @@ New Features PEP 563: Postponed Evaluation of Annotations Becomes Default ------------------------------------------------------------ -In Python 3.7, postponed evaluation of annotations added, to be enabled with -``from __future__ import annotations`` directive. In 3.10 this became the -default behavior, even without that future directive. With this being default -all annotations stored in :attr:`__annotations__` will be strings. If needed, -annotations can be resolved at runtime using :func:`typing.get_type_hints`. -Also, the :func:`inspect.signature` will try to resolve types from now on, -and when it fails it will fall back to showing the string annotations. See -:pep:`563` for a full description. (Contributed by Batuhan Taskaya in -:issue:`38605`.) +In Python 3.7, postponed evaluation of annotations was added, +to be enabled with a ``from __future__ import annotations`` +directive. In 3.10 this became the default behavior, even +without that future directive. With this being default, all +annotations stored in :attr:`__annotations__` will be strings. +If needed, annotations can be resolved at runtime using +:func:`typing.get_type_hints`. See :pep:`563` for a full +description. Also, the :func:`inspect.signature` will try to +resolve types from now on, and when it fails it will fall back to +showing the string annotations. (Contributed by Batuhan Taskaya +in :issue:`38605`.) * The :class:`int` type has a new method :meth:`int.bit_count`, returning the number of ones in the binary expansion of a given integer, also known