Skip to content

Commit e0c578f

Browse files
serhiy-storchakamiss-islington
authored andcommitted
gh-123011: Fix warn_explicit() with the globals of the __main__ module (GH-155318)
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 f2eaf17) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent a0d023f commit e0c578f

4 files changed

Lines changed: 89 additions & 3 deletions

File tree

Lib/importlib/_bootstrap_external.py

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

649+
# The __main__ module of a script or the REPL has __spec__ set to None.
650+
if spec is None and module_globals.get('__name__') == '__main__':
651+
return loader
652+
649653
if loader is None:
650654
if spec is missing:
651655
# If working with a module:

Lib/test/test_warnings/__init__.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1646,6 +1646,59 @@ def test_issue_8766(self):
16461646
assert_python_ok('-c', 'pass', '-W', 'always', PYTHONPATH=cwd)
16471647

16481648

1649+
class WarnExplicitMainTests(BaseTest):
1650+
# gh-123011: warn_explicit() with module globals of the __main__ module,
1651+
# no matter how it is executed.
1652+
code = ('import warnings\n'
1653+
'warnings.warn_explicit("eggs", UserWarning, "bar", 1,\n'
1654+
' module_globals=globals())\n')
1655+
1656+
def prepare_code(self):
1657+
"""Make the subprocess use the tested implementation."""
1658+
if self.module is py_warnings:
1659+
return ("import sys\n"
1660+
"sys.modules['_warnings'] = None\n") + self.code
1661+
return self.code
1662+
1663+
def check(self, err):
1664+
lines = err.decode().splitlines()
1665+
# Only the Python implementation adds the source line.
1666+
if len(lines) > 1 and lines[1].startswith(' '):
1667+
del lines[1]
1668+
self.assertEqual(lines, ['bar:1: UserWarning: eggs'])
1669+
1670+
def make_script(self, dirname):
1671+
filename = os.path.join(dirname, 'spam.py')
1672+
with open(filename, 'w', encoding='utf-8') as f:
1673+
f.write(self.prepare_code())
1674+
return filename
1675+
1676+
def test_script(self):
1677+
# __main__ has __spec__ set to None.
1678+
with os_helper.temp_dir() as dirname:
1679+
filename = self.make_script(dirname)
1680+
rc, out, err = assert_python_ok(filename)
1681+
self.check(err)
1682+
1683+
def test_module(self):
1684+
# __main__ has __spec__ of the module executed with -m.
1685+
with os_helper.temp_dir() as dirname:
1686+
self.make_script(dirname)
1687+
rc, out, err = assert_python_ok('-m', 'spam', PYTHONPATH=dirname)
1688+
self.check(err)
1689+
1690+
def test_command(self):
1691+
# __main__ has the built-in importer as a loader.
1692+
rc, out, err = assert_python_ok('-c', self.prepare_code())
1693+
self.check(err)
1694+
1695+
class CWarnExplicitMainTests(WarnExplicitMainTests, unittest.TestCase):
1696+
module = c_warnings
1697+
1698+
class PyWarnExplicitMainTests(WarnExplicitMainTests, unittest.TestCase):
1699+
module = py_warnings
1700+
1701+
16491702
class FinalizationTest(unittest.TestCase):
16501703
def test_finalization(self):
16511704
# 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
@@ -1237,12 +1237,33 @@ get_source_line(PyInterpreterState *interp, PyObject *module_globals, int lineno
12371237
return NULL;
12381238
}
12391239

1240-
int rc = PyDict_GetItemRef(module_globals, &_Py_ID(__name__),
1241-
&module_name);
1242-
if (rc < 0 || rc == 0) {
1240+
/* Prefer __spec__.name: __name__ is "__main__" for the module executed
1241+
as a script, but the loader can only handle its own module name. */
1242+
PyObject *spec;
1243+
if (PyDict_GetItemRef(module_globals, &_Py_ID(__spec__), &spec) < 0) {
12431244
Py_DECREF(loader);
12441245
return NULL;
12451246
}
1247+
module_name = NULL;
1248+
if (spec != NULL) {
1249+
int rc = PyObject_GetOptionalAttr(spec, &_Py_ID(name), &module_name);
1250+
Py_DECREF(spec);
1251+
if (rc < 0) {
1252+
Py_DECREF(loader);
1253+
return NULL;
1254+
}
1255+
if (module_name == Py_None) {
1256+
Py_CLEAR(module_name);
1257+
}
1258+
}
1259+
if (module_name == NULL) {
1260+
int rc = PyDict_GetItemRef(module_globals, &_Py_ID(__name__),
1261+
&module_name);
1262+
if (rc <= 0) { // not found or error
1263+
Py_DECREF(loader);
1264+
return NULL;
1265+
}
1266+
}
12461267

12471268
/* Make sure the loader implements the optional get_source() method. */
12481269
(void)PyObject_GetOptionalAttr(loader, &_Py_ID(get_source), &get_source);
@@ -1256,6 +1277,11 @@ get_source_line(PyInterpreterState *interp, PyObject *module_globals, int lineno
12561277
Py_DECREF(get_source);
12571278
Py_DECREF(module_name);
12581279
if (!source) {
1280+
/* The source line is optional: the loader can be unable to provide
1281+
the source of the module, for example if it is not its loader. */
1282+
if (PyErr_ExceptionMatches(PyExc_ImportError)) {
1283+
PyErr_Clear();
1284+
}
12591285
return NULL;
12601286
}
12611287
if (source == Py_None) {

0 commit comments

Comments
 (0)