Skip to content

Commit bb45a25

Browse files
committed
Release v0.2.0
- Change `directory.delete_contents` to `directory.delete` and accept glob patterns instead of names. - Add `directory.merge`
1 parent d7d5e90 commit bb45a25

File tree

2 files changed

+107
-26
lines changed

2 files changed

+107
-26
lines changed

pkg/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@ namespaces = true
1515

1616
# ----------------------------------------- Project Metadata -------------------------------------
1717
[project]
18-
version = "0.1.0"
18+
version = "0.2.0"
1919
name = "FileEx"
2020
requires-python = ">=3.10"

pkg/src/fileex/directory.py

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,127 @@
1-
from pathlib import Path as _Path
2-
import shutil as _shutil
1+
from pathlib import Path
2+
import shutil
3+
import os
34

4-
from fileex import exception as _exception
5+
from fileex import exception
56

7+
__all__ = ["delete_contents"]
68

7-
def delete_contents(
8-
path: str | _Path, exclude: list[str] | None = None, raise_existence: bool = True
9-
) -> list[str] | None:
10-
"""
11-
Delete all files and directories within a given directory,
12-
excluding those specified by `exclude`.
9+
10+
def delete(
11+
path: str | Path,
12+
exclude: list[str] | None = None,
13+
raise_existence: bool = True
14+
) -> tuple[list[Path], list[Path]] | tuple[None, None]:
15+
"""Recursively delete files and directories within a given directory, excluding those specified by `exclude`.
1316
1417
Parameters
1518
----------
16-
path : str | pathlib.Path
19+
path
1720
Path to the directory whose content should be deleted.
18-
exclude : list[str] | None, default: None
19-
List of file and directory names to exclude from deletion.
21+
exclude
22+
List of glob patterns to exclude from deletion.
23+
Patterns are relative to the directory specified by `path`.
24+
If a directory is excluded, all its contents will also be excluded.
2025
raise_existence : bool, default: True
2126
Raise an error when the directory does not exist.
2227
2328
Returns
2429
-------
25-
deleted_names : list[str] | None
26-
Names of the files and directories that were deleted,
30+
Paths of the files and directories that were deleted and excluded, respectively,
2731
or None if the directory does not exist and `raise_existence` is set to False.
2832
2933
Raises
3034
------
3135
fileex.exception.FileExPathNotFoundError
3236
If the directory does not exist and `raise_existence` is set to True.
3337
"""
34-
path = _Path(path)
38+
path = Path(path).resolve()
3539
if not path.is_dir():
3640
if raise_existence:
37-
raise _exception.FileExPathNotFoundError(path, is_dir=True)
38-
return
39-
if not exclude:
40-
exclude = []
41-
deleted_names = []
42-
for item in path.iterdir():
43-
if item.name not in exclude:
44-
deleted_names.append(item.name)
45-
item.unlink() if item.is_file() else _shutil.rmtree(item)
46-
return deleted_names
41+
raise exception.FileExPathNotFoundError(path, is_dir=True)
42+
return None, None
43+
44+
excluded_paths = set()
45+
for pattern in exclude or []:
46+
for excluded_path in path.glob(pattern):
47+
excluded_paths.add(excluded_path)
48+
if excluded_path.is_dir():
49+
# Also exclude all contents of the directory
50+
excluded_paths.update(excluded_path.rglob("*"))
51+
52+
deleted = []
53+
# Walk bottom-up so we can delete files before trying to delete directories
54+
for current_dir, dirs, files in os.walk(path, topdown=False):
55+
current_path = Path(current_dir)
56+
# Delete files
57+
for name in files:
58+
file_path = current_path / name
59+
if file_path.resolve() in excluded_paths:
60+
continue
61+
file_path.unlink()
62+
deleted.append(file_path)
63+
# Delete directories
64+
for name in dirs:
65+
dir_path = current_path / name
66+
if dir_path.resolve() in excluded_paths:
67+
continue
68+
shutil.rmtree(dir_path)
69+
deleted.append(dir_path)
70+
return deleted, list(excluded_paths)
71+
72+
73+
def merge(
74+
source: str | Path,
75+
destination: str | Path,
76+
raise_existence: bool = True
77+
) -> list[Path] | None:
78+
"""Recursively merge a directory into another.
79+
80+
All files and subdirectories in `source` will be moved to `destination`.
81+
Existing files in `destination` will be overwritten.
82+
83+
Parameters
84+
----------
85+
source
86+
Path to the source directory.
87+
destination
88+
Path to the destination directory.
89+
raise_existence
90+
Raise an error if the source directory does not exist.
91+
92+
Returns
93+
-------
94+
Paths of the files and directories that were moved,
95+
or None if the source directory does not exist and `raise_existence` is set to False.
96+
97+
Raises
98+
------
99+
fileex.exception.FileExPathNotFoundError
100+
If the source directory does not exist and `raise_existence` is set to True.
101+
"""
102+
source = Path(source).resolve()
103+
destination = Path(destination).resolve()
104+
moved_paths = []
105+
if not source.is_dir():
106+
if raise_existence:
107+
raise exception.FileExPathNotFoundError(source, is_dir=True)
108+
return None, None
109+
for item in source.iterdir():
110+
dest_item = destination / item.name
111+
if item.is_dir():
112+
if dest_item.exists():
113+
# Merge the subdirectory
114+
move_and_merge(item, dest_item)
115+
else:
116+
# Move the whole directory
117+
shutil.move(str(item), str(dest_item))
118+
moved_paths.append(dest_item)
119+
else:
120+
# Move or overwrite the file
121+
if dest_item.exists():
122+
# Remove the existing file
123+
dest_item.unlink()
124+
shutil.move(str(item), str(dest_item))
125+
moved_paths.append(dest_item)
126+
source.rmdir()
127+
return moved_paths

0 commit comments

Comments
 (0)