Skip to content

Commit 9b0700d

Browse files
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).
1 parent cfcbfe4 commit 9b0700d

4 files changed

Lines changed: 72 additions & 3 deletions

File tree

Lib/importlib/_bootstrap_external.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,10 @@ def _bless_my_loader(module_globals):
634634
loader = module_globals.get('__loader__', None)
635635
spec = module_globals.get('__spec__', missing)
636636

637+
# The __main__ module of a script or the REPL has __spec__ set to None.
638+
if spec is None and module_globals.get('__name__') == '__main__':
639+
return loader
640+
637641
if loader is None:
638642
if spec is missing:
639643
# If working with a module:

Lib/test/test_warnings/__init__.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1675,6 +1675,42 @@ def test_issue_8766(self):
16751675
assert_python_ok('-c', 'pass', '-W', 'always', PYTHONPATH=cwd)
16761676

16771677

1678+
class WarnExplicitMainTests(unittest.TestCase):
1679+
# gh-123011: warn_explicit() with module globals of the __main__ module,
1680+
# no matter how it is executed.
1681+
code = ('import warnings\n'
1682+
'warnings.warn_explicit("eggs", UserWarning, "bar", 1,\n'
1683+
' module_globals=globals())\n')
1684+
1685+
def check(self, err):
1686+
self.assertEqual(err.decode().rstrip(), 'bar:1: UserWarning: eggs')
1687+
1688+
def make_script(self, dirname):
1689+
filename = os.path.join(dirname, 'spam.py')
1690+
with open(filename, 'w', encoding='utf-8') as f:
1691+
f.write(self.code)
1692+
return filename
1693+
1694+
def test_script(self):
1695+
# __main__ has __spec__ set to None.
1696+
with os_helper.temp_dir() as dirname:
1697+
filename = self.make_script(dirname)
1698+
rc, out, err = assert_python_ok(filename)
1699+
self.check(err)
1700+
1701+
def test_module(self):
1702+
# __main__ has __spec__ of the module executed with -m.
1703+
with os_helper.temp_dir() as dirname:
1704+
self.make_script(dirname)
1705+
rc, out, err = assert_python_ok('-m', 'spam', PYTHONPATH=dirname)
1706+
self.check(err)
1707+
1708+
def test_command(self):
1709+
# __main__ has the built-in importer as a loader.
1710+
rc, out, err = assert_python_ok('-c', self.code)
1711+
self.check(err)
1712+
1713+
16781714
class FinalizationTest(unittest.TestCase):
16791715
def test_finalization(self):
16801716
# Issue #19421: warnings.warn() should not crash
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`warnings.warn_explicit` no longer emits a spurious
2+
:exc:`DeprecationWarning` or raises :exc:`ImportError` when it is called with
3+
the globals of the :mod:`__main__` module.

Python/_warnings.c

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,12 +1200,33 @@ get_source_line(PyInterpreterState *interp, PyObject *module_globals, int lineno
12001200
return NULL;
12011201
}
12021202

1203-
int rc = PyDict_GetItemRef(module_globals, &_Py_ID(__name__),
1204-
&module_name);
1205-
if (rc < 0 || rc == 0) {
1203+
/* Prefer __spec__.name: __name__ is "__main__" for the module executed
1204+
as a script, but the loader can only handle its own module name. */
1205+
PyObject *spec;
1206+
if (PyDict_GetItemRef(module_globals, &_Py_ID(__spec__), &spec) < 0) {
12061207
Py_DECREF(loader);
12071208
return NULL;
12081209
}
1210+
module_name = NULL;
1211+
if (spec != NULL) {
1212+
int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(name), &module_name);
1213+
Py_DECREF(spec);
1214+
if (rc < 0) {
1215+
Py_DECREF(loader);
1216+
return NULL;
1217+
}
1218+
if (module_name == Py_None) {
1219+
Py_CLEAR(module_name);
1220+
}
1221+
}
1222+
if (module_name == NULL) {
1223+
int rc = PyDict_GetItemRef(module_globals, &_Py_ID(__name__),
1224+
&module_name);
1225+
if (rc <= 0) { // not found or error
1226+
Py_DECREF(loader);
1227+
return NULL;
1228+
}
1229+
}
12091230

12101231
/* Make sure the loader implements the optional get_source() method. */
12111232
(void)PyObject_GetOptionalAttr(loader, &_Py_ID(get_source), &get_source);
@@ -1219,6 +1240,11 @@ get_source_line(PyInterpreterState *interp, PyObject *module_globals, int lineno
12191240
Py_DECREF(get_source);
12201241
Py_DECREF(module_name);
12211242
if (!source) {
1243+
/* The source line is optional: the loader can be unable to provide
1244+
the source of the module, for example if it is not its loader. */
1245+
if (PyErr_ExceptionMatches(PyExc_ImportError)) {
1246+
PyErr_Clear();
1247+
}
12221248
return NULL;
12231249
}
12241250
if (source == Py_None) {

0 commit comments

Comments
 (0)