]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-85098: Implement functional CLI of symtable (#109112)
authorSerhiy Storchaka <storchaka@gmail.com>
Tue, 7 Nov 2023 16:32:16 +0000 (18:32 +0200)
committerGitHub <noreply@github.com>
Tue, 7 Nov 2023 16:32:16 +0000 (16:32 +0000)
Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
Doc/library/symtable.rst
Lib/symtable.py
Lib/test/test_symtable.py
Misc/NEWS.d/next/Library/2023-09-08-12-10-10.gh-issue-85098.DfQbeJ.rst [new file with mode: 0644]

index 85eae5f38225758cca0c4862f21073a6d79b0b50..46159dcef940e7c1792dfe4d498377351266597c 100644 (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.
index 4b0bc6f497a5535d5944b6d9fd0b027161ac1f9d..17f820abd566604ca97d5b0fcfe45a9bfc9ef6f7 100644 (file)
@@ -233,7 +233,16 @@ class Symbol:
         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 @@ 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 = '<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:])
index 82c1d7c856a1e599c46fec9fcffc4d0db3a5c959..987e9e32afc3255e4932338cd43c15a7a8d83720 100644 (file)
@@ -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), "<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()
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 (file)
index 0000000..cf0e782
--- /dev/null
@@ -0,0 +1,2 @@
+Implement the CLI of the :mod:`symtable` module and improve the repr of
+:class:`~symtable.Symbol`.