Skip to content

Commit 7f8ec52

Browse files
authored
pythonGH-127381: pathlib ABCs: remove PathBase.unlink() and rmdir() (python#127736)
Virtual filesystems don't always make a distinction between deleting files and empty directories, and sometimes support deleting non-empty directories in a single operation. Here we remove `PathBase.unlink()` and `rmdir()`, leaving `_delete()` as the sole deletion method, now made abstract. I hope to drop the underscore prefix later on.
1 parent 2367759 commit 7f8ec52

File tree

4 files changed

+48
-86
lines changed

4 files changed

+48
-86
lines changed

Lib/pathlib/_abc.py

+6-37
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,12 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
840840
dirs_exist_ok=dirs_exist_ok,
841841
preserve_metadata=preserve_metadata)
842842

843+
def _delete(self):
844+
"""
845+
Delete this file or directory (including all sub-directories).
846+
"""
847+
raise UnsupportedOperation(self._unsupported_msg('_delete()'))
848+
843849
def move(self, target):
844850
"""
845851
Recursively move this file or directory tree to the given destination.
@@ -874,43 +880,6 @@ def lchmod(self, mode):
874880
"""
875881
self.chmod(mode, follow_symlinks=False)
876882

877-
def unlink(self, missing_ok=False):
878-
"""
879-
Remove this file or link.
880-
If the path is a directory, use rmdir() instead.
881-
"""
882-
raise UnsupportedOperation(self._unsupported_msg('unlink()'))
883-
884-
def rmdir(self):
885-
"""
886-
Remove this directory. The directory must be empty.
887-
"""
888-
raise UnsupportedOperation(self._unsupported_msg('rmdir()'))
889-
890-
def _delete(self):
891-
"""
892-
Delete this file or directory (including all sub-directories).
893-
"""
894-
if self.is_symlink() or self.is_junction():
895-
self.unlink()
896-
elif self.is_dir():
897-
self._rmtree()
898-
else:
899-
self.unlink()
900-
901-
def _rmtree(self):
902-
def on_error(err):
903-
raise err
904-
results = self.walk(
905-
on_error=on_error,
906-
top_down=False, # So we rmdir() empty directories.
907-
follow_symlinks=False)
908-
for dirpath, _, filenames in results:
909-
for filename in filenames:
910-
filepath = dirpath / filename
911-
filepath.unlink()
912-
dirpath.rmdir()
913-
914883
def owner(self, *, follow_symlinks=True):
915884
"""
916885
Return the login name of the file owner.

Lib/pathlib/_local.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -846,10 +846,18 @@ def rmdir(self):
846846
"""
847847
os.rmdir(self)
848848

849-
def _rmtree(self):
850-
# Lazy import to improve module import time
851-
import shutil
852-
shutil.rmtree(self)
849+
def _delete(self):
850+
"""
851+
Delete this file or directory (including all sub-directories).
852+
"""
853+
if self.is_symlink() or self.is_junction():
854+
self.unlink()
855+
elif self.is_dir():
856+
# Lazy import to improve module import time
857+
import shutil
858+
shutil.rmtree(self)
859+
else:
860+
self.unlink()
853861

