Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9e3c452

Browse files
bpo-31961: Fix support of path-like executables in subprocess. (GH-5914)
1 parent 1b05aa2 commit 9e3c452

File tree

4 files changed

+109
-6
lines changed

4 files changed

+109
-6
lines changed

Doc/library/subprocess.rst

+26-3
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ functions.
347347
the class uses the Windows ``CreateProcess()`` function. The arguments to
348348
:class:`Popen` are as follows.
349349

350-
*args* should be a sequence of program arguments or else a single string.
350+
*args* should be a sequence of program arguments or else a single string
351+
or :term:`path-like object`.
351352
By default, the program to execute is the first item in *args* if *args* is
352353
a sequence. If *args* is a string, the interpretation is
353354
platform-dependent and described below. See the *shell* and *executable*
@@ -381,6 +382,15 @@ functions.
381382
manner described in :ref:`converting-argument-sequence`. This is because
382383
the underlying ``CreateProcess()`` operates on strings.
383384

385+
.. versionchanged:: 3.6
386+
*args* parameter accepts a :term:`path-like object` if *shell* is
387+
``False`` and a sequence containing path-like objects on POSIX.
388+
389+
.. versionchanged:: 3.8
390+
*args* parameter accepts a :term:`path-like object` if *shell* is
391+
``False`` and a sequence containing bytes and path-like objects
392+
on Windows.
393+
384394
The *shell* argument (which defaults to ``False``) specifies whether to use
385395
the shell as the program to execute. If *shell* is ``True``, it is
386396
recommended to pass *args* as a string rather than as a sequence.
@@ -436,6 +446,13 @@ functions.
436446
:program:`ps`. If ``shell=True``, on POSIX the *executable* argument
437447
specifies a replacement shell for the default :file:`/bin/sh`.
438448

449+
.. versionchanged:: 3.6
450+
*executable* parameter accepts a :term:`path-like object` on POSIX.
451+
452+
.. versionchanged:: 3.8
453+
*executable* parameter accepts a bytes and :term:`path-like object`
454+
on Windows.
455+
439456
*stdin*, *stdout* and *stderr* specify the executed program's standard input,
440457
standard output and standard error file handles, respectively. Valid values
441458
are :data:`PIPE`, :data:`DEVNULL`, an existing file descriptor (a positive
@@ -492,13 +509,19 @@ functions.
492509
The *pass_fds* parameter was added.
493510

494511
If *cwd* is not ``None``, the function changes the working directory to
495-
*cwd* before executing the child. *cwd* can be a :class:`str` and
512+
*cwd* before executing the child. *cwd* can be a string, bytes or
496513
:term:`path-like <path-like object>` object. In particular, the function
497514
looks for *executable* (or for the first item in *args*) relative to *cwd*
498515
if the executable path is a relative path.
499516

500517
.. versionchanged:: 3.6
501-
*cwd* parameter accepts a :term:`path-like object`.
518+
*cwd* parameter accepts a :term:`path-like object` on POSIX.
519+
520+
.. versionchanged:: 3.7
521+
*cwd* parameter accepts a :term:`path-like object` on Windows.
522+
523+
.. versionchanged:: 3.8
524+
*cwd* parameter accepts a bytes object on Windows.
502525

503526
If *restore_signals* is true (the default) all signals that Python has set to
504527
SIG_IGN are restored to SIG_DFL in the child process before the exec.

Lib/subprocess.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,7 @@ def list2cmdline(seq):
521521
# "Parsing C++ Command-Line Arguments"
522522
result = []
523523
needquote = False
524-
for arg in seq:
524+
for arg in map(os.fsdecode, seq):
525525
bs_buf = []
526526

527527
# Add a space to separate this argument from the others
@@ -1203,9 +1203,23 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
12031203

12041204
assert not pass_fds, "pass_fds not supported on Windows."
12051205

1206-
if not isinstance(args, str):
1206+
if isinstance(args, str):
1207+
pass
1208+
elif isinstance(args, bytes):
1209+
if shell:
1210+
raise TypeError('bytes args is not allowed on Windows')
1211+
args = list2cmdline([args])
1212+
elif isinstance(args, os.PathLike):
1213+
if shell:
1214+
raise TypeError('path-like args is not allowed when '
1215+
'shell is true')
1216+
args = list2cmdline([args])
1217+
else:
12071218
args = list2cmdline(args)
12081219

1220+
if executable is not None:
1221+
executable = os.fsdecode(executable)
1222+
12091223
# Process startup details
12101224
if startupinfo is None:
12111225
startupinfo = STARTUPINFO()
@@ -1262,7 +1276,7 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
12621276
int(not close_fds),
12631277
creationflags,
12641278
env,
1265-
os.fspath(cwd) if cwd is not None else None,
1279+
os.fsdecode(cwd) if cwd is not None else None,
12661280
startupinfo)
12671281
finally:
12681282
# Child is launched. Close the parent's copy of those pipe
@@ -1510,6 +1524,11 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
15101524

