summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerhiy Storchaka <storchaka@gmail.com>2019-05-28 19:49:35 (GMT)
committerGitHub <noreply@github.com>2019-05-28 19:49:35 (GMT)
commit9e3c4526394856d6376eed4968d27d53e1d69b7d (patch)
tree709efb00303c937b13b77cec4e2d510d5f15811c
parent1b05aa219041eb1c9dbcb4ec6c1fa5b20f060bf5 (diff)
downloadcpython-9e3c4526394856d6376eed4968d27d53e1d69b7d.zip
cpython-9e3c4526394856d6376eed4968d27d53e1d69b7d.tar.gz
cpython-9e3c4526394856d6376eed4968d27d53e1d69b7d.tar.bz2
bpo-31961: Fix support of path-like executables in subprocess. (GH-5914)
-rw-r--r--Doc/library/subprocess.rst29
-rw-r--r--Lib/subprocess.py25
-rw-r--r--Lib/test/test_subprocess.py55
-rw-r--r--Misc/NEWS.d/next/Library/2018-03-27-13-28-16.bpo-31961.GjLoYu.rst6
4 files changed, 109 insertions, 6 deletions
diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst
index d840b46..ede5c3c 100644
--- a/Doc/library/subprocess.rst
+++ b/Doc/library/subprocess.rst
@@ -347,7 +347,8 @@ functions.
the class uses the Windows ``CreateProcess()`` function. The arguments to
:class:`Popen` are as follows.
- *args* should be a sequence of program arguments or else a single string.
+ *args* should be a sequence of program arguments or else a single string
+ or :term:`path-like object`.
By default, the program to execute is the first item in *args* if *args* is
a sequence. If *args* is a string, the interpretation is
platform-dependent and described below. See the *shell* and *executable*
@@ -381,6 +382,15 @@ functions.
manner described in :ref:`converting-argument-sequence`. This is because
the underlying ``CreateProcess()`` operates on strings.
+ .. versionchanged:: 3.6
+ *args* parameter accepts a :term:`path-like object` if *shell* is
+ ``False`` and a sequence containing path-like objects on POSIX.
+
+ .. versionchanged:: 3.8
+ *args* parameter accepts a :term:`path-like object` if *shell* is
+ ``False`` and a sequence containing bytes and path-like objects
+ on Windows.
+
The *shell* argument (which defaults to ``False``) specifies whether to use
the shell as the program to execute. If *shell* is ``True``, it is
recommended to pass *args* as a string rather than as a sequence.
@@ -436,6 +446,13 @@ functions.
:program:`ps`. If ``shell=True``, on POSIX the *executable* argument
specifies a replacement shell for the default :file:`/bin/sh`.
+ .. versionchanged:: 3.6
+ *executable* parameter accepts a :term:`path-like object` on POSIX.
+
+ .. versionchanged:: 3.8
+ *executable* parameter accepts a bytes and :term:`path-like object`
+ on Windows.
+
*stdin*, *stdout* and *stderr* specify the executed program's standard input,
standard output and standard error file handles, respectively. Valid values
are :data:`PIPE`, :data:`DEVNULL`, an existing file descriptor (a positive
@@ -492,13 +509,19 @@ functions.
The *pass_fds* parameter was added.
If *cwd* is not ``None``, the function changes the working directory to
- *cwd* before executing the child. *cwd* can be a :class:`str` and
+ *cwd* before executing the child. *cwd* can be a string, bytes or
:term:`path-like <path-like object>` object. In particular, the function
looks for *executable* (or for the first item in *args*) relative to *cwd*
if the executable path is a relative path.
.. versionchanged:: 3.6
- *cwd* parameter accepts a :term:`path-like object`.
+ *cwd* parameter accepts a :term:`path-like object` on POSIX.
+
+ .. versionchanged:: 3.7
+ *cwd* parameter accepts a :term:`path-like object` on Windows.
+
+ .. versionchanged:: 3.8
+ *cwd* parameter accepts a bytes object on Windows.
If *restore_signals* is true (the default) all signals that Python has set to
SIG_IGN are restored to SIG_DFL in the child process before the exec.
diff --git a/Lib/subprocess.py b/Lib/subprocess.py
index 6cc9eb3..9e36b9d 100644
--- a/Lib/subprocess.py
+++ b/Lib/subprocess.py
@@ -521,7 +521,7 @@ def list2cmdline(seq):
# "Parsing C++ Command-Line Arguments"
result = []
needquote = False
- for arg in seq:
+ for arg in map(os.fsdecode, seq):
bs_buf = []
# Add a space to separate this argument from the others
@@ -1203,9 +1203,23 @@ class Popen(object):
assert not pass_fds, "pass_fds not supported on Windows."
- if not isinstance(args, str):
+ if isinstance(args, str):
+ pass
+ elif isinstance(args, bytes):
+ if shell:
+ raise TypeError('bytes args is not allowed on Windows')
+ args = list2cmdline([args])
+ elif isinstance(args, os.PathLike):
+ if shell:
+ raise TypeError('path-like args is not allowed when '
+ 'shell is true')
+ args = list2cmdline([args])
+ else:
args = list2cmdline(args)
+ if executable is not None:
+ executable = os.fsdecode(executable)
+
# Process startup details
if startupinfo is None:
startupinfo = STARTUPINFO()
@@ -1262,7 +1276,7 @@ class Popen(object):
int(not close_fds),
creationflags,
env,
- os.fspath(cwd) if cwd is not None else None,
+ os.fsdecode(cwd) if cwd is not None else None,
startupinfo)
finally:
# Child is launched. Close the parent's copy of those pipe
@@ -1510,6 +1524,11 @@ class Popen(object):
if isinstance(args, (str, bytes)):
args = [args]
+ elif isinstance(args, os.PathLike):
+ if shell:
+ raise TypeError('path-like args is not allowed when '
+ 'shell is true')
+ args = [args]
else:
args = list(args)
diff --git a/Lib/test/test_subprocess.py b/Lib/test/test_subprocess.py
index b0b6b06..fca3ed6 100644
--- a/Lib/test/test_subprocess.py
+++ b/Lib/test/test_subprocess.py
@@ -304,6 +304,18 @@ class ProcessTestCase(BaseTestCase):
"doesnotexist")
self._assert_python([doesnotexist, "-c"], executable=sys.executable)
+ def test_bytes_executable(self):
+ doesnotexist = os.path.join(os.path.dirname(sys.executable),
+ "doesnotexist")
+ self._assert_python([doesnotexist, "-c"],
+ executable=os.fsencode(sys.executable))
+
+ def test_pathlike_executable(self):
+ doesnotexist = os.path.join(os.path.dirname(sys.executable),
+ "doesnotexist")
+ self._assert_python([doesnotexist, "-c"],
+ executable=FakePath(sys.executable))
+
def test_executable_takes_precedence(self):
# Check that the executable argument takes precedence over args[0].
#
@@ -320,6 +332,16 @@ class ProcessTestCase(BaseTestCase):
# when shell=True.
self._assert_python([], executable=sys.executable, shell=True)
+ @unittest.skipIf(mswindows, "executable argument replaces shell")
+ def test_bytes_executable_replaces_shell(self):
+ self._assert_python([], executable=os.fsencode(sys.executable),
+ shell=True)
+
+ @unittest.skipIf(mswindows, "executable argument replaces shell")
+ def test_pathlike_executable_replaces_shell(self):
+ self._assert_python([], executable=FakePath(sys.executable),
+ shell=True)
+
# For use in the test_cwd* tests below.
def _normalize_cwd(self, cwd):
# Normalize an expected cwd (for Tru64 support).
@@ -358,6 +380,11 @@ class ProcessTestCase(BaseTestCase):
temp_dir = self._normalize_cwd(temp_dir)
self._assert_cwd(temp_dir, sys.executable, cwd=temp_dir)
+ def test_cwd_with_bytes(self):
+ temp_dir = tempfile.gettempdir()
+ temp_dir = self._normalize_cwd(temp_dir)
+ self._assert_cwd(temp_dir, sys.executable, cwd=os.fsencode(temp_dir))
+
def test_cwd_with_pathlike(self):
temp_dir = tempfile.gettempdir()
temp_dir = self._normalize_cwd(temp_dir)
@@ -1473,6 +1500,34 @@ class RunFuncTestCase(BaseTestCase):
env=newenv)
self.assertEqual(cp.returncode, 33)
+ def test_run_with_pathlike_path(self):
+ # bpo-31961: test run(pathlike_object)
+ # the name of a command that can be run without
+ # any argumenets that exit fast
+ prog = 'tree.com' if mswindows else 'ls'
+ path = shutil.which(prog)
+ if path is None:
+ self.skipTest(f'{prog} required for this test')
+ path = FakePath(path)
+ res = subprocess.run(path, stdout=subprocess.DEVNULL)
+ self.assertEqual(res.returncode, 0)
+ with self.assertRaises(TypeError):
+ subprocess.run(path, stdout=subprocess.DEVNULL, shell=True)
+
+ def test_run_with_bytes_path_and_arguments(self):
+ # bpo-31961: test run([bytes_object, b'additional arguments'])
+ path = os.fsencode(sys.executable)
+ args = [path, '-c', b'import sys; sys.exit(57)']
+ res = subprocess.run(args)
+ self.assertEqual(res.returncode, 57)
+
+ def test_run_with_pathlike_path_and_arguments(self):
+ # bpo-31961: test run([pathlike_object, 'additional arguments'])
+ path = FakePath(sys.executable)
+ args = [path, '-c', 'import sys; sys.exit(57)']
+ res = subprocess.run(args)
+ self.assertEqual(res.returncode, 57)
+
def test_capture_output(self):
cp = self.run_python(("import sys;"
"sys.stdout.write('BDFL'); "
diff --git a/Misc/NEWS.d/next/Library/2018-03-27-13-28-16.bpo-31961.GjLoYu.rst b/Misc/NEWS.d/next/Library/2018-03-27-13-28-16.bpo-31961.GjLoYu.rst
new file mode 100644
index 0000000..a38db67
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2018-03-27-13-28-16.bpo-31961.GjLoYu.rst
@@ -0,0 +1,6 @@
+Added support for bytes and path-like objects in :func:`subprocess.Popen`
+on Windows. The *args* parameter now accepts a :term:`path-like object` if
+*shell* is ``False`` and a sequence containing bytes and path-like objects.
+The *executable* parameter now accepts a bytes and :term:`path-like object`.
+The *cwd* parameter now accepts a bytes object.
+Based on patch by Anders Lorentsen.