Commit 5680f654 authored by Cheryl Sabella's avatar Cheryl Sabella Committed by Victor Stinner

bpo-18283: Add support for bytes to shutil.which (GH-11818)

parent cfd31f0a
...@@ -396,6 +396,9 @@ Directory and files operations ...@@ -396,6 +396,9 @@ Directory and files operations
.. versionadded:: 3.3 .. 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`.
.. exception:: Error .. exception:: Error
......
...@@ -1279,6 +1279,15 @@ def get_terminal_size(fallback=(80, 24)): ...@@ -1279,6 +1279,15 @@ def get_terminal_size(fallback=(80, 24)):
return os.terminal_size((columns, lines)) return os.terminal_size((columns, lines))
# Check that a given file can be accessed with the correct mode.
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
return (os.path.exists(fn) and os.access(fn, mode)
and not os.path.isdir(fn))
def which(cmd, mode=os.F_OK | os.X_OK, path=None): def which(cmd, mode=os.F_OK | os.X_OK, path=None):
"""Given a command, mode, and a PATH string, return the path which """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 conforms to the given mode on the PATH, or None if there is no such
...@@ -1289,13 +1298,6 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): ...@@ -1289,13 +1298,6 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
path. path.
""" """
# Check that a given file can be accessed with the correct mode.
# Additionally check that `file` is not a directory, as on Windows
# directories pass the os.access check.
def _access_check(fn, mode):
return (os.path.exists(fn) and os.access(fn, mode)
and not os.path.isdir(fn))
# If we're given a path with a directory part, look it up directly rather # 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 # than referring to PATH directories. This includes checking relative to the
# current directory, e.g. ./script # current directory, e.g. ./script
...@@ -1304,19 +1306,31 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): ...@@ -1304,19 +1306,31 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
return cmd return cmd
return None return None
use_bytes = isinstance(cmd, bytes)
if path is None: if path is None:
path = os.environ.get("PATH", os.defpath) path = os.environ.get("PATH", os.defpath)
if not path: if not path:
return None return None
path = path.split(os.pathsep) if use_bytes:
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": if sys.platform == "win32":
# The current directory takes precedence on Windows. # The current directory takes precedence on Windows.
if not os.curdir in path: curdir = os.curdir
path.insert(0, os.curdir) if use_bytes:
curdir = os.fsencode(curdir)
if curdir not in path:
path.insert(0, curdir)
# PATHEXT is necessary to check on Windows. # PATHEXT is necessary to check on Windows.
pathext = os.environ.get("PATHEXT", "").split(os.pathsep) pathext = os.environ.get("PATHEXT", "").split(os.pathsep)
if use_bytes:
pathext = [os.fsencode(ext) for ext in pathext]
# See if the given file matches any of the expected path extensions. # See if the given file matches any of the expected path extensions.
# This will allow us to short circuit when given "python.exe". # This will allow us to short circuit when given "python.exe".
# If it does match, only test that one, otherwise we have to try # If it does match, only test that one, otherwise we have to try
......
...@@ -1517,6 +1517,9 @@ class TestWhich(unittest.TestCase): ...@@ -1517,6 +1517,9 @@ class TestWhich(unittest.TestCase):
os.chmod(self.temp_file.name, stat.S_IXUSR) os.chmod(self.temp_file.name, stat.S_IXUSR)
self.addCleanup(self.temp_file.close) self.addCleanup(self.temp_file.close)
self.dir, self.file = os.path.split(self.temp_file.name) self.dir, self.file = os.path.split(self.temp_file.name)
self.env_path = self.dir
self.curdir = os.curdir
self.ext = ".EXE"
def test_basic(self): def test_basic(self):
# Given an EXE in a directory, it should be returned. # Given an EXE in a directory, it should be returned.
...@@ -1549,7 +1552,7 @@ class TestWhich(unittest.TestCase): ...@@ -1549,7 +1552,7 @@ class TestWhich(unittest.TestCase):
rv = shutil.which(self.file, path=base_dir) rv = shutil.which(self.file, path=base_dir)
if sys.platform == "win32": if sys.platform == "win32":
# Windows: current directory implicitly on PATH # Windows: current directory implicitly on PATH
self.assertEqual(rv, os.path.join(os.curdir, self.file)) self.assertEqual(rv, os.path.join(self.curdir, self.file))
else: else:
# Other platforms: shouldn't match in the current directory. # Other platforms: shouldn't match in the current directory.
self.assertIsNone(rv) self.assertIsNone(rv)
...@@ -1581,11 +1584,11 @@ class TestWhich(unittest.TestCase): ...@@ -1581,11 +1584,11 @@ class TestWhich(unittest.TestCase):
# Ask for the file without the ".exe" extension, then ensure that # Ask for the file without the ".exe" extension, then ensure that
# it gets found properly with the extension. # it gets found properly with the extension.
rv = shutil.which(self.file[:-4], path=self.dir) rv = shutil.which(self.file[:-4], path=self.dir)
self.assertEqual(rv, self.temp_file.name[:-4] + ".EXE") self.assertEqual(rv, self.temp_file.name[:-4] + self.ext)
def test_environ_path(self): def test_environ_path(self):
with support.EnvironmentVarGuard() as env: with support.EnvironmentVarGuard() as env:
env['PATH'] = self.dir env['PATH'] = self.env_path
rv = shutil.which(self.file) rv = shutil.which(self.file)
self.assertEqual(rv, self.temp_file.name) self.assertEqual(rv, self.temp_file.name)
...@@ -1593,7 +1596,7 @@ class TestWhich(unittest.TestCase): ...@@ -1593,7 +1596,7 @@ class TestWhich(unittest.TestCase):
base_dir = os.path.dirname(self.dir) base_dir = os.path.dirname(self.dir)
with support.change_cwd(path=self.dir), \ with support.change_cwd(path=self.dir), \
support.EnvironmentVarGuard() as env: support.EnvironmentVarGuard() as env:
env['PATH'] = self.dir env['PATH'] = self.env_path
rv = shutil.which(self.file, path='') rv = shutil.which(self.file, path='')
self.assertIsNone(rv) self.assertIsNone(rv)
...@@ -1604,6 +1607,16 @@ class TestWhich(unittest.TestCase): ...@@ -1604,6 +1607,16 @@ class TestWhich(unittest.TestCase):
self.assertIsNone(rv) self.assertIsNone(rv)
class TestWhichBytes(TestWhich):
def setUp(self):
TestWhich.setUp(self)
self.dir = os.fsencode(self.dir)
self.file = os.fsencode(self.file)
self.temp_file.name = os.fsencode(self.temp_file.name)
self.curdir = os.fsencode(self.curdir)
self.ext = os.fsencode(self.ext)
class TestMove(unittest.TestCase): class TestMove(unittest.TestCase):
def setUp(self): def setUp(self):
......
Add support for bytes to :func:`shutil.which`.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment