Skip to content

Commit 6ce60f1

Browse files
donBarbossobolevn
andauthored
gh-131178: Add tests for ast command-line interface (#133329)
Co-authored-by: sobolevn <[email protected]>
1 parent 40be123 commit 6ce60f1

File tree

2 files changed

+168
-18
lines changed

2 files changed

+168
-18
lines changed

Lib/ast.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ def unparse(ast_obj):
626626
return unparser.visit(ast_obj)
627627

628628

629-
def main():
629+
def main(args=None):
630630
import argparse
631631
import sys
632632

@@ -643,7 +643,7 @@ def main():
643643
'column offsets')
644644
parser.add_argument('-i', '--indent', type=int, default=3,
645645
help='indentation of nodes (number of spaces)')
646-
args = parser.parse_args()
646+
args = parser.parse_args(args)
647647

648648
if args.infile == '-':
649649
name = '<stdin>'

Lib/test/test_ast/test_ast.py

+166-16
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import _ast_unparse
22
import ast
33
import builtins
4+
import contextlib
45
import copy
56
import dis
67
import enum
8+
import itertools
79
import os
810
import re
911
import sys
12+
import tempfile
1013
import textwrap
1114
import types
1215
import unittest
1316
import weakref
17+
from io import StringIO
1418
from pathlib import Path
1519
from textwrap import dedent
1620
try:
@@ -19,7 +23,7 @@
1923
_testinternalcapi = None
2024

2125
from test import support
22-
from test.support import os_helper, script_helper
26+
from test.support import os_helper
2327
from test.support import skip_emscripten_stack_overflow, skip_wasi_stack_overflow
2428
from test.support.ast_helper import ASTTestMixin
2529
from test.test_ast.utils import to_tuple
@@ -3232,23 +3236,169 @@ def test_subinterpreter(self):
32323236
self.assertEqual(res, 0)
32333237

32343238

3235-
class ASTMainTests(unittest.TestCase):
3236-
# Tests `ast.main()` function.
3239+
class CommandLineTests(unittest.TestCase):
3240+
def setUp(self):
3241+
self.filename = tempfile.mktemp()
3242+
self.addCleanup(os_helper.unlink, self.filename)
32373243

3238-
def test_cli_file_input(self):
3239-
code = "print(1, 2, 3)"
3240-
expected = ast.dump(ast.parse(code), indent=3)
3241-
3242-
with os_helper.temp_dir() as tmp_dir:
3243-
filename = os.path.join(tmp_dir, "test_module.py")
3244-
with open(filename, 'w', encoding='utf-8') as f:
3245-
f.write(code)
3246-
res, _ = script_helper.run_python_until_end("-m", "ast", filename)
3244+
@staticmethod
3245+
def text_normalize(string):
3246+
return textwrap.dedent(string).strip()
3247+
3248+
def set_source(self, content):
3249+
Path(self.filename).write_text(self.text_normalize(content))
3250+
3251+
def invoke_ast(self, *flags):
3252+
stderr = StringIO()
3253+
stdout = StringIO()
3254+
with (
3255+
contextlib.redirect_stdout(stdout),
3256+
contextlib.redirect_stderr(stderr),
3257+
):
3258+
ast.main(args=[*flags, self.filename])
3259+
self.assertEqual(stderr.getvalue(), '')
3260+
return stdout.getvalue().strip()
3261+
3262+
def check_output(self, source, expect, *flags):
3263+
self.set_source(source)
3264+
res = self.invoke_ast(*flags)
3265+
expect = self.text_normalize(expect)
3266+
self.assertEqual(res, expect)
3267+
3268+
def test_invocation(self):
3269+
# test various combinations of parameters
3270+
base_flags = (
3271+
('-m=exec', '--mode=exec'),
3272+
('--no-type-comments', '--no-type-comments'),
3273+
('-a', '--include-attributes'),
3274+
('-i=4', '--indent=4'),
3275+
)
3276+
self.set_source('''
3277+
print(1, 2, 3)
3278+
def f(x: int) -> int:
3279+
x -= 1
3280+
return x
3281+
''')
32473282

