Skip to content

Commit 7ed9351

Browse files
committed
Add lru cache for moduleless objects
1 parent 5319c66 commit 7ed9351

2 files changed

Lines changed: 75 additions & 16 deletions

File tree

Lib/inspect.py

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
import tokenize
156156
import token
157157
import types
158+
import weakref
158159
import functools
159160
import builtins
160161
from keyword import iskeyword
@@ -918,6 +919,14 @@ def getfile(object):
918919
if object.__module__ == '__main__':
919920
raise OSError('source code not available')
920921
raise TypeError('{!r} is a built-in class'.format(object))
922+
if code := _getcode(object):
923+
return code.co_filename
924+
raise TypeError('module, class, method, function, traceback, frame, or '
925+
'code object was expected, got {}'.format(
926+
type(object).__name__))
927+
928+
def _getcode(object):
929+
"""Get the code object an object is associated with, if available."""
921930
if ismethod(object):
922931
object = object.__func__
923932
if isfunction(object):
@@ -927,10 +936,8 @@ def getfile(object):
927936
if isframe(object):
928937
object = object.f_code
929938
if iscode(object):
930-
return object.co_filename
931-
raise TypeError('module, class, method, function, traceback, frame, or '
932-
'code object was expected, got {}'.format(
933-
type(object).__name__))
939+
return object
940+
return None
934941

935942
def getmodulename(path):
936943
"""Return the module name for a given file, or None."""
@@ -980,6 +987,22 @@ def getabsfile(object, _filename=None):
980987

981988
modulesbyfile = {}
982989
_filesbymodname = {}
990+
_moduleless = OrderedDict()
991+
_MAX_MODULELESS = 200
992+
993+
def _is_cached_moduleless(code):
994+
if ref := _moduleless.get(id(code)):
995+
if ref() is code:
996+
_moduleless.move_to_end(id(code), last=True)
997+
return True
998+
return False
999+
1000+
def _cache_moduleless(code):
1001+
if code is None:
1002+
return
1003+
_moduleless[id(code)] = weakref.ref(code)
1004+
if len(_moduleless) > _MAX_MODULELESS:
1005+
_moduleless.popitem(last=False)
9831006

9841007
def getmodule(object, _filename=None):
9851008
"""Return the module an object was defined in, or None if not found."""
@@ -990,31 +1013,26 @@ def getmodule(object, _filename=None):
9901013
# Try the filename to modulename cache
9911014
if _filename is not None and _filename in modulesbyfile:
9921015
return sys.modules.get(modulesbyfile[_filename])
1016+
# Check for moduleless objects
1017+
code = _getcode(object)
1018+
if _is_cached_moduleless(code):
1019+
return None
9931020
# Try the cache again with the absolute file name
9941021
try:
9951022
file = getabsfile(object, _filename)
9961023
except (TypeError, FileNotFoundError):
1024+
_cache_moduleless(code)
9971025
return None
9981026
if file in modulesbyfile:
9991027
return sys.modules.get(modulesbyfile[file])
10001028
# Update the filename to module name cache and check yet again
1001-
# Copy sys.modules in order to cope with changes while iterating
1002-
for modname, module in sys.modules.copy().items():
1003-
if ismodule(module) and hasattr(module, '__file__'):
1004-
f = module.__file__
1005-
if f == _filesbymodname.get(modname, None):
1006-
# Have already mapped this module, so skip it
1007-
continue
1008-
_filesbymodname[modname] = f
1009-
f = getabsfile(module)
1010-
# Always map to the name the module knows itself by
1011-
modulesbyfile[f] = modulesbyfile[
1012-
os.path.realpath(f)] = module.__name__
1029+
_update_module_filename_cache()
10131030
if file in modulesbyfile:
10141031
return sys.modules.get(modulesbyfile[file])
10151032
# Check the main module
10161033
main = sys.modules['__main__']
10171034
if not hasattr(object, '__name__'):
1035+
_cache_moduleless(code)
10181036
return None
10191037
if hasattr(main, object.__name__):
10201038
mainobject = getattr(main, object.__name__)
@@ -1026,6 +1044,23 @@ def getmodule(object, _filename=None):
10261044
builtinobject = getattr(builtin, object.__name__)
10271045
if builtinobject is object:
10281046
return builtin
1047+
_cache_moduleless(code)
1048+
return None
1049+
1050+
def _update_module_filename_cache():
1051+
"""Update the filename to module name cache."""
1052+
# Copy sys.modules in order to cope with changes while iterating
1053+
for modname, module in sys.modules.copy().items():
1054+
if ismodule(module) and hasattr(module, '__file__'):
1055+
f = module.__file__
1056+
if f == _filesbymodname.get(modname, None):
1057+
# Have already mapped this module, so skip it
1058+
continue
1059+
_filesbymodname[modname] = f
1060+
f = getabsfile(module)
1061+
# Always map to the name the module knows itself by
1062+
modulesbyfile[f] = modulesbyfile[
1063+
os.path.realpath(f)] = module.__name__
10291064

10301065

10311066
class ClassFoundException(Exception):

Lib/test/test_inspect/test_inspect.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,13 +671,35 @@ def test_getmodule(self):
671671

672672
def test_getmodule_file_not_found(self):
673673
# See bpo-45406
674+
checked_objs = set()
674675
def _getabsfile(obj, _filename):
676+
# Ensure non-caching is working
677+
self.assertNotIn(obj, checked_objs)
678+
checked_objs.add(obj)
675679
raise FileNotFoundError('bad file')
676680
with unittest.mock.patch('inspect.getabsfile', _getabsfile):
677681
f = inspect.currentframe()
678682
self.assertIsNone(inspect.getmodule(f))
679683
inspect.getouterframes(f) # smoke test
680684

685+
def test_getmodule_cache(self):
686+
with unittest.mock.patch(
687+
'inspect._update_module_filename_cache',
688+
side_effect=inspect._update_module_filename_cache,
689+
) as mock_update, unittest.mock.patch(
690+
'inspect.getmodule', side_effect=inspect.getmodule
691+
) as mock_getmodule:
692+
d = {}
693+
exec("def x(): pass", d)
694+
inspect.getmodule(d["x"].__code__)
695+
self.assertTrue(mock_update.call_count > 0)
696+
mock_getmodule.reset_mock()
697+
mock_update.reset_mock()
698+
inspect.getmodule(d["x"].__code__)
699+
self.assertEqual(mock_getmodule.call_count, 1)
700+
self.assertEqual(mock_update.call_count, 0)
701+
inspect._moduleless.clear()
702+
681703
def test_getframeinfo_get_first_line(self):
682704
frame_info = inspect.getframeinfo(self.fodderModule.fr, 50)
683705
self.assertEqual(frame_info.code_context[0], "# line 1\n")
@@ -750,6 +772,7 @@ def test_getmodule_recursion(self):
750772
self.assertEqual(inspect.getsourcefile(m.x.__code__), '<string>')
751773
del sys.modules[name]
752774
inspect.getmodule(compile('a=10','','single'))
775+
inspect._moduleless.clear()
753776

754777
def test_proceed_with_fake_filename(self):
755778
'''doctest monkeypatches linecache to enable inspection'''
@@ -767,6 +790,7 @@ def monkey(filename, module_globals=None):
767790
inspect.getsource(ns["x"])
768791
finally:
769792
linecache.getlines = getlines
793+
inspect._moduleless.clear()
770794

771795
def test_getsource_on_code_object(self):
772796
self.assertSourceEqual(mod.eggs.__code__, 12, 18)

0 commit comments

Comments
 (0)