summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorGuido van Rossum <guido@python.org>2018-01-26 16:20:18 (GMT)
committerƁukasz Langa <lukasz@langa.pl>2018-01-26 16:20:18 (GMT)
commit95e4d589137260530e18ef98a2ed84ee3ec57e12 (patch)
tree9d0c3bc48158e9f0c83f1b9cb509c1fbebd9cfde /Lib
parentd7773d92bd11640a8c950d6c36a9cef1cee36f96 (diff)
downloadcpython-95e4d589137260530e18ef98a2ed84ee3ec57e12.zip
cpython-95e4d589137260530e18ef98a2ed84ee3ec57e12.tar.gz
cpython-95e4d589137260530e18ef98a2ed84ee3ec57e12.tar.bz2
String annotations [PEP 563] (#4390)
* Document `from __future__ import annotations` * Provide plumbing and tests for `from __future__ import annotations` * Implement unparsing the AST back to string form This is required for PEP 563 and as such only implements a part of the unparsing process that covers expressions.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/__future__.py20
-rw-r--r--Lib/test/test_future.py163
2 files changed, 176 insertions, 7 deletions
diff --git a/Lib/__future__.py b/Lib/__future__.py
index 63b2be3..ce8bed7 100644
--- a/Lib/__future__.py
+++ b/Lib/__future__.py
@@ -57,13 +57,14 @@ all_feature_names = [
"unicode_literals",
"barry_as_FLUFL",
"generator_stop",
+ "annotations",
]
__all__ = ["all_feature_names"] + all_feature_names
-# The CO_xxx symbols are defined here under the same names used by
-# compile.h, so that an editor search will find them here. However,
-# they're not exported in __all__, because they don't really belong to
+# The CO_xxx symbols are defined here under the same names defined in
+# code.h and used by compile.h, so that an editor search will find them here.
+# However, they're not exported in __all__, because they don't really belong to
# this module.
CO_NESTED = 0x0010 # nested_scopes
CO_GENERATOR_ALLOWED = 0 # generators (obsolete, was 0x1000)
@@ -74,6 +75,7 @@ CO_FUTURE_PRINT_FUNCTION = 0x10000 # print function
CO_FUTURE_UNICODE_LITERALS = 0x20000 # unicode string literals
CO_FUTURE_BARRY_AS_BDFL = 0x40000
CO_FUTURE_GENERATOR_STOP = 0x80000 # StopIteration becomes RuntimeError in generators
+CO_FUTURE_ANNOTATIONS = 0x100000 # annotations become strings at runtime
class _Feature:
def __init__(self, optionalRelease, mandatoryRelease, compiler_flag):
@@ -132,9 +134,13 @@ unicode_literals = _Feature((2, 6, 0, "alpha", 2),
CO_FUTURE_UNICODE_LITERALS)
barry_as_FLUFL = _Feature((3, 1, 0, "alpha", 2),
- (3, 9, 0, "alpha", 0),
- CO_FUTURE_BARRY_AS_BDFL)
+ (3, 9, 0, "alpha", 0),
+ CO_FUTURE_BARRY_AS_BDFL)
generator_stop = _Feature((3, 5, 0, "beta", 1),
- (3, 7, 0, "alpha", 0),
- CO_FUTURE_GENERATOR_STOP)
+ (3, 7, 0, "alpha", 0),
+ CO_FUTURE_GENERATOR_STOP)
+
+annotations = _Feature((3, 7, 0, "beta", 1),
+ (4, 0, 0, "alpha", 0),
+ CO_FUTURE_ANNOTATIONS)
diff --git a/Lib/test/test_future.py b/Lib/test/test_future.py
index 2f1c410..29c4632 100644
--- a/Lib/test/test_future.py
+++ b/Lib/test/test_future.py
@@ -1,7 +1,9 @@
# Test various flavors of legal and illegal future statements
+from functools import partial
import unittest
from test import support
+from textwrap import dedent
import os
import re
@@ -102,6 +104,167 @@ class FutureTest(unittest.TestCase):
exec("from __future__ import unicode_literals; x = ''", {}, scope)
self.assertIsInstance(scope["x"], str)
+class AnnotationsFutureTestCase(unittest.TestCase):
+ template = dedent(
+ """
+ from __future__ import annotations
+ def f() -> {ann}:
+ ...
+ def g(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']
+ var_ann1 = scope['__annotations__']['var']
+ var_ann2 = scope['__annotations__']['var2']
+ self.assertEqual(func_ret_ann, 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("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 or (Name2 and Name3)')
+ eq('(Name1 and Name2) or (Name3 and Name4)')
+ eq('Name1 or (Name2 and Name3) or Name4')
+ eq('v1 << 2')
+ eq('1 >> v2')
+ eq(r'1 % finished')
+ eq('((1 + v2) - (v3 * 4)) ^ (((5 ** v6) / 7) // 8)')
+ eq('not great')
+ eq('~great')
+ eq('+value')
+ eq('-1')
+ eq('(~int) and (not ((v1 ^ (123 + v2)) | True))')
+ 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=(v1 << 2), e='str', **kwargs: a + b")
+ eq('1 if True else 2')
+ eq('(str or None) if True else (str or bytes or None)')
+ eq('(str or None) if (1 if True else 2) else (str or bytes or None)')
+ 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'}, (True or False), (+value), 'string', b'bytes') or None")
+ eq("()")
+ eq("(1,)")
+ eq("(1, 2)")
+ eq("(1, 2, 3)")
+ eq("[]")
+ eq("[1, 2, 3, 4, 5, 6, 7, 8, 9, (10 or A), (11 or B), (12 or 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(r"{i: 0 for i in (1, 2, 3)}")
+ eq("{i: j for i, j in ((1, 'a'), (2, 'b'), (3, 'c'))}")
+ 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, another, kwarg='hey', **kwargs)")
+ 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("tuple[str, ...]")
+ 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('(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)}}'")
+
+ def test_annotations_inexact(self):
+ """Source formatting is not always preserved
+
+ This is due to reconstruction from AST. We *need to* put the parens
+ in nested expressions because we don't know if the source code
+ had them in the first place or not.
+ """
+ eq = partial(self.assertAnnotationEqual, drop_parens=True)
+ eq('Name1 and Name2 or Name3')
+ eq('Name1 or Name2 and Name3')
+ eq('Name1 and Name2 or Name3 and Name4')
+ eq('Name1 or Name2 and Name3 or Name4')
+ eq('1 + v2 - v3 * 4 ^ v5 ** 6 / 7 // 8')
+ eq('~int and not v1 ^ 123 + v2 | True')
+ eq('str or None if True else str or bytes or None')
+ eq("{'2.7': dead, '3.7': long_live or die_hard}")
+ eq("{'2.7', '3.6', '3.7', '3.8', '3.9', '4.0' if gilectomy else '3.10'}")
+ eq("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10 or A, 11 or B, 12 or C]")
+ # Consequently, we always drop unnecessary parens if they were given in
+ # the outer scope:
+ some_name = self.getActual("(SomeName)")
+ self.assertEqual(some_name, 'SomeName')
+ # Interestingly, in the case of tuples (and generator expressions) the
+ # parens are *required* by the Python syntax in the annotation context.
+ # But there's no point storing that detail in __annotations__ so we're
+ # fine with the parens-less form.
+ eq = partial(self.assertAnnotationEqual, is_tuple=True)
+ eq("(Good, Bad, Ugly)")
+ 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("(*starred)")
+ eq('(yield from outside_of_generator)')
+ eq('(yield)')
+ eq('(await some.complicated[0].call(with_args=(True or (1 is not 1))))')
if __name__ == "__main__":