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

Skip to content

Commit 1e0a88a

Browse files
committed
Issue #15533: Merge fix from 3.3.
2 parents 009f5b3 + 28714c8 commit 1e0a88a

3 files changed

Lines changed: 101 additions & 33 deletions

File tree

Doc/library/subprocess.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -484,10 +484,10 @@ functions.
484484
.. versionadded:: 3.2
485485
The *pass_fds* parameter was added.
486486

487-
If *cwd* is not ``None``, the child's current directory will be changed to *cwd*
488-
before it is executed. Note that this directory is not considered when
489-
searching the executable, so you can't specify the program's path relative to
490-
*cwd*.
487+
If *cwd* is not ``None``, the function changes the working directory to
488+
*cwd* before executing the child. In particular, the function looks for
489+
*executable* (or for the first item in *args*) relative to *cwd* if the
490+
executable path is a relative path.
491491

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

Lib/test/test_subprocess.py

Lines changed: 94 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import unittest
2+
from test import script_helper
23
from test import support
34
import subprocess
45
import sys
@@ -191,15 +192,101 @@ def test_stderr_none(self):
191192
p.wait()
192193
self.assertEqual(p.stderr, None)
193194

195+
# For use in the test_cwd* tests below.
196+
def _normalize_cwd(self, cwd):
197+
# Normalize an expected cwd (for Tru64 support).
198+
# We can't use os.path.realpath since it doesn't expand Tru64 {memb}
199+
# strings. See bug #1063571.
200+
original_cwd = os.getcwd()
201+
os.chdir(cwd)
202+
cwd = os.getcwd()
203+
os.chdir(original_cwd)
204+
return cwd
205+
206+
# For use in the test_cwd* tests below.
207+
def _split_python_path(self):
208+
# Return normalized (python_dir, python_base).
209+
python_path = os.path.realpath(sys.executable)
210+
return os.path.split(python_path)
211+
212+
# For use in the test_cwd* tests below.
213+
def _assert_cwd(self, expected_cwd, python_arg, **kwargs):
214+
# Invoke Python via Popen, and assert that (1) the call succeeds,
215+
# and that (2) the current working directory of the child process
216+
# matches *expected_cwd*.
217+
p = subprocess.Popen([python_arg, "-c",
218+
"import os, sys; "
219+
"sys.stdout.write(os.getcwd()); "
220+
"sys.exit(47)"],
221+
stdout=subprocess.PIPE,
222+
**kwargs)
223+
self.addCleanup(p.stdout.close)
224+
p.wait()
225+
self.assertEqual(47, p.returncode)
226+
normcase = os.path.normcase
227+
self.assertEqual(normcase(expected_cwd),
228+
normcase(p.stdout.read().decode("utf-8")))
229+
230+
def test_cwd(self):
231+
# Check that cwd changes the cwd for the child process.
232+
temp_dir = tempfile.gettempdir()
233+
temp_dir = self._normalize_cwd(temp_dir)
234+
self._assert_cwd(temp_dir, sys.executable, cwd=temp_dir)
235+
236+
def test_cwd_with_relative_arg(self):
237+
# Check that Popen looks for args[0] relative to cwd if args[0]
238+
# is relative.
239+
python_dir, python_base = self._split_python_path()
240+
rel_python = os.path.join(os.curdir, python_base)
241+
with support.temp_cwd() as wrong_dir:
242+
# Before calling with the correct cwd, confirm that the call fails
243+
# without cwd and with the wrong cwd.
244+
self.assertRaises(FileNotFoundError, subprocess.Popen,
245+
[rel_python])
246+
self.assertRaises(FileNotFoundError, subprocess.Popen,
247+
[rel_python], cwd=wrong_dir)
248+
python_dir = self._normalize_cwd(python_dir)
249+
self._assert_cwd(python_dir, rel_python, cwd=python_dir)
250+
251+
def test_cwd_with_relative_executable(self):
252+
# Check that Popen looks for executable relative to cwd if executable
253+
# is relative (and that executable takes precedence over args[0]).
254+
python_dir, python_base = self._split_python_path()
255+
rel_python = os.path.join(os.curdir, python_base)
256+
doesntexist = "somethingyoudonthave"
257+
with support.temp_cwd() as wrong_dir:
258+
# Before calling with the correct cwd, confirm that the call fails
259+
# without cwd and with the wrong cwd.
260+
self.assertRaises(FileNotFoundError, subprocess.Popen,
261+
[doesntexist], executable=rel_python)
262+
self.assertRaises(FileNotFoundError, subprocess.Popen,
263+
[doesntexist], executable=rel_python,
264+
cwd=wrong_dir)
265+
python_dir = self._normalize_cwd(python_dir)
266+
self._assert_cwd(python_dir, doesntexist, executable=rel_python,
267+
cwd=python_dir)
268+
269+
def test_cwd_with_absolute_arg(self):
270+
# Check that Popen can find the executable when the cwd is wrong
271+
# if args[0] is an absolute path.
272+
python_dir, python_base = self._split_python_path()
273+
abs_python = os.path.join(python_dir, python_base)
274+
rel_python = os.path.join(os.curdir, python_base)
275+
with script_helper.temp_dir() as wrong_dir:
276+
# Before calling with an absolute path, confirm that using a
277+
# relative path fails.
278+
self.assertRaises(FileNotFoundError, subprocess.Popen,
279+
[rel_python], cwd=wrong_dir)
280+
wrong_dir = self._normalize_cwd(wrong_dir)
281+
self._assert_cwd(wrong_dir, abs_python, cwd=wrong_dir)
282+
194283
@unittest.skipIf(sys.base_prefix != sys.prefix,
195284
'Test is not venv-compatible')
196285
def test_executable_with_cwd(self):
197-
python_dir = os.path.dirname(os.path.realpath(sys.executable))
198-
p = subprocess.Popen(["somethingyoudonthave", "-c",
199-
"import sys; sys.exit(47)"],
200-
executable=sys.executable, cwd=python_dir)
201-
p.wait()
202-
self.assertEqual(p.returncode, 47)
286+
python_dir, python_base = self._split_python_path()
287+
python_dir = self._normalize_cwd(python_dir)
288+
self._assert_cwd(python_dir, "somethingyoudonthave",
289+
executable=sys.executable, cwd=python_dir)
203290

