Skip to content

Commit 9d231cb

Browse files
gh-153844: Support AST input in symtable.symtable() (GH-153845)
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>
1 parent 8ca85b5 commit 9d231cb

7 files changed

Lines changed: 146 additions & 20 deletions

File tree

Doc/library/symtable.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ Generating Symbol Tables
2020
.. function:: symtable(code, filename, compile_type, *, module=None)
2121

2222
Return the toplevel :class:`SymbolTable` for the Python source *code*.
23+
*code* can be a string, a bytes object, or an AST object,
24+
as for the builtin :func:`compile`.
2325
*filename* is the name of the file containing the code. *compile_type* is
2426
like the *mode* argument to :func:`compile`.
2527
The optional argument *module* specifies the module name.
@@ -29,6 +31,9 @@ Generating Symbol Tables
2931
.. versionadded:: 3.15
3032
Added the *module* parameter.
3133

34+
.. versionchanged:: next
35+
*code* can now be an AST object.
36+
3237

3338
Examining Symbol Tables
3439
-----------------------

Doc/whatsnew/3.16.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,14 @@ shlex
379379
(Contributed by Jay Berry in :gh:`148846`.)
380380

381381

382+
symtable
383+
--------
384+
385+
* :func:`symtable.symtable` now accepts an AST object,
386+
like the builtin :func:`compile`.
387+
(Contributed by Serhiy Storchaka in :gh:`153844`.)
388+
389+
382390
tkinter
383391
-------
384392

Lib/symtable.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
def symtable(code, filename, compile_type, *, module=None):
2121
""" Return the toplevel *SymbolTable* for the source code.
2222
23+
*code* can be a string, a bytes object, or an AST object.
2324
*filename* is the name of the file with the code
2425
and *compile_type* is the *compile()* mode argument.
2526
"""