3248-
self.assertEqual(res.err, b"")
3249-
self.assertEqual(expected.splitlines(),
3250-
res.out.decode("utf8").splitlines())
3251-
self.assertEqual(res.rc, 0)
3283+
for r in range(1, len(base_flags) + 1):
3284+
for choices in itertools.combinations(base_flags, r=r):
3285+
for args in itertools.product(*choices):
3286+
with self.subTest(flags=args):
3287+
self.invoke_ast(*args)
3288+
3289+
def test_help_message(self):
3290+
for flag in ('-h', '--help', '--unknown'):
3291+
with self.subTest(flag=flag):
3292+
output = StringIO()
3293+
with self.assertRaises(SystemExit):
3294+
with contextlib.redirect_stderr(output):
3295+
ast.main(args=flag)
3296+
self.assertStartsWith(output.getvalue(), 'usage: ')
3297+
3298+
def test_exec_mode_flag(self):
3299+
# test 'python -m ast -m/--mode exec'
3300+
source = 'x: bool = 1 # type: ignore[assignment]'
3301+
expect = '''
3302+
Module(
3303+
body=[
3304+
AnnAssign(
3305+
target=Name(id='x', ctx=Store()),
3306+
annotation=Name(id='bool', ctx=Load()),
3307+
value=Constant(value=1),
3308+
simple=1)],
3309+
type_ignores=[
3310+
TypeIgnore(lineno=1, tag='[assignment]')])
3311+
'''
3312+
for flag in ('-m=exec', '--mode=exec'):
3313+
with self.subTest(flag=flag):
3314+
self.check_output(source, expect, flag)
3315+
3316+
def test_single_mode_flag(self):
3317+
# test 'python -m ast -m/--mode single'
3318+
source = 'pass'
3319+
expect = '''
3320+
Interactive(
3321+
body=[
3322+
Pass()])
3323+
'''
3324+
for flag in ('-m=single', '--mode=single'):
3325+
with self.subTest(flag=flag):
3326+
self.check_output(source, expect, flag)
3327+
3328+
def test_eval_mode_flag(self):
3329+
# test 'python -m ast -m/--mode eval'
3330+
source = 'print(1, 2, 3)'
3331+
expect = '''
3332+
Expression(
3333+
body=Call(
3334+
func=Name(id='print', ctx=Load()),
3335+
args=[
3336+
Constant(value=1),
3337+
Constant(value=2),
3338+
Constant(value=3)]))
3339+
'''
3340+
for flag in ('-m=eval', '--mode=eval'):
3341+
with self.subTest(flag=flag):
3342+
self.check_output(source, expect, flag)
3343+
3344+
def test_func_type_mode_flag(self):
3345+
# test 'python -m ast -m/--mode func_type'
3346+
source = '(int, str) -> list[int]'
3347+
expect = '''
3348+
FunctionType(
3349+
argtypes=[
3350+
Name(id='int', ctx=Load()),
3351+
Name(id='str', ctx=Load())],
3352+
returns=Subscript(
3353+
value=Name(id='list', ctx=Load()),
3354+
slice=Name(id='int', ctx=Load()),
3355+
ctx=Load()))
3356+
'''
3357+
for flag in ('-m=func_type', '--mode=func_type'):
3358+
with self.subTest(flag=flag):
3359+
self.check_output(source, expect, flag)
3360+
3361+
def test_no_type_comments_flag(self):
3362+
# test 'python -m ast --no-type-comments'
3363+
source = 'x: bool = 1 # type: ignore[assignment]'
3364+
expect = '''
3365+
Module(
3366+
body=[
3367+
AnnAssign(
3368+
target=Name(id='x', ctx=Store()),
3369+
annotation=Name(id='bool', ctx=Load()),
3370+
value=Constant(value=1),
3371+
simple=1)])
3372+
'''
3373+
self.check_output(source, expect, '--no-type-comments')
3374+
3375+
def test_include_attributes_flag(self):
3376+
# test 'python -m ast -a/--include-attributes'
3377+
source = 'pass'
3378+
expect = '''
3379+
Module(
3380+
body=[
3381+
Pass(
3382+
lineno=1,
3383+
col_offset=0,
3384+
end_lineno=1,
3385+
end_col_offset=4)])
3386+
'''
3387+
for flag in ('-a', '--include-attributes'):
3388+
with self.subTest(flag=flag):
3389+
self.check_output(source, expect, flag)
3390+
3391+
def test_indent_flag(self):
3392+
# test 'python -m ast -i/--indent'
3393+
source = 'pass'
3394+
expect = '''
3395+
Module(
3396+
body=[
3397+
Pass()])
3398+
'''
3399+
for flag in ('-i=0', '--indent=0'):
3400+
with self.subTest(flag=flag):
3401+
self.check_output(source, expect, flag)
32523402

32533403

32543404
class ASTOptimiziationTests(unittest.TestCase):

0 commit comments

Comments
 (0)