From 7754ac626fae6b4fcb95f57b40da69b542bfdc09 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 21 Mar 2020 11:44:46 +0200 Subject: [PATCH 1/2] Improve CLI of symtable. --- Lib/symtable.py | 50 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/Lib/symtable.py b/Lib/symtable.py index 5bea7cf6155486..04ba8d135b31ca 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -176,7 +176,19 @@ def __init__(self, name, flags, namespaces=None): self.__namespaces = namespaces or () def __repr__(self): - return "".format(self.__name) + flags_repr = [_scopes_value_to_name.get(self.__scope, str(self.__scope))] + for flagname, flagvalue in _flags.items(): + if self.__flags & flagvalue: + flags_repr.append(flagname) + return "".format(self.__name, ', '.join(flags_repr)) + + def _scope_str(self): + return _scopes_value_to_name[self.__scope] + + def _flags_str(self): + for flagname, flagvalue in _flags.items(): + if self.__flags & flagvalue == flagvalue: + yield flagname def get_name(self): return self.__name @@ -237,11 +249,33 @@ def get_namespace(self): raise ValueError("name is bound to multiple namespaces") return self.__namespaces[0] +_flags = {'USE': USE} +_flags.update(kv for kv in globals().items() if kv[0].startswith('DEF_')) +_scopes_names = ('FREE', 'LOCAL', 'GLOBAL_IMPLICIT', 'GLOBAL_EXPLICIT', 'CELL') +_scopes_name_to_value = {n: globals()[n] for n in _scopes_names} +_scopes_value_to_name = {v: k for k, v in _scopes_name_to_value.items()} + + 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 + + def print_symbols(table, level=0): + indent = ' ' * level + for ident in table.get_identifiers(): + info = table.lookup(ident) + flags = ', '.join(info._flags_str()).lower() + print(f'{indent}{info._scope_str().lower()} symbol {info.get_name()!r}: {flags}') + for table2 in info.get_namespaces(): + print_symbols(table2, level + 1) + print() + + if len(sys.argv) > 1: + for filename in sys.argv[1:]: + with open(filename, 'rb') as f: + src = f.read() + mod = symtable(src, filename, 'exec') + print_symbols(mod) + else: + src = sys.stdin.read() + mod = symtable(src, '', 'exec') + print_symbols(mod) From 9e575aa5be1660df0b63ac863422c4ffafeaeef6 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 8 Sep 2023 13:39:09 +0300 Subject: [PATCH 2/2] Add tests and docs, change output. --- Doc/library/symtable.rst | 18 ++++++ Lib/symtable.py | 63 ++++++++++--------- Lib/test/test_symtable.py | 54 ++++++++++++++++ ...3-09-08-12-10-10.gh-issue-85098.DfQbeJ.rst | 2 + 4 files changed, 109 insertions(+), 28 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-09-08-12-10-10.gh-issue-85098.DfQbeJ.rst diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst index 65ff5bfe7abd61..877917277ed98d 100644 --- a/Doc/library/symtable.rst +++ b/Doc/library/symtable.rst @@ -197,3 +197,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 880c482d2182b3..848f7e63b54238 100644 --- a/Lib/symtable.py +++ b/Lib/symtable.py @@ -226,17 +226,14 @@ def __init__(self, name, flags, namespaces=None, *, module_scope=False): self.__module_scope = module_scope def __repr__(self): - flags_repr = [_scopes_value_to_name.get(self.__scope, str(self.__scope))] - for flagname, flagvalue in _flags.items(): - if self.__flags & flagvalue: - flags_repr.append(flagname) - return "".format(self.__name, ', '.join(flags_repr)) + flags_str = '|'.join(self._flags_str()) + return f'' def _scope_str(self): - return _scopes_value_to_name[self.__scope] + return _scopes_value_to_name.get(self.__scope) or str(self.__scope) def _flags_str(self): - for flagname, flagvalue in _flags.items(): + for flagname, flagvalue in _flags: if self.__flags & flagvalue == flagvalue: yield flagname @@ -328,33 +325,43 @@ def get_namespace(self): else: return self.__namespaces[0] -_flags = {'USE': USE} -_flags.update(kv for kv in globals().items() if kv[0].startswith('DEF_')) + +_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_name_to_value = {n: globals()[n] for n in _scopes_names} -_scopes_value_to_name = {v: k for k, v in _scopes_name_to_value.items()} +_scopes_value_to_name = {globals()[n]: n for n in _scopes_names} -if __name__ == "__main__": +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(): - info = table.lookup(ident) - flags = ', '.join(info._flags_str()).lower() - print(f'{indent}{info._scope_str().lower()} symbol {info.get_name()!r}: {flags}') - for table2 in info.get_namespaces(): - print_symbols(table2, level + 1) - print() - - if len(sys.argv) > 1: - for filename in sys.argv[1:]: + 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) - else: - src = sys.stdin.read() - mod = symtable(src, '', 'exec') - print_symbols(mod) + mod = symtable(src, filename, 'exec') + print_symbols(mod) + + +if __name__ == "__main__": + import sys + main(sys.argv[1:]) diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py index 25714aecda3a15..d38d29c7a49cc9 100644 --- a/Lib/test/test_symtable.py +++ b/Lib/test/test_symtable.py @@ -4,6 +4,8 @@ import symtable import unittest +from test import support +from test.support import os_helper TEST_CODE = """ @@ -251,6 +253,58 @@ def test_symtable_repr(self): 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")), + "") + + +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 00000000000000..cf0e782237b2c0 --- /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`.