]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-153844: Support AST input in symtable.symtable() (GH-153845)
authorSerhiy Storchaka <storchaka@gmail.com>
Fri, 17 Jul 2026 16:17:08 +0000 (19:17 +0300)
committerGitHub <noreply@github.com>
Fri, 17 Jul 2026 16:17:08 +0000 (19:17 +0300)
The builtin compile() accepts an AST object since Python 2.6, but
symtable.symtable() only accepted str and bytes, although the
implementation builds the symbol table from an AST anyway.  Accept an
AST object as well: convert it for the requested compile type, validate
it, honor future statements found in the tree, and build the symbol
table the same way the compiler does.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Doc/library/symtable.rst
Doc/whatsnew/3.16.rst
Lib/symtable.py
Lib/test/test_symtable.py
Misc/NEWS.d/next/Library/2026-07-17-14-30-00.gh-issue-153844.pYs7Kq.rst [new file with mode: 0644]
Modules/clinic/symtablemodule.c.h
Modules/symtablemodule.c

index 95f20b06b5aa1e7eaf124b24919fd5dc97f1135c..859687340882de9455ae41e3fd05bbf0998fb2a9 100644 (file)
@@ -20,6 +20,8 @@ Generating Symbol Tables
 .. function:: symtable(code, filename, compile_type, *, module=None)
 
    Return the toplevel :class:`SymbolTable` for the Python source *code*.
+   *code* can be a string, a bytes object, or an AST object,
+   as for the builtin :func:`compile`.
    *filename* is the name of the file containing the code.  *compile_type* is
    like the *mode* argument to :func:`compile`.
    The optional argument *module* specifies the module name.
@@ -29,6 +31,9 @@ Generating Symbol Tables
    .. versionadded:: 3.15
       Added the *module* parameter.
 
+   .. versionchanged:: next
+      *code* can now be an AST object.
+
 
 Examining Symbol Tables
 -----------------------
index ed6d303c1e28ad418df4383f49c6c95f4a1656bb..0ef281dfe1e63b357fd1722c73509e2c3e08885e 100644 (file)
@@ -379,6 +379,14 @@ shlex
   (Contributed by Jay Berry in :gh:`148846`.)
 
 
+symtable
+--------
+
+* :func:`symtable.symtable` now accepts an AST object,
+  like the builtin :func:`compile`.
+  (Contributed by Serhiy Storchaka in :gh:`153844`.)
+
+
 tkinter
 -------
 
