Skip to content

Commit e886d15

Browse files
serhiy-storchakapablogsal
authored andcommitted
pythongh-85098: Implement functional CLI of symtable (python#109112)
Co-authored-by: Pablo Galindo Salgado <[email protected]>
1 parent 643f0b6 commit e886d15

File tree

4 files changed

+123
-8
lines changed

4 files changed

+123
-8
lines changed

Doc/library/symtable.rst

+18
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,21 @@ Examining Symbol Tables
207207

208208
Return the namespace bound to this name. If more than one or no namespace
209209
is bound to this name, a :exc:`ValueError` is raised.
210+
211+
212+
.. _symtable-cli:
213+
214+
Command-Line Usage
215+
------------------
216+
217+
.. versionadded:: 3.13
218+
219+
The :mod:`symtable` module can be executed as a script from the command line.
220+
221+
.. code-block:: sh
222+
223+
python -m symtable [infile...]
224+
225+
Symbol tables are generated for the specified Python source files and
226+
dumped to stdout.
227+
If no input file is specified, the content is read from stdin.

Lib/symtable.py

+49-8
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,16 @@ def __init__(self, name, flags, namespaces=None, *, module_scope=False):
233233
self.__module_scope = module_scope
234234

235235
def __repr__(self):
236-
return "<symbol {0!r}>".format(self.__name)
236+
flags_str = '|'.join(self._flags_str())
237+
return f'<symbol {self.__name!r}: {self._scope_str()}, {flags_str}>'
238+
239+
def _scope_str(self):
240+
return _scopes_value_to_name.get(self.__scope) or str(self.__scope)
241+
242+
def _flags_str(self):
243+
for flagname, flagvalue in _flags:
244+
if self.__flags & flagvalue == flagvalue:
245+
yield flagname
237246

238247
def get_name(self):
239248
"""Return a name of a symbol.
@@ -323,11 +332,43 @@ def get_namespace(self):
323332
else:
324333
return self.__namespaces[0]
325334

335+
336+
_flags = [('USE', USE)]
337+
_flags.extend(kv for kv in globals().items() if kv[0].startswith('DEF_'))
338+
_scopes_names = ('FREE', 'LOCAL', 'GLOBAL_IMPLICIT', 'GLOBAL_EXPLICIT', 'CELL')
339+
_scopes_value_to_name = {globals()[n]: n for n in _scopes_names}
340+
341+
342+
def main(args):
343+
import sys
344+
def print_symbols(table, level=0):
345+
indent = ' ' * level
346+
nested = "nested " if table.is_nested() else ""
347+
if table.get_type() == 'module':
348+
what = f'from file {table._filename!r}'
349+
else:
350+
what = f'{table.get_name()!r}'
351+
print(f'{indent}symbol table for {nested}{table.get_type()} {what}:')
352+
for ident in table.get_identifiers():
353+
symbol = table.lookup(ident)
354+
flags = ', '.join(symbol._flags_str()).lower()
355+
print(f' {indent}{symbol._scope_str().lower()} symbol {symbol.get_name()!r}: {flags}')
356+
print()
357+
358+
for table2 in table.get_children():
359+
print_symbols(table2, level + 1)
360+
361+
for filename in args or ['-']:
362+
if filename == '-':
363+
src = sys.stdin.read()
364+
filename = '<stdin>'
365+
else:
366+
with open(filename, 'rb') as f:
367+
src = f.read()
368+
mod = symtable(src, filename, 'exec')
369+
print_symbols(mod)
370+
371+
326372
if __name__ == "__main__":
327-
import os, sys
328-
with open(sys.argv[0]) as f:
329-
src = f.read()
330-
mod = symtable(src, os.path.split(sys.argv[0])[1], "exec")
331-
for ident in mod.get_identifiers():
332-
info = mod.lookup(ident)
333-
print(info, info.is_local(), info.is_namespace())
373+
import sys
374+
main(sys.argv[1:])

Lib/test/test_symtable.py

+54
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import symtable
55
import unittest
66

7+
from test import support
8+
from test.support import os_helper
79

810

911
TEST_CODE = """
@@ -282,10 +284,62 @@ def test_symtable_repr(self):
282284
self.assertEqual(str(self.top), "<SymbolTable for module ?>")
283285
self.assertEqual(str(self.spam), "<Function SymbolTable for spam in ?>")
284286

287+
def test_symbol_repr(self):
288+
self.assertEqual(repr(self.spam.lookup("glob")),
289+
"<symbol 'glob': GLOBAL_IMPLICIT, USE>")
290+
self.assertEqual(repr(self.spam.lookup("bar")),
291+
"<symbol 'bar': GLOBAL_EXPLICIT, DEF_GLOBAL|DEF_LOCAL>")
292+
self.assertEqual(repr(self.spam.lookup("a")),
293+
"<symbol 'a': LOCAL, DEF_PARAM>")
294+
self.assertEqual(repr(self.spam.lookup("internal")),
295+
"<symbol 'internal': LOCAL, USE|DEF_LOCAL>")
296+
self.assertEqual(repr(self.spam.lookup("other_internal")),
297+
"<symbol 'other_internal': LOCAL, DEF_LOCAL>")
298+
self.assertEqual(repr(self.internal.lookup("x")),
299+
"<symbol 'x': FREE, USE>")
300+
self.assertEqual(repr(self.other_internal.lookup("some_var")),
301+
"<symbol 'some_var': FREE, USE|DEF_NONLOCAL|DEF_LOCAL>")
302+
285303
def test_symtable_entry_repr(self):
286304
expected = f"<symtable entry top({self.top.get_id()}), line {self.top.get_lineno()}>"
287305
self.assertEqual(repr(self.top._table), expected)
288306

289307

308+
class CommandLineTest(unittest.TestCase):
309+
maxDiff = None
310+
311+
def test_file(self):
312+
filename = os_helper.TESTFN
313+
self.addCleanup(os_helper.unlink, filename)
314+
with open(filename, 'w') as f:
315+
f.write(TEST_CODE)
316+
with support.captured_stdout() as stdout:
317+
symtable.main([filename])
318+
out = stdout.getvalue()
319+
self.assertIn('\n\n', out)
320+
self.assertNotIn('\n\n\n', out)
321+
lines = out.splitlines()
322+
self.assertIn(f"symbol table for module from file {filename!r}:", lines)
323+
self.assertIn(" local symbol 'glob': def_local", lines)
324+
self.assertIn(" global_implicit symbol 'glob': use", lines)
325+
self.assertIn(" local symbol 'spam': def_local", lines)
326+
self.assertIn(" symbol table for function 'spam':", lines)
327+
328+
def test_stdin(self):
329+
with support.captured_stdin() as stdin:
330+
stdin.write(TEST_CODE)
331+
stdin.seek(0)
332+
with support.captured_stdout() as stdout:
333+
symtable.main([])
334+
out = stdout.getvalue()
335+
stdin.seek(0)
336+
with support.captured_stdout() as stdout:
337+
symtable.main(['-'])
338+
self.assertEqual(stdout.getvalue(), out)
339+
lines = out.splitlines()
340+
print(out)
341+
self.assertIn("symbol table for module from file '<stdin>':", lines)
342+
343+
290344
if __name__ == '__main__':
291345
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Implement the CLI of the :mod:`symtable` module and improve the repr of
2+
:class:`~symtable.Symbol`.

0 commit comments

Comments
 (0)