gh-85098: Implement functional CLI of symtable (#109112)

Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
This commit is contained in:
Serhiy Storchaka 2023-11-07 18:32:16 +02:00 committed by GitHub
parent f55cb44359
commit 70afb8d732
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 123 additions and 8 deletions

View file

@ -207,3 +207,21 @@ Examining Symbol Tables
Return the namespace bound to this name. If more than one or no namespace
is bound to this name, a :exc:`ValueError` is raised.
.. _symtable-cli:
Command-Line Usage
------------------
.. versionadded:: 3.13
The :mod:`symtable` module can be executed as a script from the command line.
.. code-block:: sh
python -m symtable [infile...]
Symbol tables are generated for the specified Python source files and
dumped to stdout.
If no input file is specified, the content is read from stdin.

View file

@ -233,7 +233,16 @@ def __init__(self, name, flags, namespaces=None, *, module_scope=False):
self.__module_scope = module_scope
def __repr__(self):
return "<symbol {0!r}>".format(self.__name)
flags_str = '|'.join(self._flags_str())
return f'<symbol {self.__name!r}: {self._scope_str()}, {flags_str}>'
def _scope_str(self):
return _scopes_value_to_name.get(self.__scope) or str(self.__scope)
def _flags_str(self):
for flagname, flagvalue in _flags:
if self.__flags & flagvalue == flagvalue:
yield flagname
def get_name(self):
"""Return a name of a symbol.
@ -323,11 +332,43 @@ def get_namespace(self):
else:
return self.__namespaces[0]
_flags = [('USE', USE)]
_flags.extend(kv for kv in globals().items() if kv[0].startswith('DEF_'))
_scopes_names = ('FREE', 'LOCAL', 'GLOBAL_IMPLICIT', 'GLOBAL_EXPLICIT', 'CELL')
_scopes_value_to_name = {globals()[n]: n for n in _scopes_names}
def main(args):
import sys
def print_symbols(table, level=0):
indent = ' ' * level
nested = "nested " if table.is_nested() else ""
if table.get_type() == 'module':
what = f'from file {table._filename!r}'
else:
what = f'{table.get_name()!r}'
print(f'{indent}symbol table for {nested}{table.get_type()} {what}:')
for ident in table.get_identifiers():
symbol = table.lookup(ident)
flags = ', '.join(symbol._flags_str()).lower()
print(f' {indent}{symbol._scope_str().lower()} symbol {symbol.get_name()!r}: {flags}')
print()
for table2 in table.get_children():
print_symbols(table2, level + 1)
for filename in args or ['-']:
if filename == '-':
src = sys.stdin.read()
filename = '<stdin>'
else:
with open(filename, 'rb') as f:
src = f.read()
mod = symtable(src, filename, 'exec')
print_symbols(mod)
if __name__ == "__main__":
import os, sys
with open(sys.argv[0]) as f:
src = f.read()
mod = symtable(src, os.path.split(sys.argv[0])[1], "exec")
for ident in mod.get_identifiers():
info = mod.lookup(ident)
print(info, info.is_local(), info.is_namespace())
import sys
main(sys.argv[1:])

View file

@ -4,6 +4,8 @@
import symtable
import unittest
from test import support
from test.support import os_helper
TEST_CODE = """
@ -282,10 +284,62 @@ def test_symtable_repr(self):
self.assertEqual(str(self.top), "<SymbolTable for module ?>")
self.assertEqual(str(self.spam), "<Function SymbolTable for spam in ?>")
def test_symbol_repr(self):
self.assertEqual(repr(self.spam.lookup("glob")),
"<symbol 'glob': GLOBAL_IMPLICIT, USE>")
self.assertEqual(repr(self.spam.lookup("bar")),
"<symbol 'bar': GLOBAL_EXPLICIT, DEF_GLOBAL|DEF_LOCAL>")
self.assertEqual(repr(self.spam.lookup("a")),
"<symbol 'a': LOCAL, DEF_PARAM>")
self.assertEqual(repr(self.spam.lookup("internal")),
"<symbol 'internal': LOCAL, USE|DEF_LOCAL>")
self.assertEqual(repr(self.spam.lookup("other_internal")),
"<symbol 'other_internal': LOCAL, DEF_LOCAL>")
self.assertEqual(repr(self.internal.lookup("x")),
"<symbol 'x': FREE, USE>")
self.assertEqual(repr(self.other_internal.lookup("some_var")),
"<symbol 'some_var': FREE, USE|DEF_NONLOCAL|DEF_LOCAL>")
def test_symtable_entry_repr(self):
expected = f"<symtable entry top({self.top.get_id()}), line {self.top.get_lineno()}>"
self.assertEqual(repr(self.top._table), expected)
class CommandLineTest(unittest.TestCase):
maxDiff = None
def test_file(self):
filename = os_helper.TESTFN
self.addCleanup(os_helper.unlink, filename)
with open(filename, 'w') as f:
f.write(TEST_CODE)
with support.captured_stdout() as stdout:
symtable.main([filename])
out = stdout.getvalue()
self.assertIn('\n\n', out)
self.assertNotIn('\n\n\n', out)
lines = out.splitlines()
self.assertIn(f"symbol table for module from file {filename!r}:", lines)
self.assertIn(" local symbol 'glob': def_local", lines)
self.assertIn(" global_implicit symbol 'glob': use", lines)
self.assertIn(" local symbol 'spam': def_local", lines)
self.assertIn(" symbol table for function 'spam':", lines)
def test_stdin(self):
with support.captured_stdin() as stdin:
stdin.write(TEST_CODE)
stdin.seek(0)
with support.captured_stdout() as stdout:
symtable.main([])
out = stdout.getvalue()
stdin.seek(0)
with support.captured_stdout() as stdout:
symtable.main(['-'])
self.assertEqual(stdout.getvalue(), out)
lines = out.splitlines()
print(out)
self.assertIn("symbol table for module from file '<stdin>':", lines)
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,2 @@
Implement the CLI of the :mod:`symtable` module and improve the repr of
:class:`~symtable.Symbol`.