Skip to content

[3.9] bpo-41840: Report module-level globals as both local and global in the symtable module (GH-22391) #22528

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions Lib/symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __call__(self, table, filename):
_newSymbolTable = SymbolTableFactory()


class SymbolTable(object):
class SymbolTable:

def __init__(self, raw_table, filename):
self._table = raw_table
Expand All @@ -47,7 +47,7 @@ def __repr__(self):
else:
kind = "%s " % self.__class__.__name__

if self._table.name == "global":
if self._table.name == "top":
return "<{0}SymbolTable for module {1}>".format(kind, self._filename)
else:
return "<{0}SymbolTable for {1} in {2}>".format(kind,
Expand Down Expand Up @@ -90,7 +90,9 @@ def lookup(self, name):
if sym is None:
flags = self._table.symbols[name]
namespaces = self.__check_children(name)
sym = self._symbols[name] = Symbol(name, flags, namespaces)
module_scope = (self._table.name == "top")
sym = self._symbols[name] = Symbol(name, flags, namespaces,
module_scope=module_scope)
return sym

def get_symbols(self):
Expand Down Expand Up @@ -163,13 +165,14 @@ def get_methods(self):
return self.__methods


class Symbol(object):
class Symbol:

def __init__(self, name, flags, namespaces=None):
def __init__(self, name, flags, namespaces=None, *, module_scope=False):
self.__name = name
self.__flags = flags
self.__scope = (flags >> SCOPE_OFF) & SCOPE_MASK # like PyST_GetScope()
self.__namespaces = namespaces or ()
self.__module_scope = module_scope

def __repr__(self):
return "<symbol {0!r}>".format(self.__name)
Expand All @@ -184,7 +187,10 @@ def is_parameter(self):
return bool(self.__flags & DEF_PARAM)

def is_global(self):
return bool(self.__scope in (GLOBAL_IMPLICIT, GLOBAL_EXPLICIT))
"""Return *True* if the sysmbol is global.
"""
return bool(self.__scope in (GLOBAL_IMPLICIT, GLOBAL_EXPLICIT)
or (self.__module_scope and self.__flags & DEF_BOUND))

def is_nonlocal(self):
return bool(self.__flags & DEF_NONLOCAL)
Expand All @@ -193,7 +199,10 @@ def is_declared_global(self):
return bool(self.__scope == GLOBAL_EXPLICIT)

def is_local(self):
return bool(self.__scope in (LOCAL, CELL))
"""Return *True* if the symbol is local.
"""
return bool(self.__scope in (LOCAL, CELL)
or (self.__module_scope and self.__flags & DEF_BOUND))

def is_annotated(self):
return bool(self.__flags & DEF_ANNOT)
Expand Down
18 changes: 16 additions & 2 deletions Lib/test/test_symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

glob = 42
some_var = 12
some_non_assigned_global_var = 11
some_assigned_global_var = 11

class Mine:
instance_var = 24
Expand All @@ -19,6 +21,8 @@ def a_method(p1, p2):

def spam(a, b, *var, **kw):
global bar
global some_assigned_global_var
some_assigned_global_var = 12
bar = 47
some_var = 10
x = 23
Expand Down Expand Up @@ -81,14 +85,14 @@ def test_children(self):

def test_lineno(self):
self.assertEqual(self.top.get_lineno(), 0)
self.assertEqual(self.spam.get_lineno(), 12)
self.assertEqual(self.spam.get_lineno(), 14)

def test_function_info(self):
func = self.spam
self.assertEqual(sorted(func.get_parameters()), ["a", "b", "kw", "var"])
expected = ['a', 'b', 'internal', 'kw', 'other_internal', 'some_var', 'var', 'x']
self.assertEqual(sorted(func.get_locals()), expected)
self.assertEqual(sorted(func.get_globals()), ["bar", "glob"])
self.assertEqual(sorted(func.get_globals()), ["bar", "glob", "some_assigned_global_var"])
self.assertEqual(self.internal.get_frees(), ("x",))

def test_globals(self):
Expand All @@ -99,6 +103,9 @@ def test_globals(self):
self.assertFalse(self.internal.lookup("x").is_global())
self.assertFalse(self.Mine.lookup("instance_var").is_global())
self.assertTrue(self.spam.lookup("bar").is_global())
# Module-scope globals are both global and local
self.assertTrue(self.top.lookup("some_non_assigned_global_var").is_global())
self.assertTrue(self.top.lookup("some_assigned_global_var").is_global())

def test_nonlocal(self):
self.assertFalse(self.spam.lookup("some_var").is_nonlocal())
Expand All @@ -109,6 +116,9 @@ def test_nonlocal(self):
def test_local(self):
self.assertTrue(self.spam.lookup("x").is_local())
self.assertFalse(self.spam.lookup("bar").is_local())
# Module-scope globals are both global and local
self.assertTrue(self.top.lookup("some_non_assigned_global_var").is_local())
self.assertTrue(self.top.lookup("some_assigned_global_var").is_local())

def test_free(self):
self.assertTrue(self.internal.lookup("x").is_free())
Expand Down Expand Up @@ -227,6 +237,10 @@ def test_bytes(self):
top = symtable.symtable(code, "?", "exec")
self.assertIsNotNone(find_block(top, "\u017d"))

def test_symtable_repr(self):
self.assertEqual(str(self.top), "<SymbolTable for module ?>")
self.assertEqual(str(self.spam), "<Function SymbolTable for spam in ?>")


if __name__ == '__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix a bug in the :mod:`symtable` module that was causing module-scope global
variables to not be reported as both local and global. Patch by Pablo
Galindo.