From 909b8280ee97af712701645ba72d9a02a713fdaa Mon Sep 17 00:00:00 2001 From: Mike Decker Date: Fri, 14 Aug 2026 12:23:00 -0400 Subject: [PATCH 1/4] gh-92041: Resolve frame modules from globals --- Lib/inspect.py | 8 ++++++++ Lib/test/test_inspect/test_inspect.py | 16 ++++++++++++++++ .../2026-08-14-16-30-00.gh-issue-92041.rst | 1 + 3 files changed, 25 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.rst diff --git a/Lib/inspect.py b/Lib/inspect.py index 3f8991c79652d3f..ec507c468a0daf7 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -930,6 +930,14 @@ def getmodule(object, _filename=None): """Return the module an object was defined in, or None if not found.""" if ismodule(object): return object + if istraceback(object): + object = object.tb_frame + if isframe(object): + object_globals = object.f_globals + module = sys.modules.get(object_globals.get('__name__')) + if module is not None and module.__dict__ is object_globals: + return module + return None if hasattr(object, '__module__'): return sys.modules.get(object.__module__) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index ff7475447e95a03..7553a1c16278a1b 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -831,6 +831,22 @@ def test_getmodule(self): self.assertEqual(inspect.getmodule(str), sys.modules["builtins"]) # Check filename override self.assertEqual(inspect.getmodule(None, modfile), mod) + # Check frame and traceback objects + self.assertIs(inspect.getmodule(inspect.currentframe()), + sys.modules[__name__]) + try: + 1 / 0 + except ZeroDivisionError as error: + self.assertIs(inspect.getmodule(error.__traceback__), + sys.modules[__name__]) + + def test_getmodule_unregistered_exec_frame(self): + namespace = {"inspect": inspect, "__name__": "not_registered"} + exec(compile("frame = inspect.currentframe()", modfile, "exec"), + namespace) + # The frame globals are authoritative, even though the code filename + # happens to match an imported module. + self.assertIsNone(inspect.getmodule(namespace["frame"])) def test_getmodule_file_not_found(self): # See bpo-45406 diff --git a/Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.rst b/Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.rst new file mode 100644 index 000000000000000..4ec3839e147eb2a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.rst @@ -0,0 +1 @@ +Improve :func:`inspect.getmodule` performance for frame and traceback objects by resolving their module directly from the frame globals. From 2bcf546f0557ef0fb33b2f1bdfed4a038fb61695 Mon Sep 17 00:00:00 2001 From: Mike Decker Date: Fri, 14 Aug 2026 12:23:46 -0400 Subject: [PATCH 2/4] Fix NEWS entry filename --- ...ue-92041.rst => 2026-08-14-16-30-00.gh-issue-92041.kD7pQm.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Misc/NEWS.d/next/Library/{2026-08-14-16-30-00.gh-issue-92041.rst => 2026-08-14-16-30-00.gh-issue-92041.kD7pQm.rst} (100%) diff --git a/Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.rst b/Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.kD7pQm.rst similarity index 100% rename from Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.rst rename to Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.kD7pQm.rst From 58703a5c2859db32ba2d6397a6c8c20a5ab08534 Mon Sep 17 00:00:00 2001 From: Mike Decker Date: Fri, 14 Aug 2026 15:57:36 -0400 Subject: [PATCH 3/4] gh-92041: Address getmodule review findings --- Lib/inspect.py | 20 ++++++---- Lib/test/test_inspect/test_inspect.py | 37 +++++++++++++++---- ...6-08-14-16-30-00.gh-issue-92041.kD7pQm.rst | 2 +- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index ec507c468a0daf7..b49b5e323871fe3 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -930,16 +930,22 @@ def getmodule(object, _filename=None): """Return the module an object was defined in, or None if not found.""" if ismodule(object): return object + if hasattr(object, '__module__'): + return sys.modules.get(object.__module__) if istraceback(object): object = object.tb_frame if isframe(object): - object_globals = object.f_globals - module = sys.modules.get(object_globals.get('__name__')) - if module is not None and module.__dict__ is object_globals: - return module - return None - if hasattr(object, '__module__'): - return sys.modules.get(object.__module__) + # Frame globals identify the execution namespace directly. Preserve + # the private filename override when it names a different file. + if _filename is None or _filename == object.f_code.co_filename: + object_globals = object.f_globals + module_name = object_globals.get('__name__') + if not isinstance(module_name, str): + return None + module = sys.modules.get(module_name) + if ismodule(module) and module.__dict__ is object_globals: + return module + return None # Try the filename to modulename cache if _filename is not None and _filename in modulesbyfile: diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 7553a1c16278a1b..4339df8e560978b 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -831,7 +831,10 @@ def test_getmodule(self): self.assertEqual(inspect.getmodule(str), sys.modules["builtins"]) # Check filename override self.assertEqual(inspect.getmodule(None, modfile), mod) - # Check frame and traceback objects + self.assertEqual( + inspect.getmodule(inspect.currentframe(), modfile), mod) + # Check code, frame, and traceback objects + self.assertIs(inspect.getmodule(mod.eggs.__code__), mod) self.assertIs(inspect.getmodule(inspect.currentframe()), sys.modules[__name__]) try: @@ -841,12 +844,32 @@ def test_getmodule(self): sys.modules[__name__]) def test_getmodule_unregistered_exec_frame(self): - namespace = {"inspect": inspect, "__name__": "not_registered"} - exec(compile("frame = inspect.currentframe()", modfile, "exec"), - namespace) - # The frame globals are authoritative, even though the code filename - # happens to match an imported module. - self.assertIsNone(inspect.getmodule(namespace["frame"])) + def exec_namespace(namespace): + exec(compile(textwrap.dedent(""" + frame = inspect.currentframe() + try: + 1 / 0 + except ZeroDivisionError as error: + traceback = error.__traceback__ + """), modfile, "exec"), namespace) + self.assertIsNone(inspect.getmodule(namespace["frame"])) + self.assertIsNone(inspect.getmodule(namespace["traceback"])) + + # Missing and invalid module names identify no registered namespace. + exec_namespace({"inspect": inspect}) + exec_namespace({"inspect": inspect, "__name__": []}) + + module_name = f"{__name__}.not_registered" + for module in (None, object(), types.ModuleType(module_name)): + with self.subTest(module=module): + sys.modules[module_name] = module + try: + exec_namespace({ + "inspect": inspect, + "__name__": module_name, + }) + finally: + del sys.modules[module_name] def test_getmodule_file_not_found(self): # See bpo-45406 diff --git a/Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.kD7pQm.rst b/Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.kD7pQm.rst index 4ec3839e147eb2a..e1cd3a75411ef5a 100644 --- a/Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.kD7pQm.rst +++ b/Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.kD7pQm.rst @@ -1 +1 @@ -Improve :func:`inspect.getmodule` performance for frame and traceback objects by resolving their module directly from the frame globals. +Improve :func:`inspect.getmodule` performance for frame and traceback objects by resolving registered modules directly from frame globals. Frames executing in unregistered globals now return ``None`` instead of being associated with a module solely by filename. From 3fbfadc521f0d8611c542ee2f20b4b94f4657e87 Mon Sep 17 00:00:00 2001 From: Mike Decker Date: Fri, 14 Aug 2026 17:34:21 -0400 Subject: [PATCH 4/4] gh-92041: Preserve frame filename semantics --- Lib/inspect.py | 17 +++++++++++- Lib/test/test_inspect/test_inspect.py | 40 +++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/Lib/inspect.py b/Lib/inspect.py index b49b5e323871fe3..f4383e659f3b5f5 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -943,7 +943,22 @@ def getmodule(object, _filename=None): if not isinstance(module_name, str): return None module = sys.modules.get(module_name) - if ismodule(module) and module.__dict__ is object_globals: + if not (ismodule(module) and module.__dict__ is object_globals): + return None + module_file = getattr(module, '__file__', None) + if module_file is None: + return None + try: + file = getabsfile(object, _filename) + except (TypeError, FileNotFoundError): + return None + if object.f_code.co_filename == module_file: + return module + try: + module_file = getabsfile(module) + except (TypeError, FileNotFoundError): + return None + if file == module_file or file == os.path.realpath(module_file): return module return None diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 4339df8e560978b..b7a357b077ce92f 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -871,6 +871,46 @@ def exec_namespace(namespace): finally: del sys.modules[module_name] + def test_getmodule_registered_exec_frame(self): + def exec_module(module, filename): + module.inspect = inspect + exec(compile(textwrap.dedent(""" + frame = inspect.currentframe() + try: + 1 / 0 + except ZeroDivisionError as error: + traceback = error.__traceback__ + """), filename, "exec"), module.__dict__) + + module_name = f"{__name__}.registered" + module = types.ModuleType(module_name) + sys.modules[module_name] = module + try: + with temp_cwd() as cwd: + filename = os.path.join(cwd, "registered.py") + with open(filename, "w"): + pass + module.__file__ = filename + + exec_module(module, filename) + self.assertIs(inspect.getmodule(module.frame), module) + self.assertIs(inspect.getmodule(module.traceback), module) + + # Globals identity is insufficient when the code came from a + # different origin than the registered module. + exec_module(module, filename + ".other") + self.assertIsNone(inspect.getmodule(module.frame)) + self.assertIsNone(inspect.getmodule(module.traceback)) + + # Preserve the existing result for fileless modules while + # avoiding a scan of sys.modules. + del module.__file__ + exec_module(module, "") + self.assertIsNone(inspect.getmodule(module.frame)) + self.assertIsNone(inspect.getmodule(module.traceback)) + finally: + del sys.modules[module_name] + def test_getmodule_file_not_found(self): # See bpo-45406 def _getabsfile(obj, _filename):