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]

Reply via email to