diff --git a/Lib/inspect.py b/Lib/inspect.py index 3f8991c79652d3..f4383e659f3b5f 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -932,6 +932,35 @@ def getmodule(object, _filename=None): return object if hasattr(object, '__module__'): return sys.modules.get(object.__module__) + if istraceback(object): + object = object.tb_frame + if isframe(object): + # 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 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 # 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 ff7475447e95a0..b7a357b077ce92 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -831,6 +831,85 @@ def test_getmodule(self): self.assertEqual(inspect.getmodule(str), sys.modules["builtins"]) # Check filename override self.assertEqual(inspect.getmodule(None, modfile), mod) + 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: + 1 / 0 + except ZeroDivisionError as error: + self.assertIs(inspect.getmodule(error.__traceback__), + sys.modules[__name__]) + + def test_getmodule_unregistered_exec_frame(self): + 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_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 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 new file mode 100644 index 00000000000000..e1cd3a75411ef5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-14-16-30-00.gh-issue-92041.kD7pQm.rst @@ -0,0 +1 @@ +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.