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..75affc682fb5f31 100644 --- a/Lib/test/test_warnings/__init__.py +++ b/Lib/test/test_warnings/__init__.py @@ -1675,6 +1675,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 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) {