From 89cc932160ad92cce163b03e26480f783d3b952e Mon Sep 17 00:00:00 2001 From: Padraic Fanning Date: Thu, 22 Apr 2021 20:30:44 -0400 Subject: [PATCH 1/2] Add test_type_comments from CPython 3.8 --- Lib/test/test_type_comments.py | 412 +++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 Lib/test/test_type_comments.py diff --git a/Lib/test/test_type_comments.py b/Lib/test/test_type_comments.py new file mode 100644 index 000000000..43be54efd --- /dev/null +++ b/Lib/test/test_type_comments.py @@ -0,0 +1,412 @@ +import ast +import sys +import unittest + + +funcdef = """\ +def foo(): + # type: () -> int + pass + +def bar(): # type: () -> None + pass +""" + +asyncdef = """\ +async def foo(): + # type: () -> int + return await bar() + +async def bar(): # type: () -> int + return await bar() +""" + +asyncvar = """\ +async = 12 +await = 13 +""" + +asynccomp = """\ +async def foo(xs): + [x async for x in xs] +""" + +matmul = """\ +a = b @ c +""" + +fstring = """\ +a = 42 +f"{a}" +""" + +underscorednumber = """\ +a = 42_42_42 +""" + +redundantdef = """\ +def foo(): # type: () -> int + # type: () -> str + return '' +""" + +nonasciidef = """\ +def foo(): + # type: () -> àçčéñt + pass +""" + +forstmt = """\ +for a in []: # type: int + pass +""" + +withstmt = """\ +with context() as a: # type: int + pass +""" + +vardecl = """\ +a = 0 # type: int +""" + +ignores = """\ +def foo(): + pass # type: ignore + +def bar(): + x = 1 # type: ignore + +def baz(): + pass # type: ignore[excuse] + pass # type: ignore=excuse + pass # type: ignore [excuse] + x = 1 # type: ignore whatever +""" + +# Test for long-form type-comments in arguments. A test function +# named 'fabvk' would have two positional args, a and b, plus a +# var-arg *v, plus a kw-arg **k. It is verified in test_longargs() +# that it has exactly these arguments, no more, no fewer. +longargs = """\ +def fa( + a = 1, # type: A +): + pass + +def fa( + a = 1 # type: A +): + pass + +def fa( + a = 1, # type: A + / +): + pass + +def fab( + a, # type: A + b, # type: B +): + pass + +def fab( + a, # type: A + /, + b, # type: B +): + pass + +def fab( + a, # type: A + b # type: B +): + pass + +def fv( + *v, # type: V +): + pass + +def fv( + *v # type: V +): + pass + +def fk( + **k, # type: K +): + pass + +def fk( + **k # type: K +): + pass + +def fvk( + *v, # type: V + **k, # type: K +): + pass + +def fvk( + *v, # type: V + **k # type: K +): + pass + +def fav( + a, # type: A + *v, # type: V +): + pass + +def fav( + a, # type: A + /, + *v, # type: V +): + pass + +def fav( + a, # type: A + *v # type: V +): + pass + +def fak( + a, # type: A + **k, # type: K +): + pass + +def fak( + a, # type: A + /, + **k, # type: K +): + pass + +def fak( + a, # type: A + **k # type: K +): + pass + +def favk( + a, # type: A + *v, # type: V + **k, # type: K +): + pass + +def favk( + a, # type: A + /, + *v, # type: V + **k, # type: K +): + pass + +def favk( + a, # type: A + *v, # type: V + **k # type: K +): + pass +""" + + +class TypeCommentTests(unittest.TestCase): + + lowest = 4 # Lowest minor version supported + highest = sys.version_info[1] # Highest minor version + + def parse(self, source, feature_version=highest): + return ast.parse(source, type_comments=True, + feature_version=feature_version) + + def parse_all(self, source, minver=lowest, maxver=highest, expected_regex=""): + for version in range(self.lowest, self.highest + 1): + feature_version = (3, version) + if minver <= version <= maxver: + try: + yield self.parse(source, feature_version) + except SyntaxError as err: + raise SyntaxError(str(err) + f" feature_version={feature_version}") + else: + with self.assertRaisesRegex(SyntaxError, expected_regex, + msg=f"feature_version={feature_version}"): + self.parse(source, feature_version) + + def classic_parse(self, source): + return ast.parse(source) + + def test_funcdef(self): + for tree in self.parse_all(funcdef): + self.assertEqual(tree.body[0].type_comment, "() -> int") + self.assertEqual(tree.body[1].type_comment, "() -> None") + tree = self.classic_parse(funcdef) + self.assertEqual(tree.body[0].type_comment, None) + self.assertEqual(tree.body[1].type_comment, None) + + def test_asyncdef(self): + for tree in self.parse_all(asyncdef, minver=5): + self.assertEqual(tree.body[0].type_comment, "() -> int") + self.assertEqual(tree.body[1].type_comment, "() -> int") + tree = self.classic_parse(asyncdef) + self.assertEqual(tree.body[0].type_comment, None) + self.assertEqual(tree.body[1].type_comment, None) + + def test_asyncvar(self): + for tree in self.parse_all(asyncvar, maxver=6): + pass + + def test_asynccomp(self): + for tree in self.parse_all(asynccomp, minver=6): + pass + + def test_matmul(self): + for tree in self.parse_all(matmul, minver=5): + pass + + def test_fstring(self): + for tree in self.parse_all(fstring, minver=6): + pass + + def test_underscorednumber(self): + for tree in self.parse_all(underscorednumber, minver=6): + pass + + def test_redundantdef(self): + for tree in self.parse_all(redundantdef, maxver=0, + expected_regex="^Cannot have two type comments on def"): + pass + + def test_nonasciidef(self): + for tree in self.parse_all(nonasciidef): + self.assertEqual(tree.body[0].type_comment, "() -> àçčéñt") + + def test_forstmt(self): + for tree in self.parse_all(forstmt): + self.assertEqual(tree.body[0].type_comment, "int") + tree = self.classic_parse(forstmt) + self.assertEqual(tree.body[0].type_comment, None) + + def test_withstmt(self): + for tree in self.parse_all(withstmt): + self.assertEqual(tree.body[0].type_comment, "int") + tree = self.classic_parse(withstmt) + self.assertEqual(tree.body[0].type_comment, None) + + def test_vardecl(self): + for tree in self.parse_all(vardecl): + self.assertEqual(tree.body[0].type_comment, "int") + tree = self.classic_parse(vardecl) + self.assertEqual(tree.body[0].type_comment, None) + + def test_ignores(self): + for tree in self.parse_all(ignores): + self.assertEqual( + [(ti.lineno, ti.tag) for ti in tree.type_ignores], + [ + (2, ''), + (5, ''), + (8, '[excuse]'), + (9, '=excuse'), + (10, ' [excuse]'), + (11, ' whatever'), + ]) + tree = self.classic_parse(ignores) + self.assertEqual(tree.type_ignores, []) + + def test_longargs(self): + for tree in self.parse_all(longargs): + for t in tree.body: + # The expected args are encoded in the function name + todo = set(t.name[1:]) + self.assertEqual(len(t.args.args) + len(t.args.posonlyargs), + len(todo) - bool(t.args.vararg) - bool(t.args.kwarg)) + self.assertTrue(t.name.startswith('f'), t.name) + for index, c in enumerate(t.name[1:]): + todo.remove(c) + if c == 'v': + arg = t.args.vararg + elif c == 'k': + arg = t.args.kwarg + else: + assert 0 <= ord(c) - ord('a') < len(t.args.posonlyargs + t.args.args) + if index < len(t.args.posonlyargs): + arg = t.args.posonlyargs[ord(c) - ord('a')] + else: + arg = t.args.args[ord(c) - ord('a') - len(t.args.posonlyargs)] + self.assertEqual(arg.arg, c) # That's the argument name + self.assertEqual(arg.type_comment, arg.arg.upper()) + assert not todo + tree = self.classic_parse(longargs) + for t in tree.body: + for arg in t.args.args + [t.args.vararg, t.args.kwarg]: + if arg is not None: + self.assertIsNone(arg.type_comment, "%s(%s:%r)" % + (t.name, arg.arg, arg.type_comment)) + + def test_inappropriate_type_comments(self): + """Tests for inappropriately-placed type comments. + + These should be silently ignored with type comments off, + but raise SyntaxError with type comments on. + + This is not meant to be exhaustive. + """ + + def check_both_ways(source): + ast.parse(source, type_comments=False) + for tree in self.parse_all(source, maxver=0): + pass + + check_both_ways("pass # type: int\n") + check_both_ways("foo() # type: int\n") + check_both_ways("x += 1 # type: int\n") + check_both_ways("while True: # type: int\n continue\n") + check_both_ways("while True:\n continue # type: int\n") + check_both_ways("try: # type: int\n pass\nfinally:\n pass\n") + check_both_ways("try:\n pass\nfinally: # type: int\n pass\n") + check_both_ways("pass # type: ignorewhatever\n") + check_both_ways("pass # type: ignoreé\n") + + def test_func_type_input(self): + + def parse_func_type_input(source): + return ast.parse(source, "", "func_type") + + # Some checks below will crash if the returned structure is wrong + tree = parse_func_type_input("() -> int") + self.assertEqual(tree.argtypes, []) + self.assertEqual(tree.returns.id, "int") + + tree = parse_func_type_input("(int) -> List[str]") + self.assertEqual(len(tree.argtypes), 1) + arg = tree.argtypes[0] + self.assertEqual(arg.id, "int") + self.assertEqual(tree.returns.value.id, "List") + self.assertEqual(tree.returns.slice.value.id, "str") + + tree = parse_func_type_input("(int, *str, **Any) -> float") + self.assertEqual(tree.argtypes[0].id, "int") + self.assertEqual(tree.argtypes[1].id, "str") + self.assertEqual(tree.argtypes[2].id, "Any") + self.assertEqual(tree.returns.id, "float") + + with self.assertRaises(SyntaxError): + tree = parse_func_type_input("(int, *str, *Any) -> float") + + with self.assertRaises(SyntaxError): + tree = parse_func_type_input("(int, **str, Any) -> float") + + with self.assertRaises(SyntaxError): + tree = parse_func_type_input("(**int, **str) -> float") + + +if __name__ == '__main__': + unittest.main() From 65e1ee0debec065c3b5e4d768e4e9d75845ed2b0 Mon Sep 17 00:00:00 2001 From: Padraic Fanning Date: Thu, 22 Apr 2021 20:35:16 -0400 Subject: [PATCH 2/2] Mark erroring tests --- Lib/test/test_type_comments.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Lib/test/test_type_comments.py b/Lib/test/test_type_comments.py index 43be54efd..ef45586cd 100644 --- a/Lib/test/test_type_comments.py +++ b/Lib/test/test_type_comments.py @@ -243,6 +243,8 @@ class TypeCommentTests(unittest.TestCase): def classic_parse(self, source): return ast.parse(source) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_funcdef(self): for tree in self.parse_all(funcdef): self.assertEqual(tree.body[0].type_comment, "() -> int") @@ -251,6 +253,8 @@ class TypeCommentTests(unittest.TestCase): self.assertEqual(tree.body[0].type_comment, None) self.assertEqual(tree.body[1].type_comment, None) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_asyncdef(self): for tree in self.parse_all(asyncdef, minver=5): self.assertEqual(tree.body[0].type_comment, "() -> int") @@ -259,53 +263,75 @@ class TypeCommentTests(unittest.TestCase): self.assertEqual(tree.body[0].type_comment, None) self.assertEqual(tree.body[1].type_comment, None) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_asyncvar(self): for tree in self.parse_all(asyncvar, maxver=6): pass + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_asynccomp(self): for tree in self.parse_all(asynccomp, minver=6): pass + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_matmul(self): for tree in self.parse_all(matmul, minver=5): pass + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_fstring(self): for tree in self.parse_all(fstring, minver=6): pass + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_underscorednumber(self): for tree in self.parse_all(underscorednumber, minver=6): pass + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_redundantdef(self): for tree in self.parse_all(redundantdef, maxver=0, expected_regex="^Cannot have two type comments on def"): pass + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_nonasciidef(self): for tree in self.parse_all(nonasciidef): self.assertEqual(tree.body[0].type_comment, "() -> àçčéñt") + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_forstmt(self): for tree in self.parse_all(forstmt): self.assertEqual(tree.body[0].type_comment, "int") tree = self.classic_parse(forstmt) self.assertEqual(tree.body[0].type_comment, None) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_withstmt(self): for tree in self.parse_all(withstmt): self.assertEqual(tree.body[0].type_comment, "int") tree = self.classic_parse(withstmt) self.assertEqual(tree.body[0].type_comment, None) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_vardecl(self): for tree in self.parse_all(vardecl): self.assertEqual(tree.body[0].type_comment, "int") tree = self.classic_parse(vardecl) self.assertEqual(tree.body[0].type_comment, None) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_ignores(self): for tree in self.parse_all(ignores): self.assertEqual( @@ -321,6 +347,8 @@ class TypeCommentTests(unittest.TestCase): tree = self.classic_parse(ignores) self.assertEqual(tree.type_ignores, []) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_longargs(self): for tree in self.parse_all(longargs): for t in tree.body: @@ -351,6 +379,8 @@ class TypeCommentTests(unittest.TestCase): self.assertIsNone(arg.type_comment, "%s(%s:%r)" % (t.name, arg.arg, arg.type_comment)) + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_inappropriate_type_comments(self): """Tests for inappropriately-placed type comments. @@ -375,6 +405,8 @@ class TypeCommentTests(unittest.TestCase): check_both_ways("pass # type: ignorewhatever\n") check_both_ways("pass # type: ignoreé\n") + # TODO: RUSTPYTHON + @unittest.expectedFailure def test_func_type_input(self): def parse_func_type_input(source):