From 01c152e3a9ba997f5b9ca3c0774e786e19b9eb7b Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sat, 1 Apr 2023 19:07:10 -0700 Subject: [PATCH 01/31] GH-75586 - Fix case where PATHEXT isn't applied to items in PATH (Windows) --- Lib/shutil.py | 12 ++++++++---- Lib/test/test_shutil.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index b0576407e02ffb..225a71a1c7ea71 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1462,10 +1462,10 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): # If we're given a path with a directory part, look it up directly rather # than referring to PATH directories. This includes checking relative to the # current directory, e.g. ./script + # If not, later logic will further verify. if os.path.dirname(cmd): if _access_check(cmd, mode): return cmd - return None use_bytes = isinstance(cmd, bytes) @@ -1524,7 +1524,11 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): if not normdir in seen: seen.add(normdir) for thefile in files: - name = os.path.join(dir, thefile) - if _access_check(name, mode): - return name + # Only allow thefile to have a directory component if the directory is the current directory. + # This prevents allowing a directory component (to be part of cmd), being applied after a PATH component. + # Unless it with reference to the current directory, e.g. ./script (or full path: C:\scriptdir\script) + if not os.path.dirname(thefile) or dir == (os.fsencode(os.curdir) if use_bytes else os.curdir): + name = os.path.join(dir, thefile) + if _access_check(name, mode): + return name return None diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 1c0589ced9ea89..248000abf91c5e 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -2179,6 +2179,18 @@ def test_pathext_with_empty_str(self): rv = shutil.which(program, path=self.temp_dir) self.assertEqual(rv, temp_filexyz.name) + # See GH-75586 + @unittest.skipUnless(sys.platform == "win32", 'test specific to Windows') + def test_pathext_applied_on_files_in_path(self): + with os_helper.EnvironmentVarGuard() as env: + env["PATH"] = self.temp_dir + env["PATHEXT"] = ".test" + + test_path = pathlib.Path(self.temp_dir) / "test_program.test" + test_path.touch(mode=0o755) + + self.assertEqual(shutil.which("test_program"), str(test_path)) + class TestWhichBytes(TestWhich): def setUp(self): From 0d4cd7b727b82ef8cfb2ca8101fee2d91f09ef75 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 13:06:24 -0700 Subject: [PATCH 02/31] PR updates --- Lib/shutil.py | 110 ++++++++++++++++++++++------------------ Lib/test/test_shutil.py | 15 ++++++ 2 files changed, 75 insertions(+), 50 deletions(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 225a71a1c7ea71..7084e12fa1dd7f 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -40,6 +40,13 @@ elif _WINDOWS: import nt +if sys.platform == 'win32': + try: + import ctypes + _CTYPES_SUPPORTED = True + except ImportError: + _CTYPES_SUPPORTED = False + COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024 # This should never be removed, see rationale in: # https://bugs.python.org/issue43743#msg393429 @@ -1449,6 +1456,18 @@ def _access_check(fn, mode): and not os.path.isdir(fn)) +def _win32_need_current_directory_for_exe_path(cmd): + """ + On Windows, we can use NeedCurrentDirectoryForExePathW to figure out + if we should add the cwd to PATH when searching for executables. + + If we don't have ctypes, we'll fallback to old behavior which is to always add cwd. + """ + if _CTYPES_SUPPORTED: + return bool(ctypes.windll.kernel32.NeedCurrentDirectoryForExePathW(cmd)) + return True + + def which(cmd, mode=os.F_OK | os.X_OK, path=None): """Given a command, mode, and a PATH string, return the path which conforms to the given mode on the PATH, or None if there is no such @@ -1459,60 +1478,55 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): path. """ - # If we're given a path with a directory part, look it up directly rather - # than referring to PATH directories. This includes checking relative to the - # current directory, e.g. ./script - # If not, later logic will further verify. - if os.path.dirname(cmd): - if _access_check(cmd, mode): - return cmd - use_bytes = isinstance(cmd, bytes) - if path is None: - path = os.environ.get("PATH", None) - if path is None: - try: - path = os.confstr("CS_PATH") - except (AttributeError, ValueError): - # os.confstr() or CS_PATH is not available - path = os.defpath - # bpo-35755: Don't use os.defpath if the PATH environment variable is - # set to an empty string - - # PATH='' doesn't match, whereas PATH=':' looks in the current directory - if not path: - return None - - if use_bytes: - path = os.fsencode(path) - path = path.split(os.fsencode(os.pathsep)) + # If we're given a path with a directory part, look it up directly rather + # than referring to PATH directories. This includes checking relative to + # the current directory, e.g. ./script + dirname, cmd = os.path.split(cmd) + if dirname: + path = [dirname] else: - path = os.fsdecode(path) - path = path.split(os.pathsep) + if path is None: + path = os.environ.get("PATH", None) + if path is None: + try: + path = os.confstr("CS_PATH") + except (AttributeError, ValueError): + # os.confstr() or CS_PATH is not available + path = os.defpath + # bpo-35755: Don't use os.defpath if the PATH environment variable + # is set to an empty string + + # PATH='' doesn't match, whereas PATH=':' looks in the current + # directory + if not path: + return None - if sys.platform == "win32": - # The current directory takes precedence on Windows. - curdir = os.curdir if use_bytes: - curdir = os.fsencode(curdir) - if curdir not in path: - path.insert(0, curdir) + path = os.fsencode(path) + path = path.split(os.fsencode(os.pathsep)) + else: + path = os.fsdecode(path) + path = path.split(os.pathsep) + + if sys.platform == "win32" and _win32_need_current_directory_for_exe_path(cmd): + curdir = os.curdir + if use_bytes: + curdir = os.fsencode(curdir) + if curdir not in path: + path.insert(0, curdir) + if sys.platform == "win32": # PATHEXT is necessary to check on Windows. - pathext_source = os.getenv("PATHEXT") or _WIN_DEFAULT_PATHEXT + pathext_source = os.getenv("PATHEXT", _WIN_DEFAULT_PATHEXT) pathext = [ext for ext in pathext_source.split(os.pathsep) if ext] if use_bytes: pathext = [os.fsencode(ext) for ext in pathext] - # See if the given file matches any of the expected path extensions. - # This will allow us to short circuit when given "python.exe". - # If it does match, only test that one, otherwise we have to try - # others. - if any(cmd.lower().endswith(ext.lower()) for ext in pathext): - files = [cmd] - else: - files = [cmd + ext for ext in pathext] + + # Always try checking the originally given cmd, if it doesn't match, try pathext + files = [cmd] + [cmd + ext for ext in pathext] else: # On other platforms you don't have things like PATHEXT to tell you # what file suffixes are executable, so just pass on cmd as-is. @@ -1524,11 +1538,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): if not normdir in seen: seen.add(normdir) for thefile in files: - # Only allow thefile to have a directory component if the directory is the current directory. - # This prevents allowing a directory component (to be part of cmd), being applied after a PATH component. - # Unless it with reference to the current directory, e.g. ./script (or full path: C:\scriptdir\script) - if not os.path.dirname(thefile) or dir == (os.fsencode(os.curdir) if use_bytes else os.curdir): - name = os.path.join(dir, thefile) - if _access_check(name, mode): - return name + name = os.path.join(dir, thefile) + if _access_check(name, mode): + return name return None diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 248000abf91c5e..56ca09f2dc3c8f 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -2191,6 +2191,21 @@ def test_pathext_applied_on_files_in_path(self): self.assertEqual(shutil.which("test_program"), str(test_path)) + # See GH-75586 + @unittest.skipUnless(sys.platform == "win32", 'test specific to Windows') + def test_win32_need_current_directory_for_exe_path_true_without_ctypes(self): + with unittest.mock.patch('shutil._CTYPES_SUPPORTED', False): + self.assertTrue(shutil._win32_need_current_directory_for_exe_path('anything')) + + # See GH-75586 + @unittest.skipUnless(sys.platform == "win32", 'test specific to Windows') + def test_win32_need_current_directory_for_exe_path_with_ctypes(self): + with unittest.mock.patch('shutil._CTYPES_SUPPORTED', True): + with unittest.mock.patch('shutil.ctypes') as ctypes_mock: + ctypes_mock.windll.kernel32.NeedCurrentDirectoryForExePathW.return_value = 0 + self.assertFalse(shutil._win32_need_current_directory_for_exe_path('test.exe')) + ctypes_mock.windll.kernel32.NeedCurrentDirectoryForExePathW.assert_called_once_with('test.exe') + class TestWhichBytes(TestWhich): def setUp(self): From 5fac84a4bcad31d7d64964608f328931260df3cd Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 13:13:13 -0700 Subject: [PATCH 03/31] Add tests --- Lib/test/test_shutil.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 56ca09f2dc3c8f..c690f3de4ac3b4 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -2034,16 +2034,28 @@ def test_relative_cmd(self): rv = shutil.which(relpath, path=base_dir) self.assertIsNone(rv) - def test_cwd(self): + @unittest.skipUnless(sys.platform != "win32", + "test is for non win32") + def test_cwd_non_win32(self): # Issue #16957 base_dir = os.path.dirname(self.dir) with os_helper.change_cwd(path=self.dir): rv = shutil.which(self.file, path=base_dir) - if sys.platform == "win32": - # Windows: current directory implicitly on PATH + # non-win32: shouldn't match in the current directory. + self.assertIsNone(rv) + + @unittest.skipUnless(sys.platform == "win32", + "test is for win32") + def test_cwd_win32(self): + base_dir = os.path.dirname(self.dir) + with os_helper.change_cwd(path=self.dir): + with unittest.mock.patch('shutil._win32_need_current_directory_for_exe_path', return_value=True): + rv = shutil.which(self.file, path=base_dir) + # Current directory implicitly on PATH self.assertEqual(rv, os.path.join(self.curdir, self.file)) - else: - # Other platforms: shouldn't match in the current directory. + with unittest.mock.patch('shutil._win32_need_current_directory_for_exe_path', return_value=False): + rv = shutil.which(self.file, path=base_dir) + # Current directory not on PATH self.assertIsNone(rv) @os_helper.skip_if_dac_override From fa145da4e2ee79055e1e03558fefc59807f91421 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 14:59:10 -0700 Subject: [PATCH 04/31] PR updates --- Lib/shutil.py | 23 ++++++++------------- Lib/test/test_shutil.py | 23 ++++++++------------- Modules/_winapi.c | 21 +++++++++++++++++++ Modules/clinic/_winapi.c.h | 42 +++++++++++++++++++++++++++++++++++++- 4 files changed, 79 insertions(+), 30 deletions(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 7084e12fa1dd7f..2fc25a6b733985 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -41,11 +41,7 @@ import nt if sys.platform == 'win32': - try: - import ctypes - _CTYPES_SUPPORTED = True - except ImportError: - _CTYPES_SUPPORTED = False + import _winapi COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024 # This should never be removed, see rationale in: @@ -1456,16 +1452,13 @@ def _access_check(fn, mode): and not os.path.isdir(fn)) -def _win32_need_current_directory_for_exe_path(cmd): +def _win_path_needs_curdir(cmd, mode): """ - On Windows, we can use NeedCurrentDirectoryForExePathW to figure out - if we should add the cwd to PATH when searching for executables. - - If we don't have ctypes, we'll fallback to old behavior which is to always add cwd. + On Windows, we can use NeedCurrentDirectoryForExePath to figure out + if we should add the cwd to PATH when searching for executables if + the mode is executable. """ - if _CTYPES_SUPPORTED: - return bool(ctypes.windll.kernel32.NeedCurrentDirectoryForExePathW(cmd)) - return True + return mode & os.X_OK and _winapi.NeedCurrentDirectoryForExePath(cmd if isinstance(cmd, str) else os.fsdecode(cmd)) def which(cmd, mode=os.F_OK | os.X_OK, path=None): @@ -1510,7 +1503,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): path = os.fsdecode(path) path = path.split(os.pathsep) - if sys.platform == "win32" and _win32_need_current_directory_for_exe_path(cmd): + if sys.platform == "win32" and _win_path_needs_curdir(cmd, mode): curdir = os.curdir if use_bytes: curdir = os.fsencode(curdir) @@ -1519,7 +1512,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): if sys.platform == "win32": # PATHEXT is necessary to check on Windows. - pathext_source = os.getenv("PATHEXT", _WIN_DEFAULT_PATHEXT) + pathext_source = os.getenv("PATHEXT") or _WIN_DEFAULT_PATHEXT pathext = [ext for ext in pathext_source.split(os.pathsep) if ext] if use_bytes: diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index c690f3de4ac3b4..4025a83dda83de 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -2049,11 +2049,11 @@ def test_cwd_non_win32(self): def test_cwd_win32(self): base_dir = os.path.dirname(self.dir) with os_helper.change_cwd(path=self.dir): - with unittest.mock.patch('shutil._win32_need_current_directory_for_exe_path', return_value=True): + with unittest.mock.patch('shutil._win_path_needs_curdir', return_value=True): rv = shutil.which(self.file, path=base_dir) # Current directory implicitly on PATH self.assertEqual(rv, os.path.join(self.curdir, self.file)) - with unittest.mock.patch('shutil._win32_need_current_directory_for_exe_path', return_value=False): + with unittest.mock.patch('shutil._win_path_needs_curdir', return_value=False): rv = shutil.which(self.file, path=base_dir) # Current directory not on PATH self.assertIsNone(rv) @@ -2205,18 +2205,13 @@ def test_pathext_applied_on_files_in_path(self): # See GH-75586 @unittest.skipUnless(sys.platform == "win32", 'test specific to Windows') - def test_win32_need_current_directory_for_exe_path_true_without_ctypes(self): - with unittest.mock.patch('shutil._CTYPES_SUPPORTED', False): - self.assertTrue(shutil._win32_need_current_directory_for_exe_path('anything')) - - # See GH-75586 - @unittest.skipUnless(sys.platform == "win32", 'test specific to Windows') - def test_win32_need_current_directory_for_exe_path_with_ctypes(self): - with unittest.mock.patch('shutil._CTYPES_SUPPORTED', True): - with unittest.mock.patch('shutil.ctypes') as ctypes_mock: - ctypes_mock.windll.kernel32.NeedCurrentDirectoryForExePathW.return_value = 0 - self.assertFalse(shutil._win32_need_current_directory_for_exe_path('test.exe')) - ctypes_mock.windll.kernel32.NeedCurrentDirectoryForExePathW.assert_called_once_with('test.exe') + def test_win_path_needs_curdir(self): + with unittest.mock.patch('shutil._winapi.NeedCurrentDirectoryForExePath', return_value=True) as need_curdir_mock: + self.assertTrue(shutil._win_path_needs_curdir('dontcare', os.X_OK)) + need_curdir_mock.assert_called_once_with('dontcare') + need_curdir_mock.reset_mock() + self.assertFalse(shutil._win_path_needs_curdir('dontcare', 0)) + need_curdir_mock.assert_not_called() class TestWhichBytes(TestWhich): diff --git a/Modules/_winapi.c b/Modules/_winapi.c index 83cde7501176b6..fa380b8b798405 100644 --- a/Modules/_winapi.c +++ b/Modules/_winapi.c @@ -2054,6 +2054,26 @@ _winapi__mimetypes_read_windows_registry_impl(PyObject *module, #undef CB_TYPE } +/*[clinic input] +_winapi.NeedCurrentDirectoryForExePath -> bool + + exe_name: LPCWSTR + / +[clinic start generated code]*/ + +static int +_winapi_NeedCurrentDirectoryForExePath_impl(PyObject *module, + LPCWSTR exe_name) +/*[clinic end generated code: output=a65ec879502b58fc input=972aac88a1ec2f00]*/ +{ + BOOL result; + + Py_BEGIN_ALLOW_THREADS + result = NeedCurrentDirectoryForExePathW(exe_name); + Py_END_ALLOW_THREADS + + return result; +} static PyMethodDef winapi_functions[] = { _WINAPI_CLOSEHANDLE_METHODDEF @@ -2089,6 +2109,7 @@ static PyMethodDef winapi_functions[] = { _WINAPI_GETACP_METHODDEF _WINAPI_GETFILETYPE_METHODDEF _WINAPI__MIMETYPES_READ_WINDOWS_REGISTRY_METHODDEF + _WINAPI_NEEDCURRENTDIRECTORYFOREXEPATH_METHODDEF {NULL, NULL} }; diff --git a/Modules/clinic/_winapi.c.h b/Modules/clinic/_winapi.c.h index 891b3f851d1243..7bc63e612be348 100644 --- a/Modules/clinic/_winapi.c.h +++ b/Modules/clinic/_winapi.c.h @@ -1371,4 +1371,44 @@ _winapi__mimetypes_read_windows_registry(PyObject *module, PyObject *const *args exit: return return_value; } -/*[clinic end generated code: output=edb1a9d1bbfd6394 input=a9049054013a1b77]*/ + +PyDoc_STRVAR(_winapi_NeedCurrentDirectoryForExePath__doc__, +"NeedCurrentDirectoryForExePath($module, exe_name, /)\n" +"--\n" +"\n"); + +#define _WINAPI_NEEDCURRENTDIRECTORYFOREXEPATH_METHODDEF \ + {"NeedCurrentDirectoryForExePath", (PyCFunction)_winapi_NeedCurrentDirectoryForExePath, METH_O, _winapi_NeedCurrentDirectoryForExePath__doc__}, + +static int +_winapi_NeedCurrentDirectoryForExePath_impl(PyObject *module, + LPCWSTR exe_name); + +static PyObject * +_winapi_NeedCurrentDirectoryForExePath(PyObject *module, PyObject *arg) +{ + PyObject *return_value = NULL; + LPCWSTR exe_name = NULL; + int _return_value; + + if (!PyUnicode_Check(arg)) { + _PyArg_BadArgument("NeedCurrentDirectoryForExePath", "argument", "str", arg); + goto exit; + } + exe_name = PyUnicode_AsWideCharString(arg, NULL); + if (exe_name == NULL) { + goto exit; + } + _return_value = _winapi_NeedCurrentDirectoryForExePath_impl(module, exe_name); + if ((_return_value == -1) && PyErr_Occurred()) { + goto exit; + } + return_value = PyBool_FromLong((long)_return_value); + +exit: + /* Cleanup for exe_name */ + PyMem_Free((void *)exe_name); + + return return_value; +} +/*[clinic end generated code: output=96ea65ece7912d0a input=a9049054013a1b77]*/ From 84a79767803688e27d92cbb5cf5412d697289b62 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 15:00:57 -0700 Subject: [PATCH 05/31] line len fix --- Lib/shutil.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 2fc25a6b733985..4f306805cef012 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1458,7 +1458,9 @@ def _win_path_needs_curdir(cmd, mode): if we should add the cwd to PATH when searching for executables if the mode is executable. """ - return mode & os.X_OK and _winapi.NeedCurrentDirectoryForExePath(cmd if isinstance(cmd, str) else os.fsdecode(cmd)) + return mode & os.X_OK and _winapi.NeedCurrentDirectoryForExePath( + cmd if isinstance(cmd, str) else os.fsdecode(cmd) + ) def which(cmd, mode=os.F_OK | os.X_OK, path=None): From 63a06c46af05887409b2a31a4533490b063220be Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sun, 2 Apr 2023 22:04:27 +0000 Subject: [PATCH 06/31] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst diff --git a/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst b/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst new file mode 100644 index 00000000000000..dbce13eed05315 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst @@ -0,0 +1 @@ +Fix an issue with `shutil.which` on Windows where PATHEXT was not consulted when checking for a match in PATH. From 7686a232f939849094ad0d29e1c3ce7c64a8c052 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 15:08:42 -0700 Subject: [PATCH 07/31] Add changelog entry --- Doc/whatsnew/3.12.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 8ab96134596cbf..4d7193debbe32a 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -343,6 +343,10 @@ shutil will be removed in Python 3.14. (Contributed by Irit Katriel in :gh:`102828`.) +* :func:`shutil.which` now consults the *PATHEXT* environment variable to + find matches within *PATH* on Windows. + (Contributed by Charles Machalow in :gh:`103179`.) + sqlite3 ------- From 381e4feda570e1686a1e563a455208864c369bbf Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 15:19:42 -0700 Subject: [PATCH 08/31] Double backticks --- .../next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst b/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst index dbce13eed05315..744a9163243906 100644 --- a/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst +++ b/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst @@ -1 +1 @@ -Fix an issue with `shutil.which` on Windows where PATHEXT was not consulted when checking for a match in PATH. +Fix an issue with ``shutil.which`` on Windows where PATHEXT was not consulted when checking for a match in PATH. From e7c0b5857f843675a5ce340e6ec12cc2cb9199e1 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 15:36:34 -0700 Subject: [PATCH 09/31] Update Lib/shutil.py Co-authored-by: Eryk Sun --- Lib/shutil.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 4f306805cef012..98341a67d705ba 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1458,9 +1458,9 @@ def _win_path_needs_curdir(cmd, mode): if we should add the cwd to PATH when searching for executables if the mode is executable. """ - return mode & os.X_OK and _winapi.NeedCurrentDirectoryForExePath( - cmd if isinstance(cmd, str) else os.fsdecode(cmd) - ) + return (not (mode & os.X_OK) or + _winapi.NeedCurrentDirectoryForExePath( + cmd if isinstance(cmd, str) else os.fsdecode(cmd)) def which(cmd, mode=os.F_OK | os.X_OK, path=None): From 26e3b1593279dba423a97e2fe04b5500ff74256a Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 15:46:17 -0700 Subject: [PATCH 10/31] PR updates --- Lib/shutil.py | 2 +- Lib/test/test_shutil.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 98341a67d705ba..0cd6ad810e6846 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1458,7 +1458,7 @@ def _win_path_needs_curdir(cmd, mode): if we should add the cwd to PATH when searching for executables if the mode is executable. """ - return (not (mode & os.X_OK) or + return (not (mode & os.X_OK)) or \ _winapi.NeedCurrentDirectoryForExePath( cmd if isinstance(cmd, str) else os.fsdecode(cmd)) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 4025a83dda83de..80c95a4f2e4297 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -2210,9 +2210,13 @@ def test_win_path_needs_curdir(self): self.assertTrue(shutil._win_path_needs_curdir('dontcare', os.X_OK)) need_curdir_mock.assert_called_once_with('dontcare') need_curdir_mock.reset_mock() - self.assertFalse(shutil._win_path_needs_curdir('dontcare', 0)) + self.assertTrue(shutil._win_path_needs_curdir('dontcare', 0)) need_curdir_mock.assert_not_called() + with unittest.mock.patch('shutil._winapi.NeedCurrentDirectoryForExePath', return_value=False) as need_curdir_mock: + self.assertFalse(shutil._win_path_needs_curdir('dontcare', os.X_OK)) + need_curdir_mock.assert_called_once_with('dontcare') + class TestWhichBytes(TestWhich): def setUp(self): From 6272b62a4c5b3822dcb6208f565940f4ac7ab23c Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 15:54:37 -0700 Subject: [PATCH 11/31] pep8 fix --- Lib/shutil.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 0cd6ad810e6846..62fe3eae867449 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1458,8 +1458,7 @@ def _win_path_needs_curdir(cmd, mode): if we should add the cwd to PATH when searching for executables if the mode is executable. """ - return (not (mode & os.X_OK)) or \ - _winapi.NeedCurrentDirectoryForExePath( + return (not (mode & os.X_OK)) or _winapi.NeedCurrentDirectoryForExePath( cmd if isinstance(cmd, str) else os.fsdecode(cmd)) From b6d29c88183789a611f1241ad46e8dedb683ee0f Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 16:10:25 -0700 Subject: [PATCH 12/31] Update Lib/test/test_shutil.py Co-authored-by: Eryk Sun --- Lib/test/test_shutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 80c95a4f2e4297..0092babb486461 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -2213,7 +2213,7 @@ def test_win_path_needs_curdir(self): self.assertTrue(shutil._win_path_needs_curdir('dontcare', 0)) need_curdir_mock.assert_not_called() - with unittest.mock.patch('shutil._winapi.NeedCurrentDirectoryForExePath', return_value=False) as need_curdir_mock: + with unittest.mock.patch('_winapi.NeedCurrentDirectoryForExePath', return_value=False) as need_curdir_mock: self.assertFalse(shutil._win_path_needs_curdir('dontcare', os.X_OK)) need_curdir_mock.assert_called_once_with('dontcare') From 1096cb7101f2b17ff637f048f0f24c1bf3a03935 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Sun, 2 Apr 2023 16:10:32 -0700 Subject: [PATCH 13/31] Update Lib/test/test_shutil.py Co-authored-by: Eryk Sun --- Lib/test/test_shutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 0092babb486461..bbd8f59327c88a 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -2206,7 +2206,7 @@ def test_pathext_applied_on_files_in_path(self): # See GH-75586 @unittest.skipUnless(sys.platform == "win32", 'test specific to Windows') def test_win_path_needs_curdir(self): - with unittest.mock.patch('shutil._winapi.NeedCurrentDirectoryForExePath', return_value=True) as need_curdir_mock: + with unittest.mock.patch('_winapi.NeedCurrentDirectoryForExePath', return_value=True) as need_curdir_mock: self.assertTrue(shutil._win_path_needs_curdir('dontcare', os.X_OK)) need_curdir_mock.assert_called_once_with('dontcare') need_curdir_mock.reset_mock() From 92955d0d1296011fecbad419bdcc98e96a369577 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 11:40:09 -0700 Subject: [PATCH 14/31] Update Lib/shutil.py Co-authored-by: Steve Dower --- Lib/shutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 62fe3eae867449..a2fa7f33f3877b 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1459,7 +1459,7 @@ def _win_path_needs_curdir(cmd, mode): the mode is executable. """ return (not (mode & os.X_OK)) or _winapi.NeedCurrentDirectoryForExePath( - cmd if isinstance(cmd, str) else os.fsdecode(cmd)) + os.fsdecode(cmd)) def which(cmd, mode=os.F_OK | os.X_OK, path=None): From 616df6c3d7b7f80f0cad917462e11482e1437850 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 12:07:22 -0700 Subject: [PATCH 15/31] docs updates --- Doc/library/shutil.rst | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index acba66258fe8f0..4003324bbe5903 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -433,23 +433,42 @@ Directory and files operations When no *path* is specified, the results of :func:`os.environ` are used, returning either the "PATH" value or a fallback of :attr:`os.defpath`. - On Windows, the current directory is always prepended to the *path* whether - or not you use the default or provide your own, which is the behavior the - command shell uses when finding executables. Additionally, when finding the - *cmd* in the *path*, the ``PATHEXT`` environment variable is checked. For - example, if you call ``shutil.which("python")``, :func:`which` will search - ``PATHEXT`` to know that it should look for ``python.exe`` within the *path* - directories. For example, on Windows:: + On Windows, the current directory is prepended to the *path* if + the *mode* does not include ``os.X_OK``. When the *mode* does include + ``os.X_OK``, the Windows API ``NeedCurrentDirectoryForExePathW`` will be + consulted to determine if the current directory should be prepended to + *path*. + + Additionally, when finding the *cmd* in the *path*, the ``PATHEXT`` + environment variable is checked. For example, if you call + ``shutil.which("python")``, :func:`which` will search ``PATHEXT`` + to know that it should look for ``python.exe`` within the *path* + directories. For example, on Windows:: >>> shutil.which("python") 'C:\\Python33\\python.EXE' + Similar ``PATHEXT`` logic is also applied when a full path to a *cmd* is + given, though without an extension:: + + >> shutil.which("C:\\Python33\\python") + 'C:\\Python33\\python.EXE' + .. versionadded:: 3.3 .. versionchanged:: 3.8 The :class:`bytes` type is now accepted. If *cmd* type is :class:`bytes`, the result type is also :class:`bytes`. + .. versionchanged:: 3.12 + On Windows: ``NeedCurrentDirectoryForExePathW`` will be consulted + for non- ``os.X_OK`` modes to determine if the current working directory + should be prepended to *path*. Additionally, the ``PATHEXT`` environment + variable is now consulted when a full path to a cmd, minus the extension, + is given. Also, now a *cmd* with a matching ``PATHEXT`` extension will + be returned prior to one fully matching, if the fully matching one is + found later in *path*. + .. exception:: Error This exception collects exceptions that are raised during a multi-file From 255e4ffebb96be7c78af575c8c4f5ad84cfa4591 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 12:21:30 -0700 Subject: [PATCH 16/31] Add another test --- Lib/test/test_shutil.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index bbd8f59327c88a..d9c50b90ddc771 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -2058,6 +2058,26 @@ def test_cwd_win32(self): # Current directory not on PATH self.assertIsNone(rv) + @unittest.skipUnless(sys.platform == "win32", + "test is for win32") + def test_pathext_match_before_path_full_match(self): + base_dir = pathlib.Path(os.fsdecode(self.dir)) + dir1 = base_dir / 'dir1' + dir2 = base_dir / 'dir2' + dir1.mkdir() + dir2.mkdir() + + pathext_match = dir1 / 'hello.com.exe' + path_match = dir2 / 'hello.com' + pathext_match.touch() + path_match.touch() + + test_path = os.pathsep.join([str(dir1), str(dir2)]) + assert os.path.basename(shutil.which( + 'hello.com', path=test_path, mode = os.F_OK + )).lower() == 'hello.com.exe' + + @os_helper.skip_if_dac_override def test_non_matching_mode(self): # Set the file read-only and ask for writeable files. From f52868d2e0ba8ac2ea360f38f420ed6900181957 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 13:57:18 -0700 Subject: [PATCH 17/31] Update Doc/library/shutil.rst Co-authored-by: Steve Dower --- Doc/library/shutil.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index 4003324bbe5903..a2e0b008238496 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -461,13 +461,13 @@ Directory and files operations :class:`bytes`, the result type is also :class:`bytes`. .. versionchanged:: 3.12 - On Windows: ``NeedCurrentDirectoryForExePathW`` will be consulted - for non- ``os.X_OK`` modes to determine if the current working directory - should be prepended to *path*. Additionally, the ``PATHEXT`` environment - variable is now consulted when a full path to a cmd, minus the extension, - is given. Also, now a *cmd* with a matching ``PATHEXT`` extension will - be returned prior to one fully matching, if the fully matching one is - found later in *path*. + On Windows, queries allowing executables (``os.X_OK``) will now + consult ``NeedCurrentDirectoryForExePathW`` to determine if the + current working directory should be searched first. Additionally, + the ``PATHEXT`` environment variable is now consulted even when + *cmd* includes an extension. Finally, a *cmd* found with a + ``PATHEXT`` extension in an earlier directory from *path* will + now be returned ahead of any match found later in *path*. .. exception:: Error From 6e9269fbdcd3140ac6eff1fa77ed2233f994b3d7 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 13:58:19 -0700 Subject: [PATCH 18/31] rewording --- Doc/library/shutil.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index a2e0b008238496..a2ddad89314fc8 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -439,8 +439,8 @@ Directory and files operations consulted to determine if the current directory should be prepended to *path*. - Additionally, when finding the *cmd* in the *path*, the ``PATHEXT`` - environment variable is checked. For example, if you call + Also on Windows, the ``PATHEXT`` variable is used to resolve commands + that may not already include an extension. For example, if you call ``shutil.which("python")``, :func:`which` will search ``PATHEXT`` to know that it should look for ``python.exe`` within the *path* directories. For example, on Windows:: From a6b7eabf94d34a3346cf1c53fdeeb2c8e62d9c1b Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 13:59:10 -0700 Subject: [PATCH 19/31] Rewording --- Doc/library/shutil.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index a2ddad89314fc8..db4304abc57e45 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -465,7 +465,7 @@ Directory and files operations consult ``NeedCurrentDirectoryForExePathW`` to determine if the current working directory should be searched first. Additionally, the ``PATHEXT`` environment variable is now consulted even when - *cmd* includes an extension. Finally, a *cmd* found with a + *cmd* includes a directory component. Finally, a *cmd* found with a ``PATHEXT`` extension in an earlier directory from *path* will now be returned ahead of any match found later in *path*. From 7480daa80bf531a552cc96a6d98e62e9064d5856 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 14:03:11 -0700 Subject: [PATCH 20/31] Reword whats new --- Doc/whatsnew/3.12.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst index 4d7193debbe32a..a90e2d8e4b4f03 100644 --- a/Doc/whatsnew/3.12.rst +++ b/Doc/whatsnew/3.12.rst @@ -344,9 +344,19 @@ shutil (Contributed by Irit Katriel in :gh:`102828`.) * :func:`shutil.which` now consults the *PATHEXT* environment variable to - find matches within *PATH* on Windows. + find matches within *PATH* on Windows even when the given *cmd* includes + a directory component. (Contributed by Charles Machalow in :gh:`103179`.) + :func:`shutil.which` will call ``NeedCurrentDirectoryForExePathW`` when + querying for executables on Windows to determine if the current working + directory should be prepended to the search path. + (Contributed by Charles Machalow in :gh:`103179`.) + + :func:`shutil.which` will return a path matching the *cmd* with a component + from ``PATHEXT`` prior to a direct match elsewhere in the search path on + Windows. + (Contributed by Charles Machalow in :gh:`103179`.) sqlite3 ------- From a48260c3716b1aac773567a3f1d25ddbf508a22f Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 14:04:18 -0700 Subject: [PATCH 21/31] Clarify --- Doc/library/shutil.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index db4304abc57e45..e345a9f400e529 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -449,7 +449,7 @@ Directory and files operations 'C:\\Python33\\python.EXE' Similar ``PATHEXT`` logic is also applied when a full path to a *cmd* is - given, though without an extension:: + given, containing a directory component:: >> shutil.which("C:\\Python33\\python") 'C:\\Python33\\python.EXE' From 6bb6f6c61032a4372a9bbac5e4bcdc466eafa4bf Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 14:16:12 -0700 Subject: [PATCH 22/31] Clarify how to not search cwd for exes --- Doc/library/shutil.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index e345a9f400e529..2129165a7034c9 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -437,7 +437,9 @@ Directory and files operations the *mode* does not include ``os.X_OK``. When the *mode* does include ``os.X_OK``, the Windows API ``NeedCurrentDirectoryForExePathW`` will be consulted to determine if the current directory should be prepended to - *path*. + *path*. To avoid consulting the current working directory for executables, + and thereby, match the behavior of non-Windows shells: set the environment + variable ``NoDefaultCurrentDirectoryInExePath``. Also on Windows, the ``PATHEXT`` variable is used to resolve commands that may not already include an extension. For example, if you call From b5f3eba5db7a4911d74292125122cd2d97074da3 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 15:16:44 -0700 Subject: [PATCH 23/31] Doc updates --- Doc/library/shutil.rst | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index 2129165a7034c9..baaa87c5529799 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -433,12 +433,11 @@ Directory and files operations When no *path* is specified, the results of :func:`os.environ` are used, returning either the "PATH" value or a fallback of :attr:`os.defpath`. - On Windows, the current directory is prepended to the *path* if - the *mode* does not include ``os.X_OK``. When the *mode* does include - ``os.X_OK``, the Windows API ``NeedCurrentDirectoryForExePathW`` will be - consulted to determine if the current directory should be prepended to - *path*. To avoid consulting the current working directory for executables, - and thereby, match the behavior of non-Windows shells: set the environment + On Windows, the current directory is prepended to the *path* if *mode* does + not include ``os.X_OK``. When the *mode* does include ``os.X_OK``, the + Windows API ``NeedCurrentDirectoryForExePathW`` will be consulted to + determine if the current directory should be prepended to *path*. To avoid + consulting the current working directory for executables: set the environment variable ``NoDefaultCurrentDirectoryInExePath``. Also on Windows, the ``PATHEXT`` variable is used to resolve commands @@ -450,8 +449,8 @@ Directory and files operations >>> shutil.which("python") 'C:\\Python33\\python.EXE' - Similar ``PATHEXT`` logic is also applied when a full path to a *cmd* is - given, containing a directory component:: + This is also applied when *cmd* is a path that contains a directory + component:: >> shutil.which("C:\\Python33\\python") 'C:\\Python33\\python.EXE' From 3bf4b8d540cfdd707214486012ae5ceb488143dc Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 15:51:45 -0700 Subject: [PATCH 24/31] Update Doc/library/shutil.rst Co-authored-by: Eryk Sun --- Doc/library/shutil.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index baaa87c5529799..b20266eb509dde 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -462,13 +462,13 @@ Directory and files operations :class:`bytes`, the result type is also :class:`bytes`. .. versionchanged:: 3.12 - On Windows, queries allowing executables (``os.X_OK``) will now - consult ``NeedCurrentDirectoryForExePathW`` to determine if the - current working directory should be searched first. Additionally, - the ``PATHEXT`` environment variable is now consulted even when - *cmd* includes a directory component. Finally, a *cmd* found with a - ``PATHEXT`` extension in an earlier directory from *path* will - now be returned ahead of any match found later in *path*. + .. versionchanged:: 3.12 + On Windows, the current directory is no longer prepended to the search + path if *mode* includes ``os.X_OK`` and WinAPI + ``NeedCurrentDirectoryForExePathW(cmd)`` is false; ``PATHEXT`` is used + now even when *cmd* includes a directory component or ends with an + extension that is in ``PATHEXT``; and filenames that have no extension + can be found now. .. exception:: Error From be73608145ce3be2d577c4c55e1e17e25393c134 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Mon, 3 Apr 2023 15:53:18 -0700 Subject: [PATCH 25/31] Update 2023-04-02-22-04-26.gh-issue-75586.526iJm.rst --- .../next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst b/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst index 744a9163243906..8ec568ec4e4775 100644 --- a/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst +++ b/Misc/NEWS.d/next/Library/2023-04-02-22-04-26.gh-issue-75586.526iJm.rst @@ -1 +1 @@ -Fix an issue with ``shutil.which`` on Windows where PATHEXT was not consulted when checking for a match in PATH. +Fix various Windows-specific issues with ``shutil.which``. From 0169ba90ddb012b232c9c3e327656314f4b28547 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Tue, 4 Apr 2023 09:52:43 -0700 Subject: [PATCH 26/31] Update Doc/library/shutil.rst Co-authored-by: Steve Dower --- Doc/library/shutil.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index b20266eb509dde..c5cd73c5e61b4a 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -461,7 +461,6 @@ Directory and files operations The :class:`bytes` type is now accepted. If *cmd* type is :class:`bytes`, the result type is also :class:`bytes`. - .. versionchanged:: 3.12 .. versionchanged:: 3.12 On Windows, the current directory is no longer prepended to the search path if *mode* includes ``os.X_OK`` and WinAPI From 499d2de9a9f347c4b7a09325133bc12a2408bbdb Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Tue, 4 Apr 2023 13:16:28 -0700 Subject: [PATCH 27/31] Update Lib/shutil.py Co-authored-by: Steve Dower --- Lib/shutil.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index a2fa7f33f3877b..8b378645a5a375 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1508,8 +1508,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): curdir = os.curdir if use_bytes: curdir = os.fsencode(curdir) - if curdir not in path: - path.insert(0, curdir) + path.insert(0, curdir) if sys.platform == "win32": # PATHEXT is necessary to check on Windows. From 3ead78093047b3f189ac2d11310de56fe8004d39 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Tue, 4 Apr 2023 13:26:38 -0700 Subject: [PATCH 28/31] Add test for behavior --- Lib/test/test_shutil.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index d9c50b90ddc771..9eaf167a9fa3c9 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -2058,6 +2058,25 @@ def test_cwd_win32(self): # Current directory not on PATH self.assertIsNone(rv) + @unittest.skipUnless(sys.platform == "win32", + "test is for win32") + def test_cwd_win32_added_before_all_other_path(self): + base_dir = pathlib.Path(os.fsdecode(self.dir)) + + elsewhere_in_path_dir = base_dir / 'dir1' + elsewhere_in_path_dir.mkdir() + match_elsewhere_in_path = elsewhere_in_path_dir / 'hello.exe' + match_elsewhere_in_path.touch() + + exe_in_cwd = base_dir / 'hello.exe' + exe_in_cwd.touch() + + with os_helper.change_cwd(path=base_dir): + with unittest.mock.patch('shutil._win_path_needs_curdir', return_value=True): + rv = shutil.which('hello.exe', path=elsewhere_in_path_dir) + + self.assertEqual(os.path.abspath(rv), os.path.abspath(exe_in_cwd)) + @unittest.skipUnless(sys.platform == "win32", "test is for win32") def test_pathext_match_before_path_full_match(self): @@ -2077,7 +2096,6 @@ def test_pathext_match_before_path_full_match(self): 'hello.com', path=test_path, mode = os.F_OK )).lower() == 'hello.com.exe' - @os_helper.skip_if_dac_override def test_non_matching_mode(self): # Set the file read-only and ask for writeable files. From f9267da406cbd8f67bd80f3e12b80d11afd7d633 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Tue, 4 Apr 2023 13:40:00 -0700 Subject: [PATCH 29/31] kick ci From d1e68ff4cf6c450def9e536fea00bece69efac77 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Tue, 4 Apr 2023 13:49:48 -0700 Subject: [PATCH 30/31] Mention cwd first behavior --- Doc/library/shutil.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index c5cd73c5e61b4a..6a3f3b769d789e 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -464,9 +464,10 @@ Directory and files operations .. versionchanged:: 3.12 On Windows, the current directory is no longer prepended to the search path if *mode* includes ``os.X_OK`` and WinAPI - ``NeedCurrentDirectoryForExePathW(cmd)`` is false; ``PATHEXT`` is used - now even when *cmd* includes a directory component or ends with an - extension that is in ``PATHEXT``; and filenames that have no extension + ``NeedCurrentDirectoryForExePathW(cmd)`` is false. When the current + directory is a prepended, it is prepended before the *path*. ``PATHEXT`` + is used now even when *cmd* includes a directory component or ends with + an extension that is in ``PATHEXT``; and filenames that have no extension can be found now. .. exception:: Error From 9badf8cf62d59912f8c32ce616f49bca8712a2b6 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Tue, 4 Apr 2023 13:50:34 -0700 Subject: [PATCH 31/31] Wording --- Doc/library/shutil.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index 6a3f3b769d789e..373cc7d6072031 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -464,11 +464,11 @@ Directory and files operations .. versionchanged:: 3.12 On Windows, the current directory is no longer prepended to the search path if *mode* includes ``os.X_OK`` and WinAPI - ``NeedCurrentDirectoryForExePathW(cmd)`` is false. When the current - directory is a prepended, it is prepended before the *path*. ``PATHEXT`` - is used now even when *cmd* includes a directory component or ends with - an extension that is in ``PATHEXT``; and filenames that have no extension - can be found now. + ``NeedCurrentDirectoryForExePathW(cmd)`` is false, else the current + directory is prepended even if it is already in the search path; + ``PATHEXT`` is used now even when *cmd* includes a directory component + or ends with an extension that is in ``PATHEXT``; and filenames that + have no extension can now be found. .. exception:: Error