Skip to content

Commit

Permalink
pythongh-85098: Implement functional CLI of symtable (python#109112)
Browse files Browse the repository at this point in the history
Co-authored-by: Pablo Galindo Salgado <[email protected]>
  • Loading branch information
serhiy-storchaka and pablogsal authored Nov 7, 2023
1 parent f55cb44 commit 70afb8d
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 8 deletions.
18 changes: 18 additions & 0 deletions Doc/library/symtable.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
57 changes: 49 additions & 8 deletions Lib/symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:])
54 changes: 54 additions & 0 deletions Lib/test/test_symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import symtable
import unittest

from test import support
from test.support import os_helper


TEST_CODE = """
Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Implement the CLI of the :mod:`symtable` module and improve the repr of
:class:`~symtable.Symbol`.

0 comments on commit 70afb8d

Please sign in to comment.