Skip to content

Commit 33ccc7b

Browse files
committed
gh-92041: Safely cache inspect.getmodule filename lookups
Track sys.modules and relevant module metadata changes with a per-interpreter generation so filename-based lookups can be reused without returning stale results. Preserve direct registered-frame resolution and the existing filename fallback, including dynamic modules and replacement registries.
1 parent 0afefeb commit 33ccc7b

9 files changed

Lines changed: 859 additions & 92 deletions

File tree

Include/internal/pycore_interp_structs.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,8 @@ struct _import_runtime_state {
319319
struct _import_state {
320320
/* cached sys.modules dictionary */
321321
PyObject *modules;
322+
/* Incremented when sys.modules or module lookup metadata changes. */
323+
uint64_t module_registry_version;
322324
/* This is the list of module objects for all legacy (single-phase init)
323325
extension modules ever loaded in this process (i.e. imported
324326
in this interpreter or in any other). Py_None stands in for

Lib/inspect.py

Lines changed: 236 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@
143143

144144

145145
import abc
146+
import _imp
146147
from annotationlib import Format, ForwardRef
147148
from annotationlib import get_annotations # re-exported
148149
import ast
@@ -925,7 +926,65 @@ def getabsfile(object, _filename=None):
925926

926927
modulesbyfile = {}
927928
_filesbymodname = {}
928-
_modulesbyfile_snapshot = None
929+
# (registry version, filename map, per-module metadata, volatile metadata)
930+
_modulesbyfile_cache = (None, {}, {}, ())
931+
_module_cache_missing = object()
932+
933+
934+
def _get_volatile_module_metadata(module):
935+
"""Return unwatchable module metadata used to validate the cache."""
936+
try:
937+
module_file = getattr(module, '__file__')
938+
except AttributeError:
939+
return _module_cache_missing, _module_cache_missing
940+
if module_file is None:
941+
return None, _module_cache_missing
942+
return module_file, getattr(module, '__name__')
943+
944+
945+
def _volatile_modules_unchanged(modules, volatile_modules):
946+
for modname, module_file, module_name in volatile_modules:
947+
module = modules.get(modname, _module_cache_missing)
948+
if not ismodule(module):
949+
return False
950+
current_file, current_name = _get_volatile_module_metadata(module)
951+
if current_file != module_file or current_name != module_name:
952+
return False
953+
return True
954+
955+
956+
def _module_cache_is_current(modules, version, cache):
957+
if (modules is not sys.modules or
958+
version is None or version != cache[0]):
959+
return False
960+
volatile_modules = cache[3]
961+
if not volatile_modules:
962+
return True
963+
if not _volatile_modules_unchanged(modules, volatile_modules):
964+
return False
965+
# Dynamic metadata may execute arbitrary Python code.
966+
return (modules is sys.modules and
967+
version == _imp._get_module_registry_version(modules))
968+
969+
970+
def _publish_module_cache(version, modules_by_file, files_by_module,
971+
volatile_modules):
972+
"""Publish a consistent filename cache and update its legacy mirrors."""
973+
global _modulesbyfile_cache
974+
975+
# Keep these longstanding, externally visible dictionaries alive, but do
976+
# not use them as the authoritative cache: updating two dictionaries and a
977+
# version cannot be made atomic for concurrent getmodule() calls.
978+
modulesbyfile.clear()
979+
modulesbyfile.update(modules_by_file)
980+
_filesbymodname.clear()
981+
_filesbymodname.update(
982+
(modname, metadata[0])
983+
for modname, metadata in files_by_module.items()
984+
)
985+
# This single assignment is the authoritative publication point.
986+
_modulesbyfile_cache = (
987+
version, modules_by_file, files_by_module, volatile_modules)
929988

930989

931990
def _getframemodule(frame, _filename=None):
@@ -937,17 +996,29 @@ def _getframemodule(frame, _filename=None):
937996
module = sys.modules.get(module_name)
938997
if not (ismodule(module) and module.__dict__ is frame_globals):
939998
return None
940-
module_file = getattr(module, '__file__', None)
999+
try:
1000+
if type(module) is types.ModuleType:
1001+
module_file = module.__file__
1002+
else:
1003+
# A module subclass can supply dynamic metadata through
1004+
# descriptors. getattr() avoids a module-attribute specialization
1005+
# that may read the subclass's underlying module dict.
1006+
module_file = getattr(module, '__file__')
1007+
except AttributeError:
1008+
return None
9411009
if module_file is None:
9421010
return None
1011+
frame_filename = (
1012+
frame.f_code.co_filename if _filename is None else _filename
1013+
)
9431014
try:
944-
file = getabsfile(frame, _filename)
1015+
file = getabsfile(frame, frame_filename)
9451016
except (TypeError, FileNotFoundError):
9461017
return None
947-
if frame.f_code.co_filename == module_file:
1018+
if frame_filename == module_file:
9481019
return module
9491020
try:
950-
module_file = getabsfile(module)
1021+
module_file = getabsfile(module, module_file)
9511022
except (TypeError, FileNotFoundError):
9521023
return None
9531024
if file == module_file or file == os.path.realpath(module_file):
@@ -970,45 +1041,172 @@ def getmodule(object, _filename=None):
9701041
if module is not None:
9711042
return module
9721043

973-
# Try the filename to modulename cache
974-
if _filename is not None and _filename in modulesbyfile:
975-
return sys.modules.get(modulesbyfile[_filename])
1044+
modules = sys.modules
1045+
version = _imp._get_module_registry_version(modules)
1046+
cache = _modulesbyfile_cache
1047+
cache_is_current = _module_cache_is_current(modules, version, cache)
1048+
cached_modulesbyfile = cache[1]
1049+
1050+
# Try the filename to modulename cache. Recheck the version after reading
1051+
# the module so a concurrent registry mutation cannot make the result
1052+
# stale.
1053+
if (cache_is_current and _filename is not None and
1054+
_filename in cached_modulesbyfile):
1055+
module = modules.get(cached_modulesbyfile[_filename])
1056+
if _module_cache_is_current(
1057+
modules, _imp._get_module_registry_version(modules), cache):
1058+
return module
1059+
cache_is_current = False
1060+
9761061
# Try the cache again with the absolute file name
9771062
try:
9781063
file = getabsfile(object, _filename)
9791064
except (TypeError, FileNotFoundError):
9801065
return None
981-
if file in modulesbyfile:
982-
return sys.modules.get(modulesbyfile[file])
983-
# Update the filename to module name cache only when sys.modules is
984-
# replaced or its module names have changed since the previous scan.
985-
# Retaining module values in the snapshot would keep removed modules alive.
986-
global _modulesbyfile_snapshot
987-
modules = sys.modules
988-
modules_id = id(modules)
989-
try:
990-
snapshot = (modules_id, tuple(modules))
991-
except RuntimeError:
992-
# The mapping changed while its keys were being copied.
993-
snapshot = None
994-
if snapshot is None or snapshot != _modulesbyfile_snapshot:
995-
# Copy sys.modules in order to cope with changes while iterating.
996-
modules = modules.copy()
997-
snapshot = (modules_id, tuple(modules))
998-
for modname, module in modules.items():
999-
if ismodule(module) and hasattr(module, '__file__'):
1000-
f = module.__file__
1001-
if f == _filesbymodname.get(modname, None):
1002-
# Have already mapped this module, so skip it
1066+
if modules is not sys.modules:
1067+
# getabsfile() may execute user code through dynamic module metadata.
1068+
modules = sys.modules
1069+
cache_is_current = False
1070+
if not cache_is_current:
1071+
# getabsfile() can recurse through getsourcefile() and populate this
1072+
# cache. Avoid rebuilding it a second time in the outer call.
1073+
version = _imp._get_module_registry_version(modules)
1074+
cache = _modulesbyfile_cache
1075+
cache_is_current = _module_cache_is_current(modules, version, cache)
1076+
cached_modulesbyfile = cache[1]
1077+
if cache_is_current and file in cached_modulesbyfile:
1078+
module = modules.get(cached_modulesbyfile[file])
1079+
if _module_cache_is_current(
1080+
modules, _imp._get_module_registry_version(modules), cache):
1081+
return module
1082+
cache_is_current = False
1083+
1084+
if cache_is_current:
1085+
# A stable cache miss is a negative-cache hit. Validate it after the
1086+
# lookup just as for a positive hit.
1087+
cache_is_current = _module_cache_is_current(
1088+
modules, _imp._get_module_registry_version(modules), cache)
1089+
1090+
# Update the filename to module name cache when the canonical module
1091+
# registry or relevant module metadata has changed. Replacement
1092+
# sys.modules mappings cannot be watched, so scan those on every miss.
1093+
if not cache_is_current:
1094+
# Retry once if a mutation overlaps the scan. This handles imports
1095+
# triggered by the first scan without spinning under sustained churn.
1096+
for _ in range(2):
1097+
# Copy sys.modules in order to cope with changes while iterating.
1098+
modules = sys.modules
1099+
version = _imp._get_module_registry_version(modules)
1100+
modules_copy = modules.copy()
1101+
new_modulesbyfile = {}
1102+
new_filesbymodname = {}
1103+
volatile_modules = []
1104+
cached_filesbymodname = _modulesbyfile_cache[2]
1105+
for modname, module in modules_copy.items():
1106+
if ismodule(module):
1107+
if type(module) is types.ModuleType:
1108+
module_dict = module.__dict__
1109+
try:
1110+
f = module.__file__
1111+
except AttributeError:
1112+
if '__getattr__' in module_dict:
1113+
volatile_modules.append((
1114+
modname,
1115+
_module_cache_missing,
1116+
_module_cache_missing,
1117+
))
1118+
continue
1119+
volatile = (
1120+
module_dict.get(
1121+
'__file__', _module_cache_missing) is not f
1122+
)
1123+
else:
1124+
# Module subclasses may resolve __file__ or __name__
1125+
# dynamically. Validate just these entries on cache
1126+
# hits instead of making every fallback rescan all of
1127+
# sys.modules.
1128+
module_dict = None
1129+
volatile = True
1130+
try:
1131+
f = getattr(module, '__file__')
1132+
except AttributeError:
1133+
volatile_modules.append((
1134+
modname,
1135+
_module_cache_missing,
1136+
_module_cache_missing,
1137+
))
1138+
continue
1139+
if f is None:
1140+
if volatile:
1141+
volatile_modules.append((
1142+
modname, None, _module_cache_missing))
1143+
continue
1144+
if module_dict is None:
1145+
module_name = getattr(module, '__name__')
1146+
else:
1147+
module_name = module.__name__
1148+
if (module_dict is not None and
1149+
module_dict.get(
1150+
'__name__', _module_cache_missing)
1151+
is not module_name):
1152+
volatile = True
1153+
if volatile:
1154+
volatile_modules.append(
1155+
(modname, f, module_name))
1156+
cached = cached_filesbymodname.get(modname)
1157+
if (isinstance(cached, tuple) and len(cached) == 4 and
1158+
f == cached[0] and module_name == cached[1]):
1159+
_, _, absfile, realpath = cached
1160+
else:
1161+
# Canonicalize the same metadata value that was
1162+
# validated above. Re-reading __file__ here can
1163+
# observe a different dynamic value.
1164+
absfile = getabsfile(module, f)
1165+
realpath = os.path.realpath(absfile)
1166+
new_filesbymodname[modname] = (
1167+
f, module_name, absfile, realpath)
1168+
# Always map to the name the module knows itself by.
1169+
new_modulesbyfile[absfile] = new_modulesbyfile[
1170+
realpath] = module_name
1171+
module = None
1172+
found = file in new_modulesbyfile
1173+
if found:
1174+
module = modules.get(new_modulesbyfile[file])
1175+
# Watcher callbacks run before their mutations. Validate both the
1176+
# watched generation and the small set of dynamic metadata before
1177+
# retaining the rebuilt caches.
1178+
current_version = _imp._get_module_registry_version(modules)
1179+
if version is not None:
1180+
if (modules is not sys.modules or
1181+
version != current_version):
10031182
continue
1004-
_filesbymodname[modname] = f
1005-
f = getabsfile(module)
1006-
# Always map to the name the module knows itself by
1007-
modulesbyfile[f] = modulesbyfile[
1008-
os.path.realpath(f)] = module.__name__
1009-
_modulesbyfile_snapshot = snapshot
1010-
if file in modulesbyfile:
1011-
return sys.modules.get(modulesbyfile[file])
1183+
volatile_current = _volatile_modules_unchanged(
1184+
modules, volatile_modules)
1185+
current_version = _imp._get_module_registry_version(modules)
1186+
if (modules is not sys.modules or
1187+
version != current_version or
1188+
not volatile_current):
1189+
continue
1190+
_publish_module_cache(
1191+
version,
1192+
new_modulesbyfile,
1193+
new_filesbymodname,
1194+
tuple(volatile_modules),
1195+
)
1196+
if found:
1197+
return module
1198+
break
1199+
if modules is sys.modules:
1200+
# A replacement sys.modules mapping cannot be watched, but its
1201+
# just-scanned result is still useful for this call.
1202+
_publish_module_cache(
1203+
None, new_modulesbyfile, new_filesbymodname,
1204+
tuple(volatile_modules))
1205+
if found:
1206+
return module
1207+
break
1208+
# If both attempts overlap mutations, fall through rather than using
1209+
# a snapshot that could not be validated.
10121210
# Check the main module
10131211
main = sys.modules['__main__']
10141212
if not hasattr(object, '__name__'):

Lib/test/libregrtest/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def clear_caches():
296296
inspect._shadowed_dict_from_weakref_mro_tuple.cache_clear()
297297
inspect._filesbymodname.clear()
298298
inspect.modulesbyfile.clear()
299-
inspect._modulesbyfile_snapshot = None
299+
inspect._modulesbyfile_cache = (None, {}, {}, ())
300300

301301
try:
302302
importlib_metadata = sys.modules['importlib.metadata']

0 commit comments

Comments
 (0)