Skip to content

Commit fcfa919

Browse files
gh-84687: Add filename to the error raised by os.exec* (GH-19915)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent a7541c3 commit fcfa919

4 files changed

Lines changed: 53 additions & 5 deletions

File tree

Lib/os.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,11 +643,13 @@ def _execvpe(file, args, env=None):
643643
argrest = (args,)
644644
env = environ
645645

646+
file = fspath(file)
646647
if path.dirname(file):
647648
exec_func(file, *argrest)
648649
return
649650
saved_exc = None
650651
path_list = get_exec_path(env)
652+
orig_file = file
651653
if name != 'nt':
652654
file = fsencode(file)
653655
path_list = map(fsencode, path_list)
@@ -663,6 +665,11 @@ def _execvpe(file, args, env=None):
663665
saved_exc = e
664666
if saved_exc is not None:
665667
raise saved_exc
668+
# At this point, last_exc.filename contains the full path of whatever
669+
# directory happened to be last in path_list. Set it to the filename that
670+
# was passed in, which is what the caller will expect. This is what
671+
# subprocess does too (see err_filename in Popen._execute_child()).
672+
last_exc.filename = orig_file
666673
raise last_exc
667674

668675

Lib/test/test_os/test_os.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2636,12 +2636,50 @@ def mock_execve(name, *args):
26362636

26372637
@unittest.skipUnless(hasattr(os, 'execv'),
26382638
"need os.execv()")
2639+
@unittest.skipIf(support.is_emscripten,
2640+
"Emscripten always fails with ENOEXEC")
2641+
@unittest.skipIf(support.is_android,
2642+
"PATH contains an inaccessible directory on Android")
26392643
class ExecTests(unittest.TestCase):
2640-
@unittest.skipIf(USING_LINUXTHREADS,
2641-
"avoid triggering a linuxthreads bug: see issue #4970")
2644+
def _test_bad_program(self, do_exec, exc_type=OSError):
2645+
bad_filenames = ['nosuchapp', FakePath('nosuchapp')]
2646+
if os.name != 'nt':
2647+
# Bytes program names are not supported on Windows.
2648+
bad_filenames += [b'nosuchapp', FakePath(b'nosuchapp')]
2649+
for bad_filename in bad_filenames:
2650+
with self.subTest(bad_filename):
2651+
with self.assertRaises(exc_type) as ctx:
2652+
do_exec(bad_filename)
2653+
self.assertEqual(ctx.exception.filename,
2654+
os.fspath(bad_filename))
2655+
self.assertIn('nosuchapp', str(ctx.exception))
2656+
2657+
@unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970")
2658+
def test_execv_with_bad_program(self):
2659+
self._test_bad_program(lambda name: os.execv(name, ['nosuchapp']))
2660+
2661+
@unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970")
2662+
def test_execvp_with_bad_program(self):
2663+
self._test_bad_program(lambda name: os.execvp(name, ['nosuchapp']))
2664+
2665+
@unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970")
2666+
def test_execve_with_bad_program(self):
2667+
self._test_bad_program(lambda name: os.execve(name, ['nosuchapp'], {}))
2668+
2669+
@unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970")
26422670
def test_execvpe_with_bad_program(self):
2643-
self.assertRaises(OSError, os.execvpe, 'no such app-',
2644-
['no such app-'], None)
2671+
self._test_bad_program(lambda name: os.execvpe(name, ['nosuchapp'], {}))
2672+
2673+
@unittest.skipUnless(os.name == 'posix', 'POSIX specific test')
2674+
@unittest.skipIf(USING_LINUXTHREADS, "linuxthreads bug: see issue #4970")
2675+
def test_execvp_with_bad_path_entry(self):
2676+
# A regular file in PATH makes the exec fail with ENOTDIR.
2677+
create_file(os_helper.TESTFN)
2678+
self.addCleanup(os_helper.unlink, os_helper.TESTFN)
2679+
with os_helper.EnvironmentVarGuard() as env:
2680+
env['PATH'] = os.path.abspath(os_helper.TESTFN)
2681+
self._test_bad_program(lambda name: os.execvp(name, ['nosuchapp']),
2682+
NotADirectoryError)
26452683

26462684
def test_execv_with_bad_arglist(self):
26472685
self.assertRaises(ValueError, os.execv, 'notepad', ())
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The :func:`os.exec\* <os.execl>` functions now set the
2+
:attr:`~OSError.filename` attribute of the raised :exc:`FileNotFoundError`
3+
or :exc:`NotADirectoryError` to the program name passed by the caller.

Modules/posixmodule.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7520,7 +7520,7 @@ os_execv_impl(PyObject *module, path_t *path, PyObject *argv)
75207520

75217521
/* If we get here it's definitely an error */
75227522

7523-
posix_error();
7523+
posix_path_error(path);
75247524
free_string_array(argvlist, argc);
75257525
return NULL;
75267526
}

0 commit comments

Comments
 (0)