index 9238437191c00f949e31a6be2367ae51f754f6f4..18bb355d86b09e05890aed2ec19dd6046a0085fb 100644 (file)
@@ -20,6 +20,7 @@ __all__ = ["symtable", "SymbolTableType", "SymbolTable", "Class", "Function", "S
 def symtable(code, filename, compile_type, *, module=None):
     """ Return the toplevel *SymbolTable* for the source code.
 
+    *code* can be a string, a bytes object, or an AST object.
     *filename* is the name of the file with the code
     and *compile_type* is the *compile()* mode argument.
     """
index 8c03420c4c5e4b171038ec1c4fb299be057bb315..ce02b27c599c420ab8b1661def2996c158331dd1 100644 (file)
@@ -2,6 +2,7 @@
 Test the API of the symtable module.
 """
 
+import ast
 import symtable
 import warnings
 import unittest
@@ -554,6 +555,77 @@ class ComprehensionTests(unittest.TestCase):
                 self.assertEqual(len([x for x in ids if x == 'x']), 1)
 
 
+class ASTInputTests(unittest.TestCase):
+    maxDiff = None
+
+    def dump(self, table):
+        return (table.get_name(), table.get_type(), table.get_lineno(),
+                [repr(symbol) for symbol in table.get_symbols()],
+                [self.dump(child) for child in table.get_children()])
+
+    def test_exec(self):
+        top = symtable.symtable(ast.parse(TEST_CODE), "?", "exec")
+        self.assertIsNotNone(find_block(top, "Mine"))
+
+    def test_eval(self):
+        table = symtable.symtable(ast.parse("a + b", mode="eval"), "?", "eval")
+        self.assertEqual(sorted(table.get_identifiers()), ["a", "b"])
+
+    def test_single(self):
+        table = symtable.symtable(ast.parse("x = 1", mode="single"),
+                                  "?", "single")
+        self.assertIn("x", table.get_identifiers())
+
+    def test_same_result_as_string(self):
+        cases = [
+            (TEST_CODE, "exec"),
+            ("from __future__ import annotations\n"
+             "def f(x: int) -> int: return x\n", "exec"),
+            ("[x*y for x in a]", "eval"),
+            ("def f(): pass\n", "single"),
+        ]
+        for source, mode in cases:
+            with self.subTest(source=source, mode=mode):
+                from_str = symtable.symtable(source, "?", mode)
+                from_ast = symtable.symtable(ast.parse(source, mode=mode),
+                                             "?", mode)
+                self.assertEqual(self.dump(from_ast), self.dump(from_str))
+
+    def test_synthesized_ast(self):
+        # An AST created programmatically, without any source.
+        node = ast.Module(body=[
+            ast.FunctionDef(
+                name="f",
+                args=ast.arguments(args=[ast.arg(arg="x")]),
+                body=[ast.Return(ast.Name("x", ast.Load()))])])
+        ast.fix_missing_locations(node)
+        top = symtable.symtable(node, "?", "exec")
+        f = find_block(top, "f")
+        self.assertTrue(f.lookup("x").is_parameter())
+
+    def test_mode_mismatch(self):
+        tree = ast.parse("x = 1")
+        for mode in ("eval", "single"):
+            with self.subTest(mode=mode):
+                with self.assertRaises(TypeError):
+                    symtable.symtable(tree, "?", mode)
+        with self.assertRaises(TypeError):
+            symtable.symtable(ast.parse("x", mode="eval"), "?", "exec")
+
+    def test_invalid_ast(self):
+        node = ast.Expression(ast.Name("x", ast.Store()))
+        ast.fix_missing_locations(node)
+        with self.assertRaises(ValueError):
+            symtable.symtable(node, "?", "eval")
+
+    def test_misplaced_future_import(self):
+        # The parser does not enforce the placement of future imports in
+        # an existing AST; the symbol table construction does.
+        tree = ast.parse("x = 1\nfrom __future__ import annotations\n")
+        with self.assertRaises(SyntaxError):
+            symtable.symtable(tree, "?", "exec")
+
+
 class CommandLineTest(unittest.TestCase):
     maxDiff = None
 
diff --git a/Misc/NEWS.d/next/Library/2026-07-17-14-30-00.gh-issue-153844.pYs7Kq.rst b/Misc/NEWS.d/next/Library/2026-07-17-14-30-00.gh-issue-153844.pYs7Kq.rst
new file mode 100644 (file)
index 0000000..cf46eec
--- /dev/null
@@ -0,0 +1,2 @@
+:func:`symtable.symtable` now accepts an AST object,
+like the builtin :func:`compile`.
index 65352593f948022b78092fdf2836387cb2d53043..00ee4315846b60363cbea6f9e3515efbb89726e3 100644 (file)
@@ -12,7 +12,9 @@ PyDoc_STRVAR(_symtable_symtable__doc__,
 "symtable($module, source, filename, startstr, /, *, module=None)\n"
 "--\n"
 "\n"
-"Return symbol and scope dictionaries used internally by compiler.");
+"Return symbol and scope dictionaries used internally by compiler.\n"
+"\n"
+"The source can be a string, a bytes object, or an AST object.");
 
 #define _SYMTABLE_SYMTABLE_METHODDEF    \
     {"symtable", _PyCFunction_CAST(_symtable_symtable), METH_FASTCALL|METH_KEYWORDS, _symtable_symtable__doc__},
@@ -95,4 +97,4 @@ exit:
 
     return return_value;
 }
-/*[clinic end generated code: output=0137be60c487c841 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=23523cada784726e input=a9049054013a1b77]*/
index e9e1c4811b830381743bbe45e377e6ddc460dedb..7e20b5c7173ae5dacbf1970073903b9f2e55e995 100644 (file)
@@ -1,4 +1,6 @@
 #include "Python.h"
+#include "pycore_ast.h"           // PyAST_Check()
+#include "pycore_pyarena.h"       // _PyArena_New()
 #include "pycore_pythonrun.h"     // _Py_SourceAsString()
 #include "pycore_symtable.h"      // struct symtable
 
@@ -9,6 +11,29 @@ module _symtable
 /*[clinic end generated code: output=da39a3ee5e6b4b0d input=f4685845a7100605]*/
 
 
+static struct symtable *
+symtable_from_ast(PyObject *source, PyObject *filename, int compile_mode)
+{
+    PyArena *arena = _PyArena_New();
+    if (arena == NULL) {
+        return NULL;
+    }
+    struct symtable *st = NULL;
+    mod_ty mod = PyAST_obj2mod(source, arena, compile_mode);
+    if (mod == NULL || !_PyAST_Validate(mod)) {
+        goto finally;
+    }
+    _PyFutureFeatures future;
+    if (!_PyFuture_FromAST(mod, filename, &future)) {
+        goto finally;
+    }
+    st = _PySymtable_Build(mod, filename, &future);
+finally:
+    _PyArena_Free(arena);
+    return st;
+}
+
+
 /*[clinic input]
 _symtable.symtable
 
@@ -20,37 +45,29 @@ _symtable.symtable
     module as modname: object = None
 
 Return symbol and scope dictionaries used internally by compiler.
+
+The source can be a string, a bytes object, or an AST object.
 [clinic start generated code]*/
 
 static PyObject *
 _symtable_symtable_impl(PyObject *module, PyObject *source,
                         PyObject *filename, const char *startstr,
                         PyObject *modname)
-/*[clinic end generated code: output=235ec5a87a9ce178 input=fbf9adaa33c7070d]*/
+/*[clinic end generated code: output=235ec5a87a9ce178 input=6cadac0485f576a7]*/
 {
     struct symtable *st;
     PyObject *t;
-    int start;
-    PyCompilerFlags cf = _PyCompilerFlags_INIT;
-    PyObject *source_copy = NULL;
-
-    cf.cf_flags = PyCF_SOURCE_IS_UTF8;
-
-    const char *str = _Py_SourceAsString(source, "symtable", "string or bytes", &cf, &source_copy);
-    if (str == NULL) {
-        return NULL;
-    }
+    int compile_mode;
 
     if (strcmp(startstr, "exec") == 0)
-        start = Py_file_input;
+        compile_mode = 0;
     else if (strcmp(startstr, "eval") == 0)
-        start = Py_eval_input;
+        compile_mode = 1;
     else if (strcmp(startstr, "single") == 0)
-        start = Py_single_input;
+        compile_mode = 2;
     else {
         PyErr_SetString(PyExc_ValueError,
            "symtable() arg 3 must be 'exec' or 'eval' or 'single'");
-        Py_XDECREF(source_copy);
         return NULL;
     }
     if (modname == Py_None) {
@@ -60,11 +77,30 @@ _symtable_symtable_impl(PyObject *module, PyObject *source,
         PyErr_Format(PyExc_TypeError,
                      "symtable() argument 'module' must be str or None, not %T",
                      modname);
-        Py_XDECREF(source_copy);
         return NULL;
     }
-    st = _Py_SymtableStringObjectFlags(str, filename, start, &cf, modname);
-    Py_XDECREF(source_copy);
+
+    if (PyAST_Check(source)) {
+        st = symtable_from_ast(source, filename, compile_mode);
+    }
+    else {
+        static const int starts[] = {
+            Py_file_input, Py_eval_input, Py_single_input};
+        PyCompilerFlags cf = _PyCompilerFlags_INIT;
+        PyObject *source_copy = NULL;
+
+        cf.cf_flags = PyCF_SOURCE_IS_UTF8;
+        const char *str = _Py_SourceAsString(source, "symtable",
+                                             "string, bytes or AST",
+                                             &cf, &source_copy);
+        if (str == NULL) {
+            return NULL;
+        }
+        st = _Py_SymtableStringObjectFlags(str, filename,
+                                           starts[compile_mode], &cf,
+                                           modname);
+        Py_XDECREF(source_copy);
+    }
     if (st == NULL) {
         return NULL;
     }