-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
bpo-31961: Fix support of path-like executables in subprocess. #5914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9e87239
8cce344
0b4e876
e72cf67
ccd3464
291b0c6
cee318b
43eef6c
5c0dd88
ab8ebfe
80f0af3
9a0e746
148b13d
6a8d394
10fba36
7d51e79
1073a0f
fe37262
c6b0ade
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, | |
|
||
assert not pass_fds, "pass_fds not supported on Windows." | ||
|
||
if not isinstance(args, str): | ||
if isinstance(args, str): | ||
pass | ||
elif isinstance(args, bytes): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The elif chain could be simplified to have less repeated code: elif isinstance(args, (bytes, os.PathLike)):
if shell:
raise TypeError(f'{type(args)} args not allowed when shell=True on Windows')`
args = list2cmdline([args]) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Error messages should be different. Path-like |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This needs to be addressed in
Then in
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This does look like it would be nicer, allowing more mixing and matching of types. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @eryksun 's comment is related to the old version of the PR. I addressed it, except that that I decided to reject bytes |
||
|
||
if executable is not None: | ||
executable = os.fsdecode(executable) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still think bytes and pathlike should be supported for every item of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The POSIX implementation accepts also bytes as |
||
# Process startup details | ||
if startupinfo is None: | ||
startupinfo = STARTUPINFO() | ||
|
@@ -1262,7 +1276,7 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, | |
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 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, | |
|
||
if isinstance(args, (str, bytes)): | ||
args = [args] | ||
elif isinstance(args, os.PathLike): | ||
if shell: | ||
raise TypeError('path-like args is not allowed when ' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for consistency of error messages with my suggestion above I think |
||
'shell is true') | ||
args = [args] | ||
else: | ||
args = list(args) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -304,6 +304,18 @@ def test_executable(self): | |
"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 @@ def test_executable_replaces_shell(self): | |
# 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 @@ def test_cwd(self): | |
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 @@ def test_run_kwargs(self): | |
env=newenv) | ||
self.assertEqual(cp.returncode, 33) | ||
|
||
def test_run_with_pathlike_path(self): | ||
# bpo-31961: test run(pathlike_object) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like to make the first line descriptive comment a docstring. it shows up nicely in verbose testing mode. |
||
# 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'); " | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI - these 3.6 and 3.7 versionchanged updates should be made to the docs today to clarify the existing behavior on their own (and backported to at least the 3.7 docs)