204291
@unittest.skipIf(sys.base_prefix != sys.prefix,
205292
'Test is not venv-compatible')
@@ -208,11 +295,7 @@ def test_executable_with_cwd(self):
208295
def test_executable_without_cwd(self):
209296
# For a normal installation, it should work without 'cwd'
210297
# argument. For test runs in the build directory, see #7774.
211-
p = subprocess.Popen(["somethingyoudonthave", "-c",
212-
"import sys; sys.exit(47)"],
213-
executable=sys.executable)
214-
p.wait()
215-
self.assertEqual(p.returncode, 47)
298+
self._assert_cwd('', "somethingyoudonthave", executable=sys.executable)
216299

217300
def test_stdin_pipe(self):
218301
# stdin redirection
@@ -369,24 +452,6 @@ def test_stdin_devnull(self):
369452
p.wait()
370453
self.assertEqual(p.stdin, None)
371454

372-
def test_cwd(self):
373-
tmpdir = tempfile.gettempdir()
374-
# We cannot use os.path.realpath to canonicalize the path,
375-
# since it doesn't expand Tru64 {memb} strings. See bug 1063571.
376-
cwd = os.getcwd()
377-
os.chdir(tmpdir)
378-
tmpdir = os.getcwd()
379-
os.chdir(cwd)
380-
p = subprocess.Popen([sys.executable, "-c",
381-
'import sys,os;'
382-
'sys.stdout.write(os.getcwd())'],
383-
stdout=subprocess.PIPE,
384-
cwd=tmpdir)
385-
self.addCleanup(p.stdout.close)
386-
normcase = os.path.normcase
387-
self.assertEqual(normcase(p.stdout.read().decode("utf-8")),
388-
normcase(tmpdir))
389-
390455
def test_env(self):
391456
newenv = os.environ.copy()
392457
newenv["FRUIT"] = "orange"

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ Build
112112
Documentation
113113
-------------
114114

115+
- Issue #15533: Clarify docs and add tests for subprocess.Popen()'s cwd
116+
argument.
117+
115118
- Issue #16036: Improve documentation of built-in int()'s signature and
116119
arguments.
117120

0 commit comments

Comments
 (0)