Skip to content

Commit 73f440d

Browse files
committed
Add recursion-break test and enhance hash
Recursion-break test demonstrates scenario where none-cache avoids second pass through module cache loop. This also revealed identical exec strings could have equal hash. Enhanced hash with object id to ensure such instances remain distinct.
1 parent 2da643d commit 73f440d

2 files changed

Lines changed: 28 additions & 14 deletions

File tree

Lib/inspect.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,7 @@ def getmodule(object, _filename=None):
987987
return sys.modules.get(modulesbyfile[_filename])
988988
# Compute hash to track moduleless objects
989989
code = _getcode(object)
990-
hashcode = hash(code) if code else None
990+
hashcode = id(code) ^ hash(code) if code else None
991991
if hashcode and hashcode in _moduleless:
992992
return None
993993
# Try the cache again with the absolute file name
@@ -1000,19 +1000,7 @@ def getmodule(object, _filename=None):
10001000
return None
10011001
if file in modulesbyfile:
10021002
return sys.modules.get(modulesbyfile[file])
1003-
# Update the filename to module name cache and check yet again
1004-
# Copy sys.modules in order to cope with changes while iterating
1005-
for modname, module in sys.modules.copy().items():
1006-
if ismodule(module) and hasattr(module, '__file__'):
1007-
f = module.__file__
1008-
if f == _filesbymodname.get(modname, None):
1009-
# Have already mapped this module, so skip it
1010-
continue
1011-
_filesbymodname[modname] = f
1012-
f = getabsfile(module)
1013-
# Always map to the name the module knows itself by
1014-
modulesbyfile[f] = modulesbyfile[
1015-
os.path.realpath(f)] = module.__name__
1003+
_update_module_file_name_cache()
10161004
if file in modulesbyfile:
10171005
return sys.modules.get(modulesbyfile[file])
10181006
# Check the main module
@@ -1033,6 +1021,20 @@ def getmodule(object, _filename=None):
10331021
_moduleless.add(hashcode)
10341022
return None
10351023

1024+
def _update_module_file_name_cache():
1025+
"""Update the filename to module name cache."""
1026+
# Copy sys.modules in order to cope with changes while iterating
1027+
for modname, module in sys.modules.copy().items():
1028+
if ismodule(module) and hasattr(module, '__file__'):
1029+
f = module.__file__
1030+
if f == _filesbymodname.get(modname, None):
1031+
# Have already mapped this module, so skip it
1032+
continue
1033+
_filesbymodname[modname] = f
1034+
f = getabsfile(module)
1035+
# Always map to the name the module knows itself by
1036+
modulesbyfile[f] = modulesbyfile[
1037+
os.path.realpath(f)] = module.__name__
10361038

10371039
class ClassFoundException(Exception):
10381040
pass

Lib/test/test_inspect.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,18 @@ def _getabsfile(obj, _filename):
563563
self.assertIsNone(inspect.getmodule(f))
564564
inspect.getouterframes(f) # smoke test
565565

566+
def test_getmodule_break_recursion(self):
567+
with unittest.mock.patch(
568+
'inspect._update_module_file_name_cache',
569+
) as mock_update, unittest.mock.patch(
570+
'inspect.getmodule', side_effect=inspect.getmodule
571+
) as mock_getmodule:
572+
d = {}
573+
exec("def x(): pass", d)
574+
inspect.getmodule(d["x"].__code__)
575+
self.assertEqual(mock_getmodule.call_count, 2)
576+
self.assertEqual(mock_update.call_count, 1)
577+
566578
def test_getframeinfo_get_first_line(self):
567579
frame_info = inspect.getframeinfo(self.fodderModule.fr, 50)
568580
self.assertEqual(frame_info.code_context[0], "# line 1\n")

0 commit comments

Comments
 (0)