From e80568d0af6f8cb663e12c4c625e669445164c24 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Tue, 4 Jun 2024 17:36:45 +0200 Subject: [PATCH 01/11] Add a comment on when `NotADirectoryError`is raised by Path.unlink() --- Doc/library/pathlib.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index f37bb33321fa53..8acc4a5ceb9090 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1630,6 +1630,9 @@ example because the path doesn't exist). If *missing_ok* is true, :exc:`FileNotFoundError` exceptions will be ignored (same behavior as the POSIX ``rm -f`` command). + If an intermediate part of the path points to a file, + :exc:`NotADirectoryError` will be raised. + .. versionchanged:: 3.8 The *missing_ok* parameter was added. From 0db9ce0df8ca46191cd0c641063dd8f62d64309b Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Thu, 13 Jun 2024 16:20:40 +0200 Subject: [PATCH 02/11] don't prefer any specific OSError type in Path.unlink documentation --- Doc/library/pathlib.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 8acc4a5ceb9090..9d6aedb510e7bc 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1624,15 +1624,11 @@ example because the path doesn't exist). Remove this file or symbolic link. If the path points to a directory, use :func:`Path.rmdir` instead. - If *missing_ok* is false (the default), :exc:`FileNotFoundError` is - raised if the path does not exist. + This method propagates any :exc:`OSError` encountered during removal. If *missing_ok* is true, :exc:`FileNotFoundError` exceptions will be ignored (same behavior as the POSIX ``rm -f`` command). - If an intermediate part of the path points to a file, - :exc:`NotADirectoryError` will be raised. - .. versionchanged:: 3.8 The *missing_ok* parameter was added. From 2573a3a84dca7743e591b8e497938311fe09a9be Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Sun, 30 Jun 2024 14:10:46 +0200 Subject: [PATCH 03/11] maintain the original clarity that FileNotFound will be raised if missing_ok is false --- Doc/library/pathlib.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 9d6aedb510e7bc..6b83b1773d7efd 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1625,6 +1625,8 @@ example because the path doesn't exist). use :func:`Path.rmdir` instead. This method propagates any :exc:`OSError` encountered during removal. + For example: If *missing_ok* is false (the default), :exc:`FileNotFoundError` is + raised if the path does not exist. If *missing_ok* is true, :exc:`FileNotFoundError` exceptions will be ignored (same behavior as the POSIX ``rm -f`` command). From f3269d9fe94512d7dedab5d58c24d70c1cd767b2 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Sun, 30 Jun 2024 18:17:43 +0200 Subject: [PATCH 04/11] clarify that FileNotFoundError is a subclass of OSError --- Doc/library/pathlib.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 6b83b1773d7efd..bd088b33bc570f 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1624,12 +1624,12 @@ example because the path doesn't exist). Remove this file or symbolic link. If the path points to a directory, use :func:`Path.rmdir` instead. - This method propagates any :exc:`OSError` encountered during removal. - For example: If *missing_ok* is false (the default), :exc:`FileNotFoundError` is - raised if the path does not exist. + If *missing_ok* is false (the default), this method propagates any + :exc:`OSError` from the operating system, including :exc:`FileNotFoundError`. If *missing_ok* is true, :exc:`FileNotFoundError` exceptions will be - ignored (same behavior as the POSIX ``rm -f`` command). + ignored (same behavior as the POSIX ``rm -f`` command), any other + :exc:`OSError` which is encountered will continue to be propogated. .. versionchanged:: 3.8 The *missing_ok* parameter was added. From 6286d808b30bd45b4caeb955663b79691f0d5aa6 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Sun, 30 Jun 2024 18:20:28 +0200 Subject: [PATCH 05/11] fix whitespace --- Doc/library/pathlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index bd088b33bc570f..f414da59756213 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1624,7 +1624,7 @@ example because the path doesn't exist). Remove this file or symbolic link. If the path points to a directory, use :func:`Path.rmdir` instead. - If *missing_ok* is false (the default), this method propagates any + If *missing_ok* is false (the default), this method propagates any :exc:`OSError` from the operating system, including :exc:`FileNotFoundError`. If *missing_ok* is true, :exc:`FileNotFoundError` exceptions will be From ce3577dd2b4a5de0f6d3330b4767494749c352a9 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Thu, 4 Jul 2024 11:55:34 +0000 Subject: [PATCH 06/11] ignore NotADirectoryError on missing_ok = True Semantically equivalent to FileNotFoundError - the file to be unlinked does not exist --- Doc/library/pathlib.rst | 9 +++++++-- Lib/pathlib/_local.py | 2 +- Lib/test/test_pathlib/test_pathlib.py | 7 +++++++ Lib/test/test_pathlib/test_pathlib_abc.py | 5 +++++ .../2024-07-04-11-55-15.gh-issue-119993._IXsOb.rst | 2 ++ 5 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2024-07-04-11-55-15.gh-issue-119993._IXsOb.rst diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index f414da59756213..5185fb46349a68 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1627,13 +1627,18 @@ example because the path doesn't exist). If *missing_ok* is false (the default), this method propagates any :exc:`OSError` from the operating system, including :exc:`FileNotFoundError`. - If *missing_ok* is true, :exc:`FileNotFoundError` exceptions will be - ignored (same behavior as the POSIX ``rm -f`` command), any other + If *missing_ok* is true, this shows similar behavior to the POSIX ``rm -f`` + command and any :exc:`FileNotFoundError` or :exc:`NotADirectoryError` + exceptions will be ignored. This means that the file does not exist after + execution, but cannot guarantee that the file did exist before. Any other :exc:`OSError` which is encountered will continue to be propogated. .. versionchanged:: 3.8 The *missing_ok* parameter was added. + .. versionchanged:: 3.?? + The *missing_ok* parameter will also ignore :exc:`NotADirectoryError` + .. _pathlib-pattern-language: diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py index 473fd525768b50..515c9166927323 100644 --- a/Lib/pathlib/_local.py +++ b/Lib/pathlib/_local.py @@ -793,7 +793,7 @@ def unlink(self, missing_ok=False): """ try: os.unlink(self) - except FileNotFoundError: + except (FileNotFoundError, NotADirectoryError): if not missing_ok: raise diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 3df354eb25a58c..51f1ac9441f35e 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -775,6 +775,13 @@ def test_unlink_missing_ok(self): self.assertFileNotFound(p.unlink) p.unlink(missing_ok=True) + def test_unlink_missing_ok_intermediate_file(self): + p = self.cls(self.base) / 'fileAAA' + p.touch() + p = p / 'fileBBB' + self.assertNotADirectory(p.unlink) + p.unlink(missing_ok=True) + def test_rmdir(self): p = self.cls(self.base) / 'dirA' for q in p.iterdir(): diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index 57cc1612c03468..4abc0425cf86d4 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -1591,6 +1591,11 @@ def assertFileNotFound(self, func, *args, **kwargs): func(*args, **kwargs) self.assertEqual(cm.exception.errno, errno.ENOENT) + def assertNotADirectory(self, func, *args, **kwargs): + with self.assertRaises(NotADirectoryError) as cm: + func(*args, **kwargs) + self.assertEqual(cm.exception.errno, errno.ENOTDIR) + def assertEqualNormCase(self, path_a, path_b): normcase = self.parser.normcase self.assertEqual(normcase(path_a), normcase(path_b)) diff --git a/Misc/NEWS.d/next/Library/2024-07-04-11-55-15.gh-issue-119993._IXsOb.rst b/Misc/NEWS.d/next/Library/2024-07-04-11-55-15.gh-issue-119993._IXsOb.rst new file mode 100644 index 00000000000000..4199422ccabca8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-07-04-11-55-15.gh-issue-119993._IXsOb.rst @@ -0,0 +1,2 @@ +:meth:`pathlib.Path.unlink` will also ignore any :exc:`NotADirectoryError` +if *missing_ok* is true. From a4cb84641953e090ea08b404ad973a631aba9e5c Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Fri, 5 Jul 2024 06:02:00 +0000 Subject: [PATCH 07/11] Windows doesn't raise NotADirectoryError --- Lib/test/test_pathlib/test_pathlib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 977bb119718c43..3ebdb8c61cdd35 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -793,7 +793,10 @@ def test_unlink_missing_ok_intermediate_file(self): p = self.cls(self.base) / 'fileAAA' p.touch() p = p / 'fileBBB' - self.assertNotADirectory(p.unlink) + if sys.platform.startswith("win"): + self.assertFileNotFound(p.unlink) + else: + self.assertNotADirectory(p.unlink) p.unlink(missing_ok=True) def test_rmdir(self): From 9b4d22b55b6e407b53bb2f1bca995bbfa09f206e Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Fri, 6 Sep 2024 15:26:00 +0000 Subject: [PATCH 08/11] make the documentation more succinct implements comments from @barneygale --- Doc/library/pathlib.rst | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 2f5a7959cbd7f1..42790697ed7f18 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1631,20 +1631,18 @@ Copying, renaming and deleting use :func:`Path.rmdir` instead. If *missing_ok* is false (the default), this method propagates any - :exc:`OSError` from the operating system, including :exc:`FileNotFoundError`. + :exc:`OSError` from the operating system. - If *missing_ok* is true, this shows similar behavior to the POSIX ``rm -f`` - command and any :exc:`FileNotFoundError` or :exc:`NotADirectoryError` - exceptions will be ignored. This means that the file does not exist after - execution, but cannot guarantee that the file did exist before. Any other - :exc:`OSError` which is encountered will continue to be propogated. + If *missing_ok* is true, :exc:`FileNotFoundError` and + :exc:`NotADirectoryError` exceptions will be ignored. This behavior is + similar to the POSIX ``rm -f`` command. .. versionchanged:: 3.8 The *missing_ok* parameter was added. - .. versionchanged:: 3.?? - The *missing_ok* parameter will also ignore :exc:`NotADirectoryError` - + .. versionchanged:: 3.14 + Suppresses :exc:`NotADirectoryError` exceptions when *missing_ok* is + true. .. method:: Path.rmdir() From 84eeea30245cbfdc231ba8996e5eacea6f42a1fe Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Fri, 6 Sep 2024 15:34:21 +0000 Subject: [PATCH 09/11] also run `test_unlink_missing_ok_intermediate_file` against `DummyPath` --- Lib/test/test_pathlib/test_pathlib_abc.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index 495f9d52e01032..943294e92f8287 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -3,6 +3,7 @@ import os import errno import stat +import sys import unittest from pathlib._os import UnsupportedOperation @@ -2638,6 +2639,16 @@ def test_unlink_missing_ok(self): self.assertFileNotFound(p.unlink) p.unlink(missing_ok=True) + def test_unlink_missing_ok_intermediate_file(self): + p = self.cls(self.base) / 'fileAAA' + p.touch() + p = p / 'fileBBB' + if sys.platform.startswith("win"): + self.assertFileNotFound(p.unlink) + else: + self.assertNotADirectory(p.unlink) + p.unlink(missing_ok=True) + def test_rmdir(self): p = self.cls(self.base) / 'dirA' for q in p.iterdir(): From 626af70ebc3dedaca21d93fdb5bab9b8c8d882a4 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Wed, 11 Sep 2024 15:46:42 +0000 Subject: [PATCH 10/11] DummyPath.unlink raises NotADirectoryError --- Lib/test/test_pathlib/test_pathlib_abc.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index 943294e92f8287..2da269478a1697 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -1493,6 +1493,10 @@ def open(self, mode='r', buffering=-1, encoding=None, stream = io.TextIOWrapper(stream, encoding=encoding, errors=errors, newline=newline) return stream + def touch(self): + with self.open('w'): + pass + def iterdir(self): path = str(self.resolve()) if path in self._files: @@ -1524,6 +1528,9 @@ def unlink(self, missing_ok=False): path = str(path_obj) name = path_obj.name parent = str(path_obj.parent) + for interim_path in self.parents: + if str(interim_path) in self._files and not missing_ok: + raise NotADirectoryError(errno.ENOTDIR, "Not a directory", path) if path in self._directories: raise IsADirectoryError(errno.EISDIR, "Is a directory", path) elif path in self._files: @@ -1535,6 +1542,7 @@ def unlink(self, missing_ok=False): elif not missing_ok: raise FileNotFoundError(errno.ENOENT, "File not found", path) + def rmdir(self): path_obj = self.parent.resolve(strict=True) / self.name path = str(path_obj) From 22faf979e78df7acb906f69b3ff515e4f50ad0b2 Mon Sep 17 00:00:00 2001 From: Mike Foster Date: Fri, 24 Jan 2025 08:41:48 +0000 Subject: [PATCH 11/11] Only test on posix --- Lib/test/test_pathlib/test_pathlib.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 3ebdb8c61cdd35..7e5d2a02b65270 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -789,14 +789,13 @@ def test_unlink_missing_ok(self): self.assertFileNotFound(p.unlink) p.unlink(missing_ok=True) + #Windows will raise FileNotFoundError - handling already tested above. + @needs_posix def test_unlink_missing_ok_intermediate_file(self): p = self.cls(self.base) / 'fileAAA' p.touch() p = p / 'fileBBB' - if sys.platform.startswith("win"): - self.assertFileNotFound(p.unlink) - else: - self.assertNotADirectory(p.unlink) + self.assertNotADirectory(p.unlink) p.unlink(missing_ok=True) def test_rmdir(self):