diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst index 776c63d1f0fda0..c9ae0abdd663c7 100644 --- a/Doc/library/ast.rst +++ b/Doc/library/ast.rst @@ -1,4 +1,4 @@ -:mod:`!ast` --- Abstract Syntax Trees +:mod:`!ast` --- Abstract syntax trees ===================================== .. module:: ast @@ -29,7 +29,7 @@ compiled into a Python code object using the built-in :func:`compile` function. .. _abstract-grammar: -Abstract Grammar +Abstract grammar ---------------- The abstract grammar is currently defined as follows: @@ -2156,10 +2156,10 @@ Async and await of :class:`ast.operator`, :class:`ast.unaryop`, :class:`ast.cmpop`, :class:`ast.boolop` and :class:`ast.expr_context`) on the returned tree will be singletons. Changes to one will be reflected in all other - occurrences of the same value (e.g. :class:`ast.Add`). + occurrences of the same value (for example, :class:`ast.Add`). -:mod:`ast` Helpers +:mod:`ast` helpers ------------------ Apart from the node classes, the :mod:`ast` module defines these utility functions @@ -2484,7 +2484,7 @@ and classes for traversing abstract syntax trees: .. _ast-compiler-flags: -Compiler Flags +Compiler flags -------------- The following flags may be passed to :func:`compile` in order to change @@ -2533,7 +2533,7 @@ effects on the compilation of a program: .. _ast-cli: -Command-Line Usage +Command-line usage ------------------ .. versionadded:: 3.9 @@ -2572,6 +2572,28 @@ The following options are accepted: Indentation of nodes in AST (number of spaces). +.. option:: --feature-version + + Python version in the format 3.x (for example, 3.10). Defaults to the + current version of the interpreter. + + .. versionadded:: next + +.. option:: -O + --optimize + + Optimization level for parser. Defaults to no optimization. + + .. versionadded:: next + +.. option:: --show-empty + + Show empty lists and fields that are ``None``. Defaults to not showing empty + objects. + + .. versionadded:: next + + If :file:`infile` is specified its contents are parsed to AST and dumped to stdout. Otherwise, the content is read from stdin. diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 4b5515900cad41..326ce80e890476 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -870,6 +870,11 @@ ast that the root node type is appropriate. (Contributed by Irit Katriel in :gh:`130139`.) +* Add new ``--feature-version``, ``--optimize``, ``--show-empty`` options to + command-line interface. + (Contributed by Semyon Moroz in :gh:`133367`.) + + bdb --- diff --git a/Lib/ast.py b/Lib/ast.py index af4fe8ff5a8e95..be6ed0805d63dd 100644 --- a/Lib/ast.py +++ b/Lib/ast.py @@ -643,6 +643,15 @@ def main(args=None): 'column offsets') parser.add_argument('-i', '--indent', type=int, default=3, help='indentation of nodes (number of spaces)') + parser.add_argument('--feature-version', + type=str, default=None, metavar='VERSION', + help='Python version in the format 3.x ' + '(for example, 3.10)') + parser.add_argument('-O', '--optimize', + type=int, default=-1, metavar='LEVEL', + help='optimization level for parser (default -1)') + parser.add_argument('--show-empty', default=False, action='store_true', + help='show empty lists and fields in dump output') args = parser.parse_args(args) if args.infile == '-': @@ -652,8 +661,22 @@ def main(args=None): name = args.infile with open(args.infile, 'rb') as infile: source = infile.read() - tree = parse(source, name, args.mode, type_comments=args.no_type_comments) - print(dump(tree, include_attributes=args.include_attributes, indent=args.indent)) + + # Process feature_version + feature_version = None + if args.feature_version: + try: + major, minor = map(int, args.feature_version.split('.', 1)) + except ValueError: + parser.error('Invalid format for --feature-version; ' + 'expected format 3.x (for example, 3.10)') + + feature_version = (major, minor) + + tree = parse(source, name, args.mode, type_comments=args.no_type_comments, + feature_version=feature_version, optimize=args.optimize) + print(dump(tree, include_attributes=args.include_attributes, + indent=args.indent, show_empty=args.show_empty)) if __name__ == '__main__': main() diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py index 6a9b7812ef6185..ae82395e9a005a 100644 --- a/Lib/test/test_ast/test_ast.py +++ b/Lib/test/test_ast/test_ast.py @@ -3272,6 +3272,9 @@ def test_invocation(self): ('--no-type-comments', '--no-type-comments'), ('-a', '--include-attributes'), ('-i=4', '--indent=4'), + ('--feature-version=3.13', '--feature-version=3.13'), + ('-O=-1', '--optimize=-1'), + ('--show-empty', '--show-empty'), ) self.set_source(''' print(1, 2, 3) @@ -3389,7 +3392,7 @@ def test_include_attributes_flag(self): self.check_output(source, expect, flag) def test_indent_flag(self): - # test 'python -m ast -i/--indent' + # test 'python -m ast -i/--indent 0' source = 'pass' expect = ''' Module( @@ -3400,6 +3403,96 @@ def test_indent_flag(self): with self.subTest(flag=flag): self.check_output(source, expect, flag) + def test_feature_version_flag(self): + # test 'python -m ast --feature-version 3.9/3.10' + source = ''' + match x: + case 1: + pass + ''' + expect = ''' + Module( + body=[ + Match( + subject=Name(id='x', ctx=Load()), + cases=[ + match_case( + pattern=MatchValue( + value=Constant(value=1)), + body=[ + Pass()])])]) + ''' + self.check_output(source, expect, '--feature-version=3.10') + with self.assertRaises(SyntaxError): + self.invoke_ast('--feature-version=3.9') + + def test_no_optimize_flag(self): + # test 'python -m ast -O/--optimize -1/0' + source = ''' + match a: + case 1+2j: + pass + ''' + expect = ''' + Module( + body=[ + Match( + subject=Name(id='a', ctx=Load()), + cases=[ + match_case( + pattern=MatchValue( + value=BinOp( + left=Constant(value=1), + op=Add(), + right=Constant(value=2j))), + body=[ + Pass()])])]) + ''' + for flag in ('-O=-1', '--optimize=-1', '-O=0', '--optimize=0'): + with self.subTest(flag=flag): + self.check_output(source, expect, flag) + + def test_optimize_flag(self): + # test 'python -m ast -O/--optimize 1/2' + source = ''' + match a: + case 1+2j: + pass + ''' + expect = ''' + Module( + body=[ + Match( + subject=Name(id='a', ctx=Load()), + cases=[ + match_case( + pattern=MatchValue( + value=Constant(value=(1+2j))), + body=[ + Pass()])])]) + ''' + for flag in ('-O=1', '--optimize=1', '-O=2', '--optimize=2'): + with self.subTest(flag=flag): + self.check_output(source, expect, flag) + + def test_show_empty_flag(self): + # test 'python -m ast --show-empty' + source = 'print(1, 2, 3)' + expect = ''' + Module( + body=[ + Expr( + value=Call( + func=Name(id='print', ctx=Load()), + args=[ + Constant(value=1), + Constant(value=2), + Constant(value=3)], + keywords=[]))], + type_ignores=[]) + ''' + self.check_output(source, expect, '--show-empty') + class ASTOptimiziationTests(unittest.TestCase): def wrap_expr(self, expr): diff --git a/Misc/NEWS.d/next/Library/2025-05-04-13-40-05.gh-issue-133367.E5nl2u.rst b/Misc/NEWS.d/next/Library/2025-05-04-13-40-05.gh-issue-133367.E5nl2u.rst new file mode 100644 index 00000000000000..1c1b0352180e70 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-04-13-40-05.gh-issue-133367.E5nl2u.rst @@ -0,0 +1,2 @@ +Add the ``--feature-version``, ``--optimize``, and ``--show-empty`` options +to the :mod:`ast` command-line interface. Patch by Semyon Moroz.