[3.13] gh-123011: Fix warn_explicit() with the globals of the __main__ module (GH-155318) (GH-155991)

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/01189bdaf4e20bdf585db3f1a97739c7bfe4789b
commit: 01189bdaf4e20bdf585db3f1a97739c7bfe4789b
branch: 3.13
author: Miss Islington (bot) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-18T07:50:34Z
summary:

[3.13] gh-123011: Fix warn_explicit() with the globals of the __main__ module (GH-155318) (GH-155991)

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).
(cherry picked from commit f2eaf174b2505be1b3dfbe817db243adf7fd5d6b)

Co-authored-by: Serhiy Storchaka <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-08-07-01-10-00.gh-issue-123011.warnexp.rst
M Lib/importlib/_bootstrap_external.py
M Lib/test/test_warnings/__init__.py
M Python/_warnings.c

diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py
index 0741f62ee839f46..fcfe312747c0354 100644
--- a/Lib/importlib/_bootstrap_external.py
+++ b/Lib/importlib/_bootstrap_external.py
@@ -916,6 +916,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 7fbb1c99823770c..70bbfc1c14937ad 100644
--- a/Lib/test/test_warnings/__init__.py
+++ b/Lib/test/test_warnings/__init__.py
@@ -1581,6 +1581,59 @@ def test_issue_8766(self):
             assert_python_ok('-c', 'pass', '-W', 'always', PYTHONPATH=cwd)
 
 
+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):
+        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.prepare_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.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):
         # 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 fcd2c774d0a8f8f..395eae190edfe2c 100644
--- a/Python/_warnings.c
+++ b/Python/_warnings.c
@@ -1068,12 +1068,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);
@@ -1087,6 +1108,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) {

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.