diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index 85eae5f3822..46159dcef94 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -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. diff --git a/Lib/symtable.py b/Lib/symtable.py index 4b0bc6f497a..17f820abd56 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -233,7 +233,16 @@ class Symbol: self.__module_scope = module_scope def __repr__(self): - return "".format(self.__name) + flags_str = '|'.join(self._flags_str()) + return f'' + + 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 @@ class Symbol: 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 = '' + 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:]) diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 82c1d7c856a..987e9e32afc 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -4,6 +4,8 @@ Test the API of the symtable module. import symtable import unittest +from test import support +from test.support import os_helper TEST_CODE = """ @@ -282,10 +284,62 @@ class SymtableTest(unittest.TestCase): self.assertEqual(str(self.top), "") self.assertEqual(str(self.spam), "") + def test_symbol_repr(self): + self.assertEqual(repr(self.spam.lookup("glob")), + "") + self.assertEqual(repr(self.spam.lookup("bar")), + "") + self.assertEqual(repr(self.spam.lookup("a")), + "") + self.assertEqual(repr(self.spam.lookup("internal")), + "") + self.assertEqual(repr(self.spam.lookup("other_internal")), + "") + self.assertEqual(repr(self.internal.lookup("x")), + "") + self.assertEqual(repr(self.other_internal.lookup("some_var")), + "") + def test_symtable_entry_repr(self): expected = f"" 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 '':", lines) + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2023-09-08-12-10-10.gh-issue-85098.DfQbeJ.rst b/Misc/NEWS.d/next/Library/2023-09-08-12-10-10.gh-issue-85098.DfQbeJ.rst new file mode 100644 index 00000000000..cf0e782237b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-09-08-12-10-10.gh-issue-85098.DfQbeJ.rst @@ -0,0 +1,2 @@ +Implement the CLI of the :mod:`symtable` module and improve the repr of +:class:`~symtable.Symbol`.