From da559e7d67af9c24dfa1c68926b2a750ed3c553c Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 30 Aug 2026 22:57:19 +0200 Subject: [PATCH 1/2] gh-92041: Avoid a second sys.modules scan in inspect.getmodule Pass the filename to getabsfile() explicitly, so that computing the cache key does not recurse into getmodule() through the loader check in getsourcefile(). Co-Authored-By: Claude Fable 5 --- Lib/inspect.py | 2 ++ .../next/Library/2026-08-30-12-00-00.gh-issue-92041.nRcsQe.rst | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-30-12-00-00.gh-issue-92041.nRcsQe.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index c52469e63861a22..f726e2bafb08894 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -938,6 +938,8 @@ def getmodule(object, _filename=None): return sys.modules.get(modulesbyfile[_filename]) # Try the cache again with the absolute file name try: + if _filename is None: + _filename = getfile(object) file = getabsfile(object, _filename) except (TypeError, FileNotFoundError): return None diff --git a/Misc/NEWS.d/next/Library/2026-08-30-12-00-00.gh-issue-92041.nRcsQe.rst b/Misc/NEWS.d/next/Library/2026-08-30-12-00-00.gh-issue-92041.nRcsQe.rst new file mode 100644 index 000000000000000..d3b6af46d555271 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-30-12-00-00.gh-issue-92041.nRcsQe.rst @@ -0,0 +1,2 @@ +Speed up :func:`inspect.getmodule` for objects resolved by filename, such +as frames, tracebacks and code objects. From 03df224b0164352e784baaa962c8e41735b7121f Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sun, 30 Aug 2026 23:32:53 +0200 Subject: [PATCH 2/2] gh-92041: Test getmodule of a frame exec'd in a fresh namespace A frame whose filename belongs to a zipimported module must resolve through the filename fallback so getsource() can use the module's loader. Test adapted from PR #92042. Co-authored-by: Mike Decker Co-Authored-By: Claude Fable 5 --- Lib/test/test_zipimport_support.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Lib/test/test_zipimport_support.py b/Lib/test/test_zipimport_support.py index 2b28f46149b4ff3..3c71e42072b85f4 100644 --- a/Lib/test/test_zipimport_support.py +++ b/Lib/test/test_zipimport_support.py @@ -3,6 +3,7 @@ # The tests are centralised in this fashion to make it easy to drop them # if a platform doesn't support zipimport import test.support +import importlib import os import os.path import sys @@ -96,6 +97,35 @@ def test_inspect_getsource_issue4223(self): finally: del sys.modules["zip_pkg"] + def test_inspect_fresh_namespace_uses_module_loader(self): + # gh-92041: a frame exec'd in a plain namespace resolves to the + # zipimported module owning its filename, so getsource() can use + # the module's loader. + test_src = textwrap.dedent("""\ + import inspect + + def capture(): + return inspect.currentframe() + + frame = capture() + """) + with os_helper.temp_dir() as d: + script_name = make_script(d, "zipped_mod", test_src) + zip_name, _ = make_zip_script(d, "test_zip", script_name) + os.remove(script_name) + sys.path.insert(0, zip_name) + module = importlib.import_module("zipped_mod") + try: + namespace = {} + exec(compile(test_src, module.__file__, "exec"), namespace) + frame = namespace["frame"] + self.assertIs(inspect.getmodule(frame), module) + self.assertEqual(inspect.getsource(frame), + "def capture():\n" + " return inspect.currentframe()\n") + finally: + del sys.modules["zipped_mod"] + def test_doctest_issue4197(self): # To avoid having to keep two copies of the doctest module's # unit tests in sync, this test works by taking the source of