gh-84687: Add filename to the error raised by os.exec* (GH-19915)
serhiy-storchaka <[email protected]> Tue, 11 Aug 2026 09:08:30 -0400 (EDT)
| Newsgroups | gmane.comp.python.cvs |
|---|---|
| Message-ID | <[email protected]> |
https://github.com/python/cpython/commit/fcfa919d9c1b3a65c78e99e8ed6615dd4d583404 commit: fcfa919d9c1b3a65c78e99e8ed6615dd4d583404 branch: main author: Russell Davis <[email protected]> committer: serhiy-storchaka <[email protected]> date: 2026-08-11T16:08:15+03:00 summary: gh-84687: Add filename to the error raised by os.exec* (GH-19915) Co-authored-by: Serhiy Storchaka <[email protected]> files: A Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst M Lib/os.py M Lib/test/test_os/test_os.py M Modules/posixmodule.c diff --git a/Lib/os.py b/Lib/os.py index a5e1d805556998..87547e369db817 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -643,11 +643,13 @@ def _execvpe(file, args, env=None): argrest = (args,) env = environ + file = fspath(file) if path.dirname(file): exec_func(file, *argrest) return saved_exc = None path_list = get_exec_path(env) + orig_file = file if name != 'nt': file = fsencode(file) path_list = map(fsencode, path_list) @@ -663,6 +665,11 @@ def _execvpe(file, args, env=None): saved_exc = e if saved_exc is not None: raise saved_exc + # At this point, last_exc.filename contains the full path of whatever + # directory happened to be last in path_list. Set it to the filename that + # was passed in, which is what the caller will expect. This is what + # subprocess does too (see err_filename in Popen._execute_child()). + last_exc.filename = orig_file raise last_exc diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index bcf83a314f1a6e..4c1ab96065587e 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -2636,12 +2636,50 @@ def mock_execve(name, *args): @unittest.skipUnless(hasattr(os, 'execv'), "need os.execv()") [email protected](support.is_emscripten, + "Emscripten always fails with ENOEXEC") [email protected](support.is_android, + "PATH contains an inaccessible directory on Android") class ExecTests(unittest.TestCase): - @unittest.skipIf(USING_LINUXTHREADS, - "avoid triggering a linuxthreads bug: see issue #4970") + def _test_bad_program(self, do_exec, exc_type=OSError): + bad_filenames = ['nosuchapp', FakePath('nosuchapp')] + if os.name != 'nt': + # Bytes program names are not supported on Windows. + bad_filenames += [b'nosuchapp', FakePath(b'nosuchapp')] + for bad_filename in bad_filenames: + with self.subTest(bad_filename): + with self.assertRaises(exc_type) as ctx: + do_exec(bad_filename) + self.assertEqual(ctx.exception.filename, + os.fspath(bad_filename)) + self.assertIn('nosuchapp', str(ctx.exception)) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execv_with_bad_program(self): + self._test_bad_program(lambda name: os.execv(name, ['nosuchapp'])) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execvp_with_bad_program(self): + self._test_bad_program(lambda name: os.execvp(name, ['nosuchapp'])) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execve_with_bad_program(self): + self._test_bad_program(lambda name: os.execve(name, ['nosuchapp'], {})) + + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") def test_execvpe_with_bad_program(self): - self.assertRaises(OSError, os.execvpe, 'no such app-', - ['no such app-'], None) + self._test_bad_program(lambda name: os.execvpe(name, ['nosuchapp'], {})) + + @unittest.skipUnless(os.name == 'posix', 'POSIX specific test') + @unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970") + def test_execvp_with_bad_path_entry(self): + # A regular file in PATH makes the exec fail with ENOTDIR. + create_file(os_helper.TESTFN) + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + with os_helper.EnvironmentVarGuard() as env: + env['PATH'] = os.path.abspath(os_helper.TESTFN) + self._test_bad_program(lambda name: os.execvp(name, ['nosuchapp']), + NotADirectoryError) def test_execv_with_bad_arglist(self): self.assertRaises(ValueError, os.execv, 'notepad', ()) diff --git a/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst b/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst new file mode 100644 index 00000000000000..b6dd108136f6dc --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-05-06-05-24.gh-issue-84687.ggjoGl.rst @@ -0,0 +1,3 @@ +The :func:`os.exec\* <os.execl>` functions now set the +:attr:`~OSError.filename` attribute of the raised :exc:`FileNotFoundError` +or :exc:`NotADirectoryError` to the program name passed by the caller. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index db65d586244065..a9dd8647545bc9 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -7520,7 +7520,7 @@ os_execv_impl(PyObject *module, path_t *path, PyObject *argv) /* If we get here it's definitely an error */ - posix_error(); + posix_path_error(path); free_string_array(argvlist, argc); return NULL; } _______________________________________________ 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]