Skip to content

Commit ef27738

Browse files
committed
gh-92041: Address frame lookup review and document cache precedence
1 parent bda170b commit ef27738

4 files changed

Lines changed: 96 additions & 10 deletions

File tree

‎Doc/library/inspect.rst‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,6 +771,16 @@ Retrieving source code
771771
Try to guess which module an object was defined in. Return ``None``
772772
if the module cannot be determined.
773773

774+
.. versionchanged:: 3.16
775+
On a filename-cache miss, frames and tracebacks may be resolved from
776+
a registered module whose global namespace and canonical filename match
777+
the frame. Existing filename-cache entries retain precedence. If multiple
778+
modules share a filename, the result may differ from earlier Python
779+
versions and can depend on previous lookups: the frame's globals owner
780+
is not guaranteed to take precedence. Code objects continue to use
781+
filename-based lookup and can also be affected by earlier frame or
782+
traceback lookups.
783+
774784

775785
.. function:: getsourcefile(object)
776786

‎Lib/inspect.py‎

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -936,16 +936,17 @@ def _getframemodule(frame, filename):
936936
if type(name) is not str:
937937
return None
938938
module = sys.modules.get(name)
939-
# Leave dynamic module metadata to the existing filename fallback.
940-
if (type(module) is not types.ModuleType or
941-
module.__dict__ is not namespace or '__getattr__' in namespace or
942-
type(namespace.get('__file__')) is not str):
939+
# Leave module subclasses and dynamically supplied filenames to the fallback.
940+
if (type(module) is not types.ModuleType
941+
or module.__dict__ is not namespace
942+
or type(namespace.get('__file__')) is not str):
943943
return None
944944
try:
945945
if filename == getabsfile(module):
946946
return module
947-
except (TypeError, FileNotFoundError):
947+
except FileNotFoundError, TypeError:
948948
pass
949+
return None
949950

950951

951952
def getmodule(object, _filename=None):
@@ -965,8 +966,12 @@ def getmodule(object, _filename=None):
965966
return None
966967
if file in modulesbyfile:
967968
return sys.modules.get(modulesbyfile[file])
968-
frame = object if isframe(object) else (
969-
object.tb_frame if istraceback(object) else None)
969+
if isframe(object):
970+
frame = object
971+
elif istraceback(object):
972+
frame = object.tb_frame
973+
else:
974+
frame = None
970975
if frame is not None and (
971976
_filename is None or _filename == frame.f_code.co_filename):
972977
module = _getframemodule(frame, file)

‎Lib/test/test_inspect/test_inspect.py‎

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1090,6 +1090,76 @@ def test_filename_collision_uses_globals_owner(self):
10901090
for object in (frame, traceback):
10911091
self.assertIs(inspect.getmodule(object), self.module)
10921092

1093+
# An unrelated miss scans the registry and can replace that entry.
1094+
self.assertIsNone(inspect.getmodule(
1095+
compile('pass', '<inspect-unrelated-miss>', 'exec')))
1096+
for object in (frame, traceback):
1097+
self.assertIs(inspect.getmodule(object), other)
1098+
1099+
# Bare code uses the filename map, including entries populated
1100+
# by an earlier frame lookup.
1101+
inspect.modulesbyfile.clear()
1102+
inspect._filesbymodname.clear()
1103+
self.assertIs(inspect.getmodule(frame.f_code), other)
1104+
inspect.modulesbyfile.clear()
1105+
inspect._filesbymodname.clear()
1106+
self.assertIs(inspect.getmodule(frame), self.module)
1107+
self.assertIs(inspect.getmodule(frame.f_code), self.module)
1108+
1109+
def test_static_filename_with_module_getattr_avoids_scan(self):
1110+
def module_getattr(name):
1111+
self.fail(f'unexpected module attribute lookup: {name}')
1112+
1113+
self.module.__getattr__ = module_getattr
1114+
frame, traceback = self.capture()
1115+
1116+
class NoScanDict(dict):
1117+
def copy(self):
1118+
raise AssertionError('registered frame scanned sys.modules')
1119+
1120+
with unittest.mock.patch.object(sys, 'modules', NoScanDict(sys.modules)):
1121+
for object in (frame, traceback):
1122+
inspect.modulesbyfile.clear()
1123+
inspect._filesbymodname.clear()
1124+
self.assertIs(inspect.getmodule(object), self.module)
1125+
1126+
def test_filename_supplied_by_module_getattr_falls_back(self):
1127+
del self.module.__file__
1128+
1129+
def module_getattr(name):
1130+
if name == '__file__':
1131+
return self.module.source
1132+
raise AttributeError(name)
1133+
1134+
self.module.__getattr__ = module_getattr
1135+
for suffix in ('.dynamic', '.changed'):
1136+
self.module.source = self.filename + suffix
1137+
frame, traceback = self.capture(filename=self.module.source)
1138+
for object in (frame, traceback):
1139+
inspect.modulesbyfile.clear()
1140+
inspect._filesbymodname.clear()
1141+
self.assertIs(inspect.getmodule(object), self.module)
1142+
1143+
def test_loader_supplied_by_module_getattr(self):
1144+
self.module.__file__ = self.filename + 'c'
1145+
del self.module.__loader__
1146+
loader = object()
1147+
requested = []
1148+
1149+
def module_getattr(name):
1150+
requested.append(name)
1151+
if name == '__loader__':
1152+
return loader
1153+
raise AttributeError(name)
1154+
1155+
self.module.__getattr__ = module_getattr
1156+
frame, traceback = self.capture()
1157+
for obj in (frame, traceback):
1158+
inspect.modulesbyfile.clear()
1159+
inspect._filesbymodname.clear()
1160+
self.assertIs(inspect.getmodule(obj), self.module)
1161+
self.assertIn('__loader__', requested)
1162+
10931163
def test_unregistered_namespace_uses_filename_fallback(self):
10941164
namespace = {'__name__': self.module.__name__}
10951165
frame, traceback = self.capture(namespace)
Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
Speed up :func:`inspect.getmodule` for frames and tracebacks whose globals
2-
belong to a registered module with a matching filename. The globals owner
3-
takes precedence over unrelated modules sharing the same filename.
1+
Avoid scanning :data:`sys.modules` in :func:`inspect.getmodule` for an uncached
2+
frame or traceback whose globals identify a registered module with a matching
3+
canonical filename. Existing filename-cache entries retain precedence. When
4+
modules share a filename, results can depend on earlier lookups.

0 commit comments

Comments
 (0)