Skip to content

Commit

Permalink
Behavior updated from pr comments
Browse files Browse the repository at this point in the history
  • Loading branch information
csm10495 committed Sep 30, 2023
1 parent d93e51f commit 4bed27c
Show file tree
Hide file tree
Showing 4 changed files with 19 additions and 10 deletions.
6 changes: 3 additions & 3 deletions Doc/library/shutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 12 additions & 5 deletions Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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)


Expand Down
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 4bed27c

Please sign in to comment.