Lib/test/test_symtable.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Test the API of the symtable module.
33
"""
44

5+
import ast
56
import symtable
67
import warnings
78
import unittest
@@ -554,6 +555,77 @@ def test_loopvar_in_only_one_scope(self):
554555
self.assertEqual(len([x for x in ids if x == 'x']), 1)
555556

556557

558+
class ASTInputTests(unittest.TestCase):
559+
maxDiff = None
560+
561+
def dump(self, table):
562+
return (table.get_name(), table.get_type(), table.get_lineno(),
563+
[repr(symbol) for symbol in table.get_symbols()],
564+
[self.dump(child) for child in table.get_children()])
565+
566+
def test_exec(self):
567+
top = symtable.symtable(ast.parse(TEST_CODE), "?", "exec")
568+
self.assertIsNotNone(find_block(top, "Mine"))
569+
570+
def test_eval(self):
571+
table = symtable.symtable(ast.parse("a + b", mode="eval"), "?", "eval")
572+
self.assertEqual(sorted(table.get_identifiers()), ["a", "b"])
573+
574+
def test_single(self):
575+
table = symtable.symtable(ast.parse("x = 1", mode="single"),
576+
"?", "single")
577+
self.assertIn("x", table.get_identifiers())
578+
579+
def test_same_result_as_string(self):
580+
cases = [
581+
(TEST_CODE, "exec"),
582+
("from __future__ import annotations\n"
583+
"def f(x: int) -> int: return x\n", "exec"),
584+
("[x*y for x in a]", "eval"),
585+
("def f(): pass\n", "single"),
586+
]
587+
for source, mode in cases:
588+
with self.subTest(source=source, mode=mode):
589+
from_str = symtable.symtable(source, "?", mode)
590+
from_ast = symtable.symtable(ast.parse(source, mode=mode),
591+
"?", mode)
592+
self.assertEqual(self.dump(from_ast), self.dump(from_str))
593+
594+
def test_synthesized_ast(self):
595+
# An AST created programmatically, without any source.
596+
node = ast.Module(body=[
597+
ast.FunctionDef(
598+
name="f",
599+
args=ast.arguments(args=[ast.arg(arg="x")]),
600+
body=[ast.Return(ast.Name("x", ast.Load()))])])
601+
ast.fix_missing_locations(node)
602+
top = symtable.symtable(node, "?", "exec")
603+
f = find_block(top, "f")
604+
self.assertTrue(f.lookup("x").is_parameter())
605+
606+
def test_mode_mismatch(self):
607+
tree = ast.parse("x = 1")
608+
for mode in ("eval", "single"):
609+
with self.subTest(mode=mode):
610+
with self.assertRaises(TypeError):
611+
symtable.symtable(tree, "?", mode)
612+
with self.assertRaises(TypeError):
613+
symtable.symtable(ast.parse("x", mode="eval"), "?", "exec")
614+
615+
def test_invalid_ast(self):
616+
node = ast.Expression(ast.Name("x", ast.Store()))
617+
ast.fix_missing_locations(node)
618+
with self.assertRaises(ValueError):
619+
symtable.symtable(node, "?", "eval")
620+
621+
def test_misplaced_future_import(self):
622+
# The parser does not enforce the placement of future imports in
623+
# an existing AST; the symbol table construction does.
624+
tree = ast.parse("x = 1\nfrom __future__ import annotations\n")
625+
with self.assertRaises(SyntaxError):
626+
symtable.symtable(tree, "?", "exec")
627+
628+
557629
class CommandLineTest(unittest.TestCase):
558630
maxDiff = None
559631

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:func:`symtable.symtable` now accepts an AST object,
2+
like the builtin :func:`compile`.

Modules/clinic/symtablemodule.c.h

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/symtablemodule.c

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#include "Python.h"
2+
#include "pycore_ast.h" // PyAST_Check()
3+
#include "pycore_pyarena.h" // _PyArena_New()
24
#include "pycore_pythonrun.h" // _Py_SourceAsString()
35
#include "pycore_symtable.h" // struct symtable
46

@@ -9,6 +11,29 @@ module _symtable
911
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=f4685845a7100605]*/
1012

1113

14+
static struct symtable *
15+
symtable_from_ast(PyObject *source, PyObject *filename, int compile_mode)
16+
{
17+
PyArena *arena = _PyArena_New();
18+
if (arena == NULL) {
19+
return NULL;
20+
}
21+
struct symtable *st = NULL;
22+
mod_ty mod = PyAST_obj2mod(source, arena, compile_mode);
23+
if (mod == NULL || !_PyAST_Validate(mod)) {
24+
goto finally;
25+
}
26+
_PyFutureFeatures future;
27+
if (!_PyFuture_FromAST(mod, filename, &future)) {
28+
goto finally;
29+
}
30+
st = _PySymtable_Build(mod, filename, &future);
31+
finally:
32+
_PyArena_Free(arena);
33+
return st;
34+
}
35+
36+
1237
/*[clinic input]
1338
_symtable.symtable
1439
@@ -20,37 +45,29 @@ _symtable.symtable
2045
module as modname: object = None
2146
2247
Return symbol and scope dictionaries used internally by compiler.
48+
49+
The source can be a string, a bytes object, or an AST object.
2350
[clinic start generated code]*/
2451

2552
static PyObject *
2653
_symtable_symtable_impl(PyObject *module, PyObject *source,
2754
PyObject *filename, const char *startstr,
2855
PyObject *modname)
29-
/*[clinic end generated code: output=235ec5a87a9ce178 input=fbf9adaa33c7070d]*/
56+
/*[clinic end generated code: output=235ec5a87a9ce178 input=6cadac0485f576a7]*/
3057
{
3158
struct symtable *st;
3259
PyObject *t;
33-
int start;
34-
PyCompilerFlags cf = _PyCompilerFlags_INIT;
35-
PyObject *source_copy = NULL;
36-
37-
cf.cf_flags = PyCF_SOURCE_IS_UTF8;
38-
39-
const char *str = _Py_SourceAsString(source, "symtable", "string or bytes", &cf, &source_copy);
40-
if (str == NULL) {
41-
return NULL;
42-
}
60+
int compile_mode;
4361

4462
if (strcmp(startstr, "exec") == 0)
45-
start = Py_file_input;
63+
compile_mode = 0;
4664
else if (strcmp(startstr, "eval") == 0)
47-
start = Py_eval_input;
65+
compile_mode = 1;
4866
else if (strcmp(startstr, "single") == 0)
49-
start = Py_single_input;
67+
compile_mode = 2;
5068
else {
5169
PyErr_SetString(PyExc_ValueError,
5270
"symtable() arg 3 must be 'exec' or 'eval' or 'single'");
53-
Py_XDECREF(source_copy);
5471
return NULL;
5572
}
5673
if (modname == Py_None) {
@@ -60,11 +77,30 @@ _symtable_symtable_impl(PyObject *module, PyObject *source,
6077
PyErr_Format(PyExc_TypeError,
6178
"symtable() argument 'module' must be str or None, not %T",
6279
modname);
63-
Py_XDECREF(source_copy);
6480
return NULL;
6581
}
66-
st = _Py_SymtableStringObjectFlags(str, filename, start, &cf, modname);
67-
Py_XDECREF(source_copy);
82+
83+
if (PyAST_Check(source)) {
84+
st = symtable_from_ast(source, filename, compile_mode);
85+
}
86+
else {
87+
static const int starts[] = {
88+
Py_file_input, Py_eval_input, Py_single_input};
89+
PyCompilerFlags cf = _PyCompilerFlags_INIT;
90+
PyObject *source_copy = NULL;
91+
92+
cf.cf_flags = PyCF_SOURCE_IS_UTF8;
93+
const char *str = _Py_SourceAsString(source, "symtable",
94+
"string, bytes or AST",
95+
&cf, &source_copy);
96+
if (str == NULL) {
97+
return NULL;
98+
}
99+
st = _Py_SymtableStringObjectFlags(str, filename,
100+
starts[compile_mode], &cf,
101+
modname);
102+
Py_XDECREF(source_copy);
103+
}
68104
if (st == NULL) {
69105
return NULL;
70106
}

0 commit comments

Comments
 (0)