From 5111fa7b949b9adc2b0b336631f0440e107dc544 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 6 Dec 2021 22:10:59 +0800 Subject: [PATCH 01/10] Don't mask PermissionError with IsADirectoryError during tempdirectory cleanup Co-Authored-By: andrei kulakov --- Lib/tempfile.py | 9 ++++++++- Lib/test/test_tempfile.py | 10 ++++++++++ .../Library/2021-12-06-22-10-53.bpo-43153.J7mjSy.rst | 4 ++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2021-12-06-22-10-53.bpo-43153.J7mjSy.rst diff --git a/Lib/tempfile.py b/Lib/tempfile.py index 531cbf32f1283f..97bf4f1a9292a3 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -822,8 +822,15 @@ def resetperms(path): try: _os.unlink(path) + except IsADirectoryError: + cls._rmtree(path, ignore_errors=ignore_errors) # PermissionError is raised on FreeBSD for directories - except (IsADirectoryError, PermissionError): + except PermissionError: + # On WIndows, _rmtree will cause IsADirectoryError + if _os.name == 'nt' and _os.path.isfile(path): + if ignore_errors: + return + raise cls._rmtree(path, ignore_errors=ignore_errors) except FileNotFoundError: pass diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index 2b0ec46a103276..4b0578cfb03679 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -1460,6 +1460,16 @@ def test_explict_cleanup_ignore_errors(self): temp_path.exists(), f"TemporaryDirectory {temp_path!s} exists after cleanup") + @unittest.skipUnless(os.name == "nt", "Only on Windows.") + def test_explicit_cleanup_correct_error(self): + with tempfile.TemporaryDirectory() as working_dir: + temp_dir = self.do_create(dir=working_dir) + with open(os.path.join(temp_dir.name, "example.txt"), 'wb'): + # Used to raise IsADirectoryError on Windows. + with self.assertRaises(PermissionError): + temp_dir.cleanup() + + @os_helper.skip_unless_symlink def test_cleanup_with_symlink_to_a_directory(self): # cleanup() should not follow symlinks to directories (issue #12464) diff --git a/Misc/NEWS.d/next/Library/2021-12-06-22-10-53.bpo-43153.J7mjSy.rst b/Misc/NEWS.d/next/Library/2021-12-06-22-10-53.bpo-43153.J7mjSy.rst new file mode 100644 index 00000000000000..7800e0a4869adf --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-12-06-22-10-53.bpo-43153.J7mjSy.rst @@ -0,0 +1,4 @@ +On Windows, ``tempfile.TemporaryDirectory`` previously masked a +``PermissionError`` with ``NotADirectoryError`` during directory cleanup. It +now correctly raises ``PermissionError`` if errors are not ignored. Patch by +Andrei Kulakov and Ken Jin. From b3da7cd2f17ac0ee2fdb7b6f02b827baf1fcb23a Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 6 Dec 2021 22:13:21 +0800 Subject: [PATCH 02/10] fix Window spelling --- Lib/tempfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/tempfile.py b/Lib/tempfile.py index 97bf4f1a9292a3..7d270376649158 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -826,7 +826,7 @@ def resetperms(path): cls._rmtree(path, ignore_errors=ignore_errors) # PermissionError is raised on FreeBSD for directories except PermissionError: - # On WIndows, _rmtree will cause IsADirectoryError + # On Windows, _rmtree will cause IsADirectoryError if _os.name == 'nt' and _os.path.isfile(path): if ignore_errors: return From 0673393b59ac4fcd1bedcd2c1a40afdd80baba36 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 6 Dec 2021 22:44:53 +0800 Subject: [PATCH 03/10] improve comment --- Lib/tempfile.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/tempfile.py b/Lib/tempfile.py index 7d270376649158..a9db2e2d46d73f 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -826,7 +826,9 @@ def resetperms(path): cls._rmtree(path, ignore_errors=ignore_errors) # PermissionError is raised on FreeBSD for directories except PermissionError: - # On Windows, _rmtree will cause IsADirectoryError + # On Windows, calling _rmtree again will raise + # IsADirectoryError and mask the PermissionError. + # So we must re-raise the current PermissionError. if _os.name == 'nt' and _os.path.isfile(path): if ignore_errors: return From 98725c8f24c9dc4138d29eecdd350991cc246316 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Mon, 6 Dec 2021 23:32:00 +0800 Subject: [PATCH 04/10] fix comments IsADirectoryError -> NotADirectoryError --- Lib/tempfile.py | 2 +- Lib/test/test_tempfile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/tempfile.py b/Lib/tempfile.py index a9db2e2d46d73f..39927813dc12df 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -827,7 +827,7 @@ def resetperms(path): # PermissionError is raised on FreeBSD for directories except PermissionError: # On Windows, calling _rmtree again will raise - # IsADirectoryError and mask the PermissionError. + # NotADirectoryError and mask the PermissionError. # So we must re-raise the current PermissionError. if _os.name == 'nt' and _os.path.isfile(path): if ignore_errors: diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index 4b0578cfb03679..6ecb6a5dfeed91 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -1465,7 +1465,7 @@ def test_explicit_cleanup_correct_error(self): with tempfile.TemporaryDirectory() as working_dir: temp_dir = self.do_create(dir=working_dir) with open(os.path.join(temp_dir.name, "example.txt"), 'wb'): - # Used to raise IsADirectoryError on Windows. + # Used to raise NotADirectoryError on Windows. with self.assertRaises(PermissionError): temp_dir.cleanup() From 283df0acd0755b2afe2d2db12229e6907febebf0 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 7 Dec 2021 00:10:38 +0800 Subject: [PATCH 05/10] use os.path.isdir instead --- Lib/tempfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/tempfile.py b/Lib/tempfile.py index 39927813dc12df..0ada746f774b6b 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -829,7 +829,7 @@ def resetperms(path): # On Windows, calling _rmtree again will raise # NotADirectoryError and mask the PermissionError. # So we must re-raise the current PermissionError. - if _os.name == 'nt' and _os.path.isfile(path): + if _os.name == 'nt' and not _os.path.isdir(path): if ignore_errors: return raise From b9f074fc9fee9ab6115073c2c70ce8170e44b407 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 7 Dec 2021 21:26:32 +0800 Subject: [PATCH 06/10] add issue number to comments --- Lib/tempfile.py | 4 ++-- Lib/test/test_tempfile.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/tempfile.py b/Lib/tempfile.py index 0ada746f774b6b..340fbeb5f465da 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -826,8 +826,8 @@ def resetperms(path): cls._rmtree(path, ignore_errors=ignore_errors) # PermissionError is raised on FreeBSD for directories except PermissionError: - # On Windows, calling _rmtree again will raise - # NotADirectoryError and mask the PermissionError. + # bpo-43153: On Windows, calling _rmtree again will + # raise NotADirectoryError and mask the PermissionError. # So we must re-raise the current PermissionError. if _os.name == 'nt' and not _os.path.isdir(path): if ignore_errors: diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index 6ecb6a5dfeed91..cbeb5fcd5de353 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -1465,7 +1465,7 @@ def test_explicit_cleanup_correct_error(self): with tempfile.TemporaryDirectory() as working_dir: temp_dir = self.do_create(dir=working_dir) with open(os.path.join(temp_dir.name, "example.txt"), 'wb'): - # Used to raise NotADirectoryError on Windows. + # Used to raise NotADirectoryError on Windows. See bpo-43153. with self.assertRaises(PermissionError): temp_dir.cleanup() From b658bfc99921eb0b545a6acd16e46c7b12c226ca Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 7 Dec 2021 22:46:48 +0800 Subject: [PATCH 07/10] Remove windows check --- Lib/tempfile.py | 4 ++-- Lib/test/test_tempfile.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/tempfile.py b/Lib/tempfile.py index 340fbeb5f465da..6b74ae63bf6aa4 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -826,10 +826,10 @@ def resetperms(path): cls._rmtree(path, ignore_errors=ignore_errors) # PermissionError is raised on FreeBSD for directories except PermissionError: - # bpo-43153: On Windows, calling _rmtree again will + # bpo-43153: Calling _rmtree again may # raise NotADirectoryError and mask the PermissionError. # So we must re-raise the current PermissionError. - if _os.name == 'nt' and not _os.path.isdir(path): + if not _os.path.isdir(path): if ignore_errors: return raise diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index cbeb5fcd5de353..cb71de354eeb6c 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -1460,12 +1460,12 @@ def test_explict_cleanup_ignore_errors(self): temp_path.exists(), f"TemporaryDirectory {temp_path!s} exists after cleanup") - @unittest.skipUnless(os.name == "nt", "Only on Windows.") def test_explicit_cleanup_correct_error(self): with tempfile.TemporaryDirectory() as working_dir: temp_dir = self.do_create(dir=working_dir) with open(os.path.join(temp_dir.name, "example.txt"), 'wb'): - # Used to raise NotADirectoryError on Windows. See bpo-43153. + # Previously raised NotADirectoryError on some OSes + # (e.g. WIndows). See bpo-43153. with self.assertRaises(PermissionError): temp_dir.cleanup() From 11a31ac4c75e69310d6ac0e0a9d9b40875dc74c4 Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Tue, 7 Dec 2021 23:17:03 +0800 Subject: [PATCH 08/10] Skip test on other OSes except Windows --- Lib/test/test_tempfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_tempfile.py b/Lib/test/test_tempfile.py index cb71de354eeb6c..33b05f897291d2 100644 --- a/Lib/test/test_tempfile.py +++ b/Lib/test/test_tempfile.py @@ -1460,12 +1460,13 @@ def test_explict_cleanup_ignore_errors(self): temp_path.exists(), f"TemporaryDirectory {temp_path!s} exists after cleanup") + @unittest.skipUnless(os.name == "nt", "Only on Windows.") def test_explicit_cleanup_correct_error(self): with tempfile.TemporaryDirectory() as working_dir: temp_dir = self.do_create(dir=working_dir) with open(os.path.join(temp_dir.name, "example.txt"), 'wb'): # Previously raised NotADirectoryError on some OSes - # (e.g. WIndows). See bpo-43153. + # (e.g. Windows). See bpo-43153. with self.assertRaises(PermissionError): temp_dir.cleanup() From bfd39eaf1346ab7ea1eeda4e07d7d0b91bb4c0ec Mon Sep 17 00:00:00 2001 From: Fidget-Spinner <28750310+Fidget-Spinner@users.noreply.github.com> Date: Wed, 8 Dec 2021 23:33:18 +0800 Subject: [PATCH 09/10] Address Serhiy's code review --- Lib/tempfile.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/tempfile.py b/Lib/tempfile.py index 6b74ae63bf6aa4..b6130d8f749a15 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -824,12 +824,15 @@ def resetperms(path): _os.unlink(path) except IsADirectoryError: cls._rmtree(path, ignore_errors=ignore_errors) - # PermissionError is raised on FreeBSD for directories except PermissionError: + # The PermissionError handler was originally added for + # FreeBSD in directories, but it seems that it is raised + # on Windows too. # bpo-43153: Calling _rmtree again may # raise NotADirectoryError and mask the PermissionError. - # So we must re-raise the current PermissionError. - if not _os.path.isdir(path): + # So we must re-raise the current PermissionError if + # path is not a directory. + if _os.path.islink(path) or not _os.path.isdir(path): if ignore_errors: return raise From 77f5e8ac343715c216fb82c25684915c985ca979 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 5 Dec 2023 12:46:54 +0200 Subject: [PATCH 10/10] Test for junctions on Windows. --- Lib/tempfile.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Lib/tempfile.py b/Lib/tempfile.py index e2da4b9e5cb54d..55403ad1faf46d 100644 --- a/Lib/tempfile.py +++ b/Lib/tempfile.py @@ -41,6 +41,7 @@ import io as _io import os as _os import shutil as _shutil +import stat as _stat import errno as _errno from random import Random as _Random import sys as _sys @@ -899,7 +900,18 @@ def resetperms(path): # raise NotADirectoryError and mask the PermissionError. # So we must re-raise the current PermissionError if # path is not a directory. - if _os.path.islink(path) or not _os.path.isdir(path): + try: + st = _os.lstat(path) + except OSError: + if ignore_errors: + return + raise + if (_stat.S_ISLNK(st.st_mode) or + not _stat.S_ISDIR(st.st_mode) or + (hasattr(st, 'st_file_attributes') and + st.st_file_attributes & _stat.FILE_ATTRIBUTE_REPARSE_POINT and + st.st_reparse_tag == _stat.IO_REPARSE_TAG_MOUNT_POINT) + ): if ignore_errors: return raise