diff options
| -rw-r--r-- | Doc/library/subprocess.rst | 8 | ||||
| -rw-r--r-- | Lib/test/test_subprocess.py | 123 | ||||
| -rw-r--r-- | Misc/NEWS | 3 | 
3 files changed, 101 insertions, 33 deletions
| diff --git a/Doc/library/subprocess.rst b/Doc/library/subprocess.rst index 042f8f4..3ecdf4b 100644 --- a/Doc/library/subprocess.rst +++ b/Doc/library/subprocess.rst @@ -484,10 +484,10 @@ functions.     .. versionadded:: 3.2        The *pass_fds* parameter was added. -   If *cwd* is not ``None``, the child's current directory will be changed to *cwd* -   before it is executed.  Note that this directory is not considered when -   searching the executable, so you can't specify the program's path relative to -   *cwd*. +   If *cwd* is not ``None``, the function changes the working directory to +   *cwd* before executing the child.  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.     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/test/test_subprocess.py b/Lib/test/test_subprocess.py index 2420772..24924f6 100644 --- a/Lib/test/test_subprocess.py +++ b/Lib/test/test_subprocess.py @@ -1,4 +1,5 @@  import unittest +from test import script_helper  from test import support  import subprocess  import sys @@ -191,15 +192,101 @@ class ProcessTestCase(BaseTestCase):          p.wait()          self.assertEqual(p.stderr, None) +    # For use in the test_cwd* tests below. +    def _normalize_cwd(self, cwd): +        # Normalize an expected cwd (for Tru64 support). +        # We can't use os.path.realpath since it doesn't expand Tru64 {memb} +        # strings.  See bug #1063571. +        original_cwd = os.getcwd() +        os.chdir(cwd) +        cwd = os.getcwd() +        os.chdir(original_cwd) +        return cwd + +    # For use in the test_cwd* tests below. +    def _split_python_path(self): +        # Return normalized (python_dir, python_base). +        python_path = os.path.realpath(sys.executable) +        return os.path.split(python_path) + +    # For use in the test_cwd* tests below. +    def _assert_cwd(self, expected_cwd, python_arg, **kwargs): +        # Invoke Python via Popen, and assert that (1) the call succeeds, +        # and that (2) the current working directory of the child process +        # matches *expected_cwd*. +        p = subprocess.Popen([python_arg, "-c", +                              "import os, sys; " +                              "sys.stdout.write(os.getcwd()); " +                              "sys.exit(47)"], +                              stdout=subprocess.PIPE, +                              **kwargs) +        self.addCleanup(p.stdout.close) +        p.wait() +        self.assertEqual(47, p.returncode) +        normcase = os.path.normcase +        self.assertEqual(normcase(expected_cwd), +                         normcase(p.stdout.read().decode("utf-8"))) + +    def test_cwd(self): +        # Check that cwd changes the cwd for the child process. +        temp_dir = tempfile.gettempdir() +        temp_dir = self._normalize_cwd(temp_dir) +        self._assert_cwd(temp_dir, sys.executable, cwd=temp_dir) + +    def test_cwd_with_relative_arg(self): +        # Check that Popen looks for args[0] relative to cwd if args[0] +        # is relative. +        python_dir, python_base = self._split_python_path() +        rel_python = os.path.join(os.curdir, python_base) +        with support.temp_cwd() as wrong_dir: +            # Before calling with the correct cwd, confirm that the call fails +            # without cwd and with the wrong cwd. +            self.assertRaises(FileNotFoundError, subprocess.Popen, +                              [rel_python]) +            self.assertRaises(FileNotFoundError, subprocess.Popen, +                              [rel_python], cwd=wrong_dir) +            python_dir = self._normalize_cwd(python_dir) +            self._assert_cwd(python_dir, rel_python, cwd=python_dir) + +    def test_cwd_with_relative_executable(self): +        # Check that Popen looks for executable relative to cwd if executable +        # is relative (and that executable takes precedence over args[0]). +        python_dir, python_base = self._split_python_path() +        rel_python = os.path.join(os.curdir, python_base) +        doesntexist = "somethingyoudonthave" +        with support.temp_cwd() as wrong_dir: +            # Before calling with the correct cwd, confirm that the call fails +            # without cwd and with the wrong cwd. +            self.assertRaises(FileNotFoundError, subprocess.Popen, +                              [doesntexist], executable=rel_python) +            self.assertRaises(FileNotFoundError, subprocess.Popen, +                              [doesntexist], executable=rel_python, +                              cwd=wrong_dir) +            python_dir = self._normalize_cwd(python_dir) +            self._assert_cwd(python_dir, doesntexist, executable=rel_python, +                             cwd=python_dir) + +    def test_cwd_with_absolute_arg(self): +        # Check that Popen can find the executable when the cwd is wrong +        # if args[0] is an absolute path. +        python_dir, python_base = self._split_python_path() +        abs_python = os.path.join(python_dir, python_base) +        rel_python = os.path.join(os.curdir, python_base) +        with script_helper.temp_dir() as wrong_dir: +            # Before calling with an absolute path, confirm that using a +            # relative path fails. +            self.assertRaises(FileNotFoundError, subprocess.Popen, +                              [rel_python], cwd=wrong_dir) +            wrong_dir = self._normalize_cwd(wrong_dir) +            self._assert_cwd(wrong_dir, abs_python, cwd=wrong_dir) +      @unittest.skipIf(sys.base_prefix != sys.prefix,                       'Test is not venv-compatible')      def test_executable_with_cwd(self): -        python_dir = os.path.dirname(os.path.realpath(sys.executable)) -        p = subprocess.Popen(["somethingyoudonthave", "-c", -                              "import sys; sys.exit(47)"], -                             executable=sys.executable, cwd=python_dir) -        p.wait() -        self.assertEqual(p.returncode, 47) +        python_dir, python_base = self._split_python_path() +        python_dir = self._normalize_cwd(python_dir) +        self._assert_cwd(python_dir, "somethingyoudonthave", +                         executable=sys.executable, cwd=python_dir)      @unittest.skipIf(sys.base_prefix != sys.prefix,                       'Test is not venv-compatible') @@ -208,11 +295,7 @@ class ProcessTestCase(BaseTestCase):      def test_executable_without_cwd(self):          # For a normal installation, it should work without 'cwd'          # argument.  For test runs in the build directory, see #7774. -        p = subprocess.Popen(["somethingyoudonthave", "-c", -                              "import sys; sys.exit(47)"], -                             executable=sys.executable) -        p.wait() -        self.assertEqual(p.returncode, 47) +        self._assert_cwd('', "somethingyoudonthave", executable=sys.executable)      def test_stdin_pipe(self):          # stdin redirection @@ -369,24 +452,6 @@ class ProcessTestCase(BaseTestCase):          p.wait()          self.assertEqual(p.stdin, None) -    def test_cwd(self): -        tmpdir = tempfile.gettempdir() -        # We cannot use os.path.realpath to canonicalize the path, -        # since it doesn't expand Tru64 {memb} strings. See bug 1063571. -        cwd = os.getcwd() -        os.chdir(tmpdir) -        tmpdir = os.getcwd() -        os.chdir(cwd) -        p = subprocess.Popen([sys.executable, "-c", -                              'import sys,os;' -                              'sys.stdout.write(os.getcwd())'], -                             stdout=subprocess.PIPE, -                             cwd=tmpdir) -        self.addCleanup(p.stdout.close) -        normcase = os.path.normcase -        self.assertEqual(normcase(p.stdout.read().decode("utf-8")), -                         normcase(tmpdir)) -      def test_env(self):          newenv = os.environ.copy()          newenv["FRUIT"] = "orange" @@ -105,6 +105,9 @@ Build  Documentation  ------------- +- Issue #15533: Clarify docs and add tests for subprocess.Popen()'s cwd +  argument. +  - Issue #16036: Improve documentation of built-in int()'s signature and    arguments. | 
