GH-75586: Make shutil.which() on Windows more consistent with the OS (GH-103179)

This commit is contained in:
Charles Machalow 2023-04-04 15:24:13 -07:00 committed by GitHub
parent f184abbdc9
commit 935aa45235
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 233 additions and 54 deletions

View file

@ -40,6 +40,9 @@ if os.name == 'posix':
elif _WINDOWS:
import nt
if sys.platform == 'win32':
import _winapi
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 +1452,16 @@ def _access_check(fn, mode):
and not os.path.isdir(fn))
def _win_path_needs_curdir(cmd, mode):
"""
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.
"""
return (not (mode & os.X_OK)) or _winapi.NeedCurrentDirectoryForExePath(
os.fsdecode(cmd))
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 +1472,54 @@ 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 os.path.dirname(cmd):
if _access_check(cmd, mode):
return cmd
return None
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 = 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 _win_path_needs_curdir(cmd, mode):
curdir = os.curdir
if use_bytes:
curdir = os.fsencode(curdir)
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 = [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.