854862
def rename(self, target):
855863
"""

Lib/test/test_pathlib/test_pathlib.py

+19
Original file line numberDiff line numberDiff line change
@@ -1352,6 +1352,25 @@ def test_group_no_follow_symlinks(self):
13521352
self.assertEqual(expected_gid, gid_2)
13531353
self.assertEqual(expected_name, link.group(follow_symlinks=False))
13541354

1355+
def test_unlink(self):
1356+
p = self.cls(self.base) / 'fileA'
1357+
p.unlink()
1358+
self.assertFileNotFound(p.stat)
1359+
self.assertFileNotFound(p.unlink)
1360+
1361+
def test_unlink_missing_ok(self):
1362+
p = self.cls(self.base) / 'fileAAA'
1363+
self.assertFileNotFound(p.unlink)
1364+
p.unlink(missing_ok=True)
1365+
1366+
def test_rmdir(self):
1367+
p = self.cls(self.base) / 'dirA'
1368+
for q in p.iterdir():
1369+
q.unlink()
1370+
p.rmdir()
1371+
self.assertFileNotFound(p.stat)
1372+
self.assertFileNotFound(p.unlink)
1373+
13551374
@needs_symlinks
13561375
def test_delete_symlink(self):
13571376
tmp = self.cls(self.base, 'delete')

Lib/test/test_pathlib/test_pathlib_abc.py

+11-45
Original file line numberDiff line numberDiff line change
@@ -1370,8 +1370,6 @@ def test_unsupported_operation(self):
13701370
self.assertRaises(e, p.touch)
13711371
self.assertRaises(e, p.chmod, 0o755)
13721372
self.assertRaises(e, p.lchmod, 0o755)
1373-
self.assertRaises(e, p.unlink)
1374-
self.assertRaises(e, p.rmdir)
13751373
self.assertRaises(e, p.owner)
13761374
self.assertRaises(e, p.group)
13771375
self.assertRaises(e, p.as_uri)
@@ -1493,31 +1491,18 @@ def mkdir(self, mode=0o777, parents=False, exist_ok=False):
14931491
self.parent.mkdir(parents=True, exist_ok=True)
14941492
self.mkdir(mode, parents=False, exist_ok=exist_ok)
14951493

1496-
def unlink(self, missing_ok=False):
1497-
path = str(self)
1498-
name = self.name
1499-
parent = str(self.parent)
1500-
if path in self._directories:
1501-
raise IsADirectoryError(errno.EISDIR, "Is a directory", path)
1502-
elif path in self._files:
1503-
self._directories[parent].remove(name)
1504-
del self._files[path]
1505-
elif not missing_ok:
1506-
raise FileNotFoundError(errno.ENOENT, "File not found", path)
1507-
1508-
def rmdir(self):
1494+
def _delete(self):
15091495
path = str(self)
15101496
if path in self._files:
1511-
raise NotADirectoryError(errno.ENOTDIR, "Not a directory", path)
1512-
elif path not in self._directories:
1513-
raise FileNotFoundError(errno.ENOENT, "File not found", path)
1514-
elif self._directories[path]:
1515-
raise OSError(errno.ENOTEMPTY, "Directory not empty", path)
1516-
else:
1517-
name = self.name
1518-
parent = str(self.parent)
1519-
self._directories[parent].remove(name)
1497+
del self._files[path]
1498+
elif path in self._directories:
1499+
for name in list(self._directories[path]):
1500+
self.joinpath(name)._delete()
15201501
del self._directories[path]
1502+
else:
1503+
raise FileNotFoundError(errno.ENOENT, "File not found", path)
1504+
parent = str(self.parent)
1505+
self._directories[parent].remove(self.name)
15211506

15221507

15231508
class DummyPathTest(DummyPurePathTest):
@@ -2245,30 +2230,11 @@ def test_is_char_device_false(self):
22452230
self.assertIs((P / 'fileA\udfff').is_char_device(), False)
22462231
self.assertIs((P / 'fileA\x00').is_char_device(), False)
22472232

2248-
def test_unlink(self):
2249-
p = self.cls(self.base) / 'fileA'
2250-
p.unlink()
2251-
self.assertFileNotFound(p.stat)
2252-
self.assertFileNotFound(p.unlink)
2253-
2254-
def test_unlink_missing_ok(self):
2255-
p = self.cls(self.base) / 'fileAAA'
2256-
self.assertFileNotFound(p.unlink)
2257-
p.unlink(missing_ok=True)
2258-
2259-
def test_rmdir(self):
2260-
p = self.cls(self.base) / 'dirA'
2261-
for q in p.iterdir():
2262-
q.unlink()
2263-
p.rmdir()
2264-
self.assertFileNotFound(p.stat)
2265-
self.assertFileNotFound(p.unlink)
2266-
22672233
def test_delete_file(self):
22682234
p = self.cls(self.base) / 'fileA'
22692235
p._delete()
22702236
self.assertFileNotFound(p.stat)
2271-
self.assertFileNotFound(p.unlink)
2237+
self.assertFileNotFound(p._delete)
22722238

22732239
def test_delete_dir(self):
22742240
base = self.cls(self.base)
@@ -2347,7 +2313,7 @@ def setUp(self):
23472313

23482314
def tearDown(self):
23492315
base = self.cls(self.base)
2350-
base._rmtree()
2316+
base._delete()
23512317

23522318
def test_walk_topdown(self):
23532319
walker = self.walk_path.walk()

0 commit comments

Comments
 (0)