From 8ca85b5bc5a888b8329915aff0ceb34de9373546 Mon Sep 17 00:00:00 2001 From: Stan Ulbrych Date: Fri, 17 Jul 2026 17:19:48 +0200 Subject: [PATCH 1/2] Shut down leaked executors in `test_interpreter_pool` (#153780) --- .../test_interpreter_pool.py | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/Lib/test/test_concurrent_futures/test_interpreter_pool.py b/Lib/test/test_concurrent_futures/test_interpreter_pool.py index db5b89bb76ada9..e319849adf366d 100644 --- a/Lib/test/test_concurrent_futures/test_interpreter_pool.py +++ b/Lib/test/test_concurrent_futures/test_interpreter_pool.py @@ -129,12 +129,12 @@ def test_init_script(self): """ os.write(w, b'\0') - executor = self.executor_type(initializer=initscript) - before_init = os.read(r, 100) - fut = executor.submit(script) - after_init = read_msg(r) - fut.result() - after_run = read_msg(r) + with self.executor_type(initializer=initscript) as executor: + before_init = os.read(r, 100) + fut = executor.submit(script) + after_init = read_msg(r) + fut.result() + after_run = read_msg(r) self.assertEqual(before_init, b'\0') self.assertEqual(after_init, msg1) @@ -150,11 +150,11 @@ def test_init_func(self): r, w = self.pipe() os.write(w, b'\0') - executor = self.executor_type( - initializer=write_msg, initargs=(w, msg)) - before = os.read(r, 100) - executor.submit(mul, 10, 10) - after = read_msg(r) + with self.executor_type( + initializer=write_msg, initargs=(w, msg)) as executor: + before = os.read(r, 100) + executor.submit(mul, 10, 10) + after = read_msg(r) self.assertEqual(before, b'\0') self.assertEqual(after, msg) @@ -260,11 +260,10 @@ def test_submit_script(self): import os os.write({w}, __name__.encode('utf-8') + b'\\0') """ - executor = self.executor_type() - - fut = executor.submit(script) - res = fut.result() - after = read_msg(r) + with self.executor_type() as executor: + fut = executor.submit(script) + res = fut.result() + after = read_msg(r) self.assertEqual(after, b'__main__') self.assertIs(res, None) @@ -278,25 +277,24 @@ def task2(): spam += 1 return spam - executor = self.executor_type() - - fut = executor.submit(task1) - with self.assertRaises(_interpreters.NotShareableError): - fut.result() + with self.executor_type() as executor: + fut = executor.submit(task1) + with self.assertRaises(_interpreters.NotShareableError): + fut.result() - fut = executor.submit(task2) - with self.assertRaises(_interpreters.NotShareableError): - fut.result() + fut = executor.submit(task2) + with self.assertRaises(_interpreters.NotShareableError): + fut.result() def test_submit_local_instance(self): class Spam: def __init__(self): self.value = True - executor = self.executor_type() - fut = executor.submit(Spam) - with self.assertRaises(_interpreters.NotShareableError): - fut.result() + with self.executor_type() as executor: + fut = executor.submit(Spam) + with self.assertRaises(_interpreters.NotShareableError): + fut.result() def test_submit_instance_method(self): class Spam: @@ -304,15 +302,15 @@ def run(self): return True spam = Spam() - executor = self.executor_type() - fut = executor.submit(spam.run) - with self.assertRaises(_interpreters.NotShareableError): - fut.result() + with self.executor_type() as executor: + fut = executor.submit(spam.run) + with self.assertRaises(_interpreters.NotShareableError): + fut.result() def test_submit_func_globals(self): - executor = self.executor_type() - fut = executor.submit(get_current_name) - name = fut.result() + with self.executor_type() as executor: + fut = executor.submit(get_current_name) + name = fut.result() self.assertEqual(name, __name__) self.assertNotEqual(name, '__main__') From 9d231cbc937f51c92f8d2883815aab64e52fe5e7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 17 Jul 2026 19:17:08 +0300 Subject: [PATCH 2/2] 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 --- Doc/library/symtable.rst | 5 ++ Doc/whatsnew/3.16.rst | 8 +++ Lib/symtable.py | 1 + Lib/test/test_symtable.py | 72 +++++++++++++++++++ ...-07-17-14-30-00.gh-issue-153844.pYs7Kq.rst | 2 + Modules/clinic/symtablemodule.c.h | 6 +- Modules/symtablemodule.c | 72 ++++++++++++++----- 7 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-17-14-30-00.gh-issue-153844.pYs7Kq.rst diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index 95f20b06b5aa1e..859687340882de 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -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 ----------------------- diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index ed6d303c1e28ad..0ef281dfe1e63b 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -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 ------- diff --git a/Lib/symtable.py b/Lib/symtable.py index 9238437191c00f..18bb355d86b09e 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -20,6 +20,7 @@ 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. """ diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 8c03420c4c5e4b..ce02b27c599c42 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -2,6 +2,7 @@ Test the API of the symtable module. """ +import ast import symtable import warnings import unittest @@ -554,6 +555,77 @@ def test_loopvar_in_only_one_scope(self): 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 index 00000000000000..cf46eec8513f67 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-17-14-30-00.gh-issue-153844.pYs7Kq.rst @@ -0,0 +1,2 @@ +:func:`symtable.symtable` now accepts an AST object, +like the builtin :func:`compile`. diff --git a/Modules/clinic/symtablemodule.c.h b/Modules/clinic/symtablemodule.c.h index 65352593f94802..00ee4315846b60 100644 --- a/Modules/clinic/symtablemodule.c.h +++ b/Modules/clinic/symtablemodule.c.h @@ -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 @@ _symtable_symtable(PyObject *module, PyObject *const *args, Py_ssize_t nargs, Py return return_value; } -/*[clinic end generated code: output=0137be60c487c841 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=23523cada784726e input=a9049054013a1b77]*/ diff --git a/Modules/symtablemodule.c b/Modules/symtablemodule.c index e9e1c4811b8303..7e20b5c7173ae5 100644 --- a/Modules/symtablemodule.c +++ b/Modules/symtablemodule.c @@ -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; }