From 4bed27cb6e36ea525a29fb14125b36b8cc53e2e5 Mon Sep 17 00:00:00 2001 From: Charles Machalow Date: Fri, 29 Sep 2023 20:10:15 -0700 Subject: [PATCH] Behavior updated from pr comments --- Doc/library/shutil.rst | 6 +++--- Lib/shutil.py | 3 ++- Lib/test/test_shutil.py | 17 ++++++++++++----- ...23-09-24-06-04-14.gh-issue-109590.9EMofC.rst | 3 ++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst index 95a7b73fc3f28b..d1949d698f5614 100644 --- a/Doc/library/shutil.rst +++ b/Doc/library/shutil.rst @@ -477,9 +477,9 @@ Directory and files operations have no extension can now be found. .. versionchanged:: 3.12.1 - On Windows, if *mode* includes ``os.X_OK``, only executables with an - extension in ``PATHEXT`` will be returned. Therefore extension-less - files cannot be returned by :func:`shutil.which` by default on Windows. + On Windows, if *mode* includes ``os.X_OK``, executables with an + extension in ``PATHEXT`` will be preferred over executables without a + matching extension. This brings behavior closer to that of Python 3.11. .. exception:: Error diff --git a/Lib/shutil.py b/Lib/shutil.py index 86c3ff99eb824b..5bcfa563fea9af 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1560,9 +1560,10 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): # for a PATHEXT match. The first cmd is the direct match # (e.g. python.exe instead of python) # Check that direct match first if and only if the extension is in PATHEXT + # Otherwise check it last suffix = os.path.splitext(files[0])[1].upper() if mode & os.X_OK and not any(suffix == ext.upper() for ext in pathext): - del files[0] + files.append(files.pop(0)) 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. diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 20ea2e2d62de5a..d231e66b7b889f 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -2323,7 +2323,7 @@ def test_win_path_needs_curdir(self): # See GH-109590 @unittest.skipUnless(sys.platform == "win32", 'test specific to Windows') - def test_pathext_enforced_for_execute(self): + def test_pathext_preferred_for_execute(self): with os_helper.EnvironmentVarGuard() as env: env["PATH"] = self.temp_dir if isinstance(self.temp_dir, str) else self.temp_dir.decode() env["PATHEXT"] = ".test" @@ -2332,10 +2332,18 @@ def test_pathext_enforced_for_execute(self): open(exe, 'w').close() os.chmod(exe, 0o755) - # default does not match since .exe is not in PATHEXT - self.assertIsNone(shutil.which(self.to_text_type("test.exe"))) + # default behavior allows a direct match if nothing in PATHEXT matches + self.assertEqual(shutil.which(self.to_text_type("test.exe")), exe) + + dot_test = os.path.join(self.temp_dir, self.to_text_type("test.exe.test")) + open(dot_test, 'w').close() + os.chmod(dot_test, 0o755) - # but if we don't use os.X_OK we're ok not matching PATHEXT + # now we have a PATHEXT match, so it take precedence + self.assertEqual(shutil.which(self.to_text_type("test.exe")), dot_test) + + # but if we don't use os.X_OK we don't change the order based off PATHEXT + # and therefore get the direct match. self.assertEqual(shutil.which(self.to_text_type("test.exe"), mode=os.F_OK), exe) # See GH-109590 @@ -2355,7 +2363,6 @@ def test_pathext_given_extension_preferred(self): # even though .exe2 is preferred in PATHEXT, we matched directly to test.exe self.assertEqual(shutil.which(self.to_text_type("test.exe")), exe) - self.assertEqual(shutil.which(self.to_text_type("test")), exe2) diff --git a/Misc/NEWS.d/next/Library/2023-09-24-06-04-14.gh-issue-109590.9EMofC.rst b/Misc/NEWS.d/next/Library/2023-09-24-06-04-14.gh-issue-109590.9EMofC.rst index b08e32f10c345e..647e84e71b42d2 100644 --- a/Misc/NEWS.d/next/Library/2023-09-24-06-04-14.gh-issue-109590.9EMofC.rst +++ b/Misc/NEWS.d/next/Library/2023-09-24-06-04-14.gh-issue-109590.9EMofC.rst @@ -1,2 +1,3 @@ -:func:`shutil.which` will only match files with an extension in ``PATHEXT`` if the given mode includes ``os.X_OK`` on win32. +:func:`shutil.which` will prefer files with an extension in ``PATHEXT`` if the given mode includes ``os.X_OK`` on win32. +If no ``PATHEXT`` match is found, a file without an extension in ``PATHEXT`` can be returned. This change will have :func:`shutil.which` act more similarly to previous behavior in Python 3.11.