15111525
if isinstance(args, (str, bytes)):
15121526
args = [args]
1527+
elif isinstance(args, os.PathLike):
1528+
if shell:
1529+
raise TypeError('path-like args is not allowed when '
1530+
'shell is true')
1531+
args = [args]
15131532
else:
15141533
args = list(args)
15151534

Lib/test/test_subprocess.py

+55
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,18 @@ def test_executable(self):
304304
"doesnotexist")
305305
self._assert_python([doesnotexist, "-c"], executable=sys.executable)
306306

307+
def test_bytes_executable(self):
308+
doesnotexist = os.path.join(os.path.dirname(sys.executable),
309+
"doesnotexist")
310+
self._assert_python([doesnotexist, "-c"],
311+
executable=os.fsencode(sys.executable))
312+
313+
def test_pathlike_executable(self):
314+
doesnotexist = os.path.join(os.path.dirname(sys.executable),
315+
"doesnotexist")
316+
self._assert_python([doesnotexist, "-c"],
317+
executable=FakePath(sys.executable))
318+
307319
def test_executable_takes_precedence(self):
308320
# Check that the executable argument takes precedence over args[0].
309321
#
@@ -320,6 +332,16 @@ def test_executable_replaces_shell(self):
320332
# when shell=True.
321333
self._assert_python([], executable=sys.executable, shell=True)
322334

335+
@unittest.skipIf(mswindows, "executable argument replaces shell")
336+
def test_bytes_executable_replaces_shell(self):
337+
self._assert_python([], executable=os.fsencode(sys.executable),
338+
shell=True)
339+
340+
@unittest.skipIf(mswindows, "executable argument replaces shell")
341+
def test_pathlike_executable_replaces_shell(self):
342+
self._assert_python([], executable=FakePath(sys.executable),
343+
shell=True)
344+
323345
# For use in the test_cwd* tests below.
324346
def _normalize_cwd(self, cwd):
325347
# Normalize an expected cwd (for Tru64 support).
@@ -358,6 +380,11 @@ def test_cwd(self):
358380
temp_dir = self._normalize_cwd(temp_dir)
359381
self._assert_cwd(temp_dir, sys.executable, cwd=temp_dir)
360382

383+
def test_cwd_with_bytes(self):
384+
temp_dir = tempfile.gettempdir()
385+
temp_dir = self._normalize_cwd(temp_dir)
386+
self._assert_cwd(temp_dir, sys.executable, cwd=os.fsencode(temp_dir))
387+
361388
def test_cwd_with_pathlike(self):
362389
temp_dir = tempfile.gettempdir()
363390
temp_dir = self._normalize_cwd(temp_dir)
@@ -1473,6 +1500,34 @@ def test_run_kwargs(self):
14731500
env=newenv)
14741501
self.assertEqual(cp.returncode, 33)
14751502

1503+
def test_run_with_pathlike_path(self):
1504+
# bpo-31961: test run(pathlike_object)
1505+
# the name of a command that can be run without
1506+
# any argumenets that exit fast
1507+
prog = 'tree.com' if mswindows else 'ls'
1508+
path = shutil.which(prog)
1509+
if path is None:
1510+
self.skipTest(f'{prog} required for this test')
1511+
path = FakePath(path)
1512+
res = subprocess.run(path, stdout=subprocess.DEVNULL)
1513+
self.assertEqual(res.returncode, 0)
1514+
with self.assertRaises(TypeError):
1515+
subprocess.run(path, stdout=subprocess.DEVNULL, shell=True)
1516+
1517+
def test_run_with_bytes_path_and_arguments(self):
1518+
# bpo-31961: test run([bytes_object, b'additional arguments'])
1519+
path = os.fsencode(sys.executable)
1520+
args = [path, '-c', b'import sys; sys.exit(57)']
1521+
res = subprocess.run(args)
1522+
self.assertEqual(res.returncode, 57)
1523+
1524+
def test_run_with_pathlike_path_and_arguments(self):
1525+
# bpo-31961: test run([pathlike_object, 'additional arguments'])
1526+
path = FakePath(sys.executable)
1527+
args = [path, '-c', 'import sys; sys.exit(57)']
1528+
res = subprocess.run(args)
1529+
self.assertEqual(res.returncode, 57)
1530+
14761531
def test_capture_output(self):
14771532
cp = self.run_python(("import sys;"
14781533
"sys.stdout.write('BDFL'); "
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Added support for bytes and path-like objects in :func:`subprocess.Popen`
2+
on Windows. The *args* parameter now accepts a :term:`path-like object` if
3+
*shell* is ``False`` and a sequence containing bytes and path-like objects.
4+
The *executable* parameter now accepts a bytes and :term:`path-like object`.
5+
The *cwd* parameter now accepts a bytes object.
6+
Based on patch by Anders Lorentsen.

0 commit comments

Comments
 (0)