From 9b0700ddd4a675ec5eda1ebe22b2b9fe367a60d7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 7 Aug 2026 11:55:13 +0300 Subject: [PATCH 1/2] gh-123011: Fix warn_explicit() with the globals of the __main__ module The __main__ module executed as a script or a command has __spec__ set to None, so warn_explicit(module_globals=globals()) emitted a spurious DeprecationWarning. It also raised ImportError when the loader was unable to provide the source of the module: when the module was executed with -m (the loader can only handle its own module name) or as a command (the built-in importer has no source). --- Lib/importlib/_bootstrap_external.py | 4 +++ Lib/test/test_warnings/__init__.py | 36 +++++++++++++++++++ ...08-07-01-10-00.gh-issue-123011.warnexp.rst | 3 ++ Python/_warnings.c | 32 +++++++++++++++-- 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-07-01-10-00.gh-issue-123011.warnexp.rst diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index a1cb729efb7fef2..176652230c092a8 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -634,6 +634,10 @@ def _bless_my_loader(module_globals): loader = module_globals.get('__loader__', None) spec = module_globals.get('__spec__', missing) + # The __main__ module of a script or the REPL has __spec__ set to None. + if spec is None and module_globals.get('__name__') == '__main__': + return loader + if loader is None: if spec is missing: # If working with a module: diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index bf1bcf8e6ed5d9a..a7f5f8dfa131ae1 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1675,6 +1675,42 @@ def test_issue_8766(self): assert_python_ok('-c', 'pass', '-W', 'always', PYTHONPATH=cwd) +class WarnExplicitMainTests(unittest.TestCase): + # gh-123011: warn_explicit() with module globals of the __main__ module, + # no matter how it is executed. + code = ('import warnings\n' + 'warnings.warn_explicit("eggs", UserWarning, "bar", 1,\n' + ' module_globals=globals())\n') + + def check(self, err): + self.assertEqual(err.decode().rstrip(), 'bar:1: UserWarning: eggs') + + def make_script(self, dirname): + filename = os.path.join(dirname, 'spam.py') + with open(filename, 'w', encoding='utf-8') as f: + f.write(self.code) + return filename + + def test_script(self): + # __main__ has __spec__ set to None. + with os_helper.temp_dir() as dirname: + filename = self.make_script(dirname) + rc, out, err = assert_python_ok(filename) + self.check(err) + + def test_module(self): + # __main__ has __spec__ of the module executed with -m. + with os_helper.temp_dir() as dirname: + self.make_script(dirname) + rc, out, err = assert_python_ok('-m', 'spam', PYTHONPATH=dirname) + self.check(err) + + def test_command(self): + # __main__ has the built-in importer as a loader. + rc, out, err = assert_python_ok('-c', self.code) + self.check(err) + + class FinalizationTest(unittest.TestCase): def test_finalization(self): # Issue #19421: warnings.warn() should not crash diff --git a/Misc/NEWS.d/next/Library/2026-08-07-01-10-00.gh-issue-123011.warnexp.rst b/Misc/NEWS.d/next/Library/2026-08-07-01-10-00.gh-issue-123011.warnexp.rst new file mode 100644 index 000000000000000..6c990b2eb938452 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-07-01-10-00.gh-issue-123011.warnexp.rst @@ -0,0 +1,3 @@ +:func:`warnings.warn_explicit` no longer emits a spurious +:exc:`DeprecationWarning` or raises :exc:`ImportError` when it is called with +the globals of the :mod:`__main__` module. diff --git a/Python/_warnings.c b/Python/_warnings.c index 4f6de50efa14a8e..11f9b305978f3e8 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -1200,12 +1200,33 @@ get_source_line(PyInterpreterState *interp, PyObject *module_globals, int lineno return NULL; } - int rc = PyDict_GetItemRef(module_globals, &_Py_ID(__name__), - &module_name); - if (rc < 0 || rc == 0) { + /* Prefer __spec__.name: __name__ is "__main__" for the module executed + as a script, but the loader can only handle its own module name. */ + PyObject *spec; + if (PyDict_GetItemRef(module_globals, &_Py_ID(__spec__), &spec) < 0) { Py_DECREF(loader); return NULL; } + module_name = NULL; + if (spec != NULL) { + int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(name), &module_name); + Py_DECREF(spec); + if (rc < 0) { + Py_DECREF(loader); + return NULL; + } + if (module_name == Py_None) { + Py_CLEAR(module_name); + } + } + if (module_name == NULL) { + int rc = PyDict_GetItemRef(module_globals, &_Py_ID(__name__), + &module_name); + if (rc <= 0) { // not found or error + Py_DECREF(loader); + return NULL; + } + } /* Make sure the loader implements the optional get_source() method. */ (void)PyObject_GetOptionalAttr(loader, &_Py_ID(get_source), &get_source); @@ -1219,6 +1240,11 @@ get_source_line(PyInterpreterState *interp, PyObject *module_globals, int lineno Py_DECREF(get_source); Py_DECREF(module_name); if (!source) { + /* The source line is optional: the loader can be unable to provide + the source of the module, for example if it is not its loader. */ + if (PyErr_ExceptionMatches(PyExc_ImportError)) { + PyErr_Clear(); + } return NULL; } if (source == Py_None) { From 5644f84b6d31e85a3d175b5b1c63b9e0b976e4a0 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 7 Aug 2026 12:12:05 +0300 Subject: [PATCH 2/2] Run the new tests for both implementations --- Lib/test/test_warnings/__init__.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py index a7f5f8dfa131ae1..75affc682fb5f31 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1675,20 +1675,31 @@ def test_issue_8766(self): assert_python_ok('-c', 'pass', '-W', 'always', PYTHONPATH=cwd) -class WarnExplicitMainTests(unittest.TestCase): +class WarnExplicitMainTests(BaseTest): # gh-123011: warn_explicit() with module globals of the __main__ module, # no matter how it is executed. code = ('import warnings\n' 'warnings.warn_explicit("eggs", UserWarning, "bar", 1,\n' ' module_globals=globals())\n') + def prepare_code(self): + """Make the subprocess use the tested implementation.""" + if self.module is py_warnings: + return ("import sys\n" + "sys.modules['_warnings'] = None\n") + self.code + return self.code + def check(self, err): - self.assertEqual(err.decode().rstrip(), 'bar:1: UserWarning: eggs') + lines = err.decode().splitlines() + # Only the Python implementation adds the source line. + if len(lines) > 1 and lines[1].startswith(' '): + del lines[1] + self.assertEqual(lines, ['bar:1: UserWarning: eggs']) def make_script(self, dirname): filename = os.path.join(dirname, 'spam.py') with open(filename, 'w', encoding='utf-8') as f: - f.write(self.code) + f.write(self.prepare_code()) return filename def test_script(self): @@ -1707,9 +1718,15 @@ def test_module(self): def test_command(self): # __main__ has the built-in importer as a loader. - rc, out, err = assert_python_ok('-c', self.code) + rc, out, err = assert_python_ok('-c', self.prepare_code()) self.check(err) +class CWarnExplicitMainTests(WarnExplicitMainTests, unittest.TestCase): + module = c_warnings + +class PyWarnExplicitMainTests(WarnExplicitMainTests, unittest.TestCase): + module = py_warnings + class FinalizationTest(unittest.TestCase): def test_finalization(self):