From 2981e84f66e06e758a0eac2620a87ff85318722c Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Thu, 14 May 2020 17:30:03 +0200 Subject: [PATCH 01/12] shutil.which will not return None anymore for empty str in PATHEXT --- Lib/shutil.py | 2 +- Lib/test/test_shutil.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index a4ce2c0290bc93..71b39d39a82912 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1415,7 +1415,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): path.insert(0, curdir) # PATHEXT is necessary to check on Windows. - pathext = os.environ.get("PATHEXT", "").split(os.pathsep) + pathext = [ext for ext in os.environ.get("PATHEXT", "").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. diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index b9fdfd1350a096..f525d67047af4c 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1848,6 +1848,23 @@ def test_pathext(self): rv = shutil.which(program, path=self.temp_dir) self.assertEqual(rv, temp_filexyz.name) + @unittest.skipUnless(sys.platform == "win32", 'test specific to Windows') + def test_pathext_with_empty_str(self): + ext = ".xyz" + temp_filexyz = tempfile.NamedTemporaryFile(dir=self.temp_dir, + prefix="Tmp2", suffix=ext) + os.chmod(temp_filexyz.name, stat.S_IXUSR) + self.addCleanup(temp_filexyz.close) + + # strip path and extension + program = os.path.basename(temp_filexyz.name) + program = os.path.splitext(program)[0] + + with support.EnvironmentVarGuard() as env: + env['PATHEXT'] = f"{ext};" # note the ; + rv = shutil.which(program, path=self.temp_dir) + self.assertEqual(rv, temp_filexyz.name) + class TestWhichBytes(TestWhich): def setUp(self): From 9df7b99bcf203e5cbd82e52419121887e1079860 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 14 May 2020 16:01:35 +0000 Subject: [PATCH 02/12] =?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 --- .../NEWS.d/next/Library/2020-05-14-16-01-34.bpo-40592.Cmk855.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2020-05-14-16-01-34.bpo-40592.Cmk855.rst diff --git a/Misc/NEWS.d/next/Library/2020-05-14-16-01-34.bpo-40592.Cmk855.rst b/Misc/NEWS.d/next/Library/2020-05-14-16-01-34.bpo-40592.Cmk855.rst new file mode 100644 index 00000000000000..44dd5e2b38ed01 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-05-14-16-01-34.bpo-40592.Cmk855.rst @@ -0,0 +1 @@ +shutil.which returns not None anymore with ; at the end of PATHEXT \ No newline at end of file From 6dd95748ea478555614c5028e1ed8e1b52e2f553 Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Thu, 14 May 2020 19:35:44 +0200 Subject: [PATCH 03/12] Defaulted missing PATHEX env to cmd defaults --- Lib/shutil.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 71b39d39a82912..f8f8e1a3cb3b3d 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -52,6 +52,7 @@ COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024 _USE_CP_SENDFILE = hasattr(os, "sendfile") and sys.platform.startswith("linux") _HAS_FCOPYFILE = posix and hasattr(posix, "_fcopyfile") # macOS +_WIN_DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC" __all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2", "copytree", "move", "rmtree", "Error", "SpecialFileError", @@ -1415,7 +1416,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): path.insert(0, curdir) # PATHEXT is necessary to check on Windows. - pathext = [ext for ext in os.environ.get("PATHEXT", "").split(os.pathsep) if ext] + pathext = [ext for ext in os.environ.get("PATHEXT", _WIN_DEFAULT_PATHEXT).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. From 9eb17bb4aef95cde503e22c3fc79830d485d92dc Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Fri, 15 May 2020 08:38:02 +0200 Subject: [PATCH 04/12] Changed os.chmod from 'S_IXUSR' to 'S_IXUSR | S_IWUSR' --- Lib/test/test_shutil.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index f525d67047af4c..8a59b80d97c37b 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1848,12 +1848,13 @@ def test_pathext(self): rv = shutil.which(program, path=self.temp_dir) self.assertEqual(rv, temp_filexyz.name) + # Issue 40592: See https://bugs.python.org/issue40592 @unittest.skipUnless(sys.platform == "win32", 'test specific to Windows') def test_pathext_with_empty_str(self): ext = ".xyz" temp_filexyz = tempfile.NamedTemporaryFile(dir=self.temp_dir, prefix="Tmp2", suffix=ext) - os.chmod(temp_filexyz.name, stat.S_IXUSR) + os.chmod(temp_filexyz.name, stat.S_IXUSR | stat.S_IWUSR) self.addCleanup(temp_filexyz.close) # strip path and extension From e6a9c2c78e154878cf0b722a12737e8a60569772 Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Fri, 15 May 2020 17:20:03 +0200 Subject: [PATCH 05/12] Added comment to explain the _WIN_DEFAULT_PATHEXT properly --- Lib/shutil.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/shutil.py b/Lib/shutil.py index f8f8e1a3cb3b3d..07eb8bf921b11b 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -52,6 +52,8 @@ COPY_BUFSIZE = 1024 * 1024 if _WINDOWS else 64 * 1024 _USE_CP_SENDFILE = hasattr(os, "sendfile") and sys.platform.startswith("linux") _HAS_FCOPYFILE = posix and hasattr(posix, "_fcopyfile") # macOS + +# CMD defaults in Windows 10 _WIN_DEFAULT_PATHEXT = ".COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC" __all__ = ["copyfileobj", "copyfile", "copymode", "copystat", "copy", "copy2", From 496695670b5ac2cfbfc46845f96d541991be40dd Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Tue, 19 May 2020 21:47:23 +0200 Subject: [PATCH 06/12] Updated change description Co-authored-by: Steve Dower --- .../next/Library/2020-05-14-16-01-34.bpo-40592.Cmk855.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2020-05-14-16-01-34.bpo-40592.Cmk855.rst b/Misc/NEWS.d/next/Library/2020-05-14-16-01-34.bpo-40592.Cmk855.rst index 44dd5e2b38ed01..3211a1bc345fa5 100644 --- a/Misc/NEWS.d/next/Library/2020-05-14-16-01-34.bpo-40592.Cmk855.rst +++ b/Misc/NEWS.d/next/Library/2020-05-14-16-01-34.bpo-40592.Cmk855.rst @@ -1 +1 @@ -shutil.which returns not None anymore with ; at the end of PATHEXT \ No newline at end of file +:func:`shutil.which` now ignores empty entries in :envvar:`PATHEXT` instead of treating them as a match. From cdb179e68243580064a41500b79e005840a23ec5 Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Tue, 19 May 2020 21:56:12 +0200 Subject: [PATCH 07/12] Adjusted list comprehension to adhere line length --- Lib/shutil.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 07eb8bf921b11b..c9c8c0733907ac 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1418,7 +1418,9 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): path.insert(0, curdir) # PATHEXT is necessary to check on Windows. - pathext = [ext for ext in os.environ.get("PATHEXT", _WIN_DEFAULT_PATHEXT).split(os.pathsep) if ext] + pathext = [ext for ext in + os.environ.get("PATHEXT", _WIN_DEFAULT_PATHEXT).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. From 01efb429a2b550fa0895f2f6b8e1ee07b501734b Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Tue, 19 May 2020 21:57:34 +0200 Subject: [PATCH 08/12] Removed chown from unittest --- Lib/test/test_shutil.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py index 8a59b80d97c37b..27997671a4331b 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1854,7 +1854,6 @@ def test_pathext_with_empty_str(self): ext = ".xyz" temp_filexyz = tempfile.NamedTemporaryFile(dir=self.temp_dir, prefix="Tmp2", suffix=ext) - os.chmod(temp_filexyz.name, stat.S_IXUSR | stat.S_IWUSR) self.addCleanup(temp_filexyz.close) # strip path and extension From fc87309eae7b72ba19341f5b7b8d08a8546f21a1 Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Wed, 21 Oct 2020 18:21:39 +0200 Subject: [PATCH 09/12] Empty PATHEXT () will now also be defaulted to _WIN_DEFAULT_PATHEXT --- Lib/shutil.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index c9c8c0733907ac..223e9a8a705064 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1418,9 +1418,9 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): path.insert(0, curdir) # PATHEXT is necessary to check on Windows. - pathext = [ext for ext in - os.environ.get("PATHEXT", _WIN_DEFAULT_PATHEXT).split( - os.pathsep) if ext] + 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. From 6af00e791c256045d8d31a93acaf83079b52e124 Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Wed, 21 Oct 2020 18:23:31 +0200 Subject: [PATCH 10/12] When no extension is found, we return a list with the command --- Lib/shutil.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 223e9a8a705064..18627ae22b236f 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1427,7 +1427,8 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): # 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): + if not pathext or any( + cmd.lower().endswith(ext.lower()) for ext in pathext): files = [cmd] else: files = [cmd + ext for ext in pathext] From ee618eb819d1c984cc79b1fa1a1a8a05a26bda79 Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Wed, 21 Oct 2020 19:00:29 +0200 Subject: [PATCH 11/12] Revert "When no extension is found, we return a list with the command" This reverts commit 6af00e791c256045d8d31a93acaf83079b52e124. --- Lib/shutil.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/shutil.py b/Lib/shutil.py index 18627ae22b236f..223e9a8a705064 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -1427,8 +1427,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): # 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 not pathext or any( - cmd.lower().endswith(ext.lower()) for ext in pathext): + if any(cmd.lower().endswith(ext.lower()) for ext in pathext): files = [cmd] else: files = [cmd + ext for ext in pathext] From 6c66dfc024d080c7ba3d2482b1652382df232f9e Mon Sep 17 00:00:00 2001 From: Christopher Marchfelder Date: Thu, 22 Oct 2020 22:32:02 +0200 Subject: [PATCH 12/12] Switched to from support to os_helper in unittest --- 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 460135893d29f3..890f2c7d406c24 100644 --- a/Lib/test/test_shutil.py +++ b/Lib/test/test_shutil.py @@ -1861,7 +1861,7 @@ def test_pathext_with_empty_str(self): program = os.path.basename(temp_filexyz.name) program = os.path.splitext(program)[0] - with support.EnvironmentVarGuard() as env: + with os_helper.EnvironmentVarGuard() as env: env['PATHEXT'] = f"{ext};" # note the ; rv = shutil.which(program, path=self.temp_dir) self.assertEqual(rv, temp_filexyz.name)