Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Doc/library/symtable.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
-----------------------
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.16.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------

Expand Down
1 change: 1 addition & 0 deletions Lib/symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down
68 changes: 33 additions & 35 deletions Lib/test/test_concurrent_futures/test_interpreter_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -278,41 +277,40 @@ 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:
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__')
Expand Down
72 changes: 72 additions & 0 deletions Lib/test/test_symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Test the API of the symtable module.
"""

import ast
import symtable
import warnings
import unittest
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:func:`symtable.symtable` now accepts an AST object,
like the builtin :func:`compile`.
6 changes: 4 additions & 2 deletions Modules/clinic/symtablemodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 54 additions & 18 deletions Modules/symtablemodule.c
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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) {
Expand All @@ -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;
}
Expand Down
Loading