kaparoo-python 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/PKG-INFO +9 -5
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/README.md +8 -4
- kaparoo_python-0.4.0/kaparoo/filesystem/README.md +230 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/__init__.py +15 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/directory.py +81 -2
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/classes.py +1 -1
- kaparoo_python-0.4.0/kaparoo/filesystem/staged.py +458 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/utils.py +144 -1
- kaparoo_python-0.4.0/kaparoo/utils/README.md +220 -0
- kaparoo_python-0.4.0/kaparoo/utils/__init__.py +41 -0
- kaparoo_python-0.4.0/kaparoo/utils/aggregate.py +342 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/utils/timer.py +146 -53
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/pyproject.toml +5 -5
- kaparoo_python-0.3.0/kaparoo/filesystem/README.md +0 -120
- kaparoo_python-0.3.0/kaparoo/utils/README.md +0 -121
- kaparoo_python-0.3.0/kaparoo/utils/__init__.py +0 -21
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/LICENSE +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/__init__.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/data/README.md +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/data/__init__.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/data/sequences/__init__.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/data/sequences/base.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/data/sequences/composers.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/data/sequences/templates.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/data/sequences/utils.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/exceptions.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/existence.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/README.md +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/__init__.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/deprecated.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/filters/__init__.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/filters/base.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/filters/logical.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/filters/multi_pattern.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/filters/pattern.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/filters/types.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/filters/utils.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/search/wrappers.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/filesystem/types.py +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/py.typed +0 -0
- {kaparoo_python-0.3.0 → kaparoo_python-0.4.0}/kaparoo/utils/optional.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kaparoo-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Personally common and useful Python features
|
|
5
5
|
Keywords: filesystem,pathlib,paths,utilities
|
|
6
6
|
Author: Jaewoo Park
|
|
@@ -51,8 +51,10 @@ Each submodule ships its own README with focused examples.
|
|
|
51
51
|
### [`kaparoo.filesystem`](https://github.com/kaparoo/kaparoo-python/tree/main/kaparoo/filesystem)
|
|
52
52
|
|
|
53
53
|
`pathlib`-based filesystem helpers: existence checks (`*_exists`),
|
|
54
|
-
`ensure_*` validators, `make_dir(s)
|
|
55
|
-
|
|
54
|
+
`ensure_*` validators, `make_dir(s)` (with a destructive `clean` reset
|
|
55
|
+
option), `dir_empty(s)`, `reserve_path(s)` guards for not-yet-existing
|
|
56
|
+
destinations, `StagedFile` / `StagedDirectory` for safe (atomic) writes,
|
|
57
|
+
path stringification, and a small exception hierarchy.
|
|
56
58
|
|
|
57
59
|
### [`kaparoo.filesystem.search`](https://github.com/kaparoo/kaparoo-python/tree/main/kaparoo/filesystem/search)
|
|
58
60
|
|
|
@@ -63,8 +65,10 @@ hook for custom filter kinds.
|
|
|
63
65
|
|
|
64
66
|
### [`kaparoo.utils`](https://github.com/kaparoo/kaparoo-python/tree/main/kaparoo/utils)
|
|
65
67
|
|
|
66
|
-
`Timer` / `
|
|
67
|
-
|
|
68
|
+
`Timer` / `SegmentTimer` context-manager-and-decorator timers (with
|
|
69
|
+
`lap`-split and `measure`-block timings); `Aggregator` for nested,
|
|
70
|
+
pluggable metric aggregation (the batch → epoch → run pattern); plus a
|
|
71
|
+
small family of helpers for working with `Optional[T]` values
|
|
68
72
|
(`replace_if_none`, `unwrap_or_default`, ...).
|
|
69
73
|
|
|
70
74
|
### [`kaparoo.data`](https://github.com/kaparoo/kaparoo-python/tree/main/kaparoo/data)
|
|
@@ -30,8 +30,10 @@ Each submodule ships its own README with focused examples.
|
|
|
30
30
|
### [`kaparoo.filesystem`](https://github.com/kaparoo/kaparoo-python/tree/main/kaparoo/filesystem)
|
|
31
31
|
|
|
32
32
|
`pathlib`-based filesystem helpers: existence checks (`*_exists`),
|
|
33
|
-
`ensure_*` validators, `make_dir(s)
|
|
34
|
-
|
|
33
|
+
`ensure_*` validators, `make_dir(s)` (with a destructive `clean` reset
|
|
34
|
+
option), `dir_empty(s)`, `reserve_path(s)` guards for not-yet-existing
|
|
35
|
+
destinations, `StagedFile` / `StagedDirectory` for safe (atomic) writes,
|
|
36
|
+
path stringification, and a small exception hierarchy.
|
|
35
37
|
|
|
36
38
|
### [`kaparoo.filesystem.search`](https://github.com/kaparoo/kaparoo-python/tree/main/kaparoo/filesystem/search)
|
|
37
39
|
|
|
@@ -42,8 +44,10 @@ hook for custom filter kinds.
|
|
|
42
44
|
|
|
43
45
|
### [`kaparoo.utils`](https://github.com/kaparoo/kaparoo-python/tree/main/kaparoo/utils)
|
|
44
46
|
|
|
45
|
-
`Timer` / `
|
|
46
|
-
|
|
47
|
+
`Timer` / `SegmentTimer` context-manager-and-decorator timers (with
|
|
48
|
+
`lap`-split and `measure`-block timings); `Aggregator` for nested,
|
|
49
|
+
pluggable metric aggregation (the batch → epoch → run pattern); plus a
|
|
50
|
+
small family of helpers for working with `Optional[T]` values
|
|
47
51
|
(`replace_if_none`, `unwrap_or_default`, ...).
|
|
48
52
|
|
|
49
53
|
### [`kaparoo.data`](https://github.com/kaparoo/kaparoo-python/tree/main/kaparoo/data)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# `kaparoo.filesystem`
|
|
2
|
+
|
|
3
|
+
`pathlib`-based filesystem helpers.
|
|
4
|
+
|
|
5
|
+
## Modules
|
|
6
|
+
|
|
7
|
+
- [`existence`](./existence.py) — boolean predicates (`*_exists`) and
|
|
8
|
+
validating `ensure_*` variants
|
|
9
|
+
- [`directory`](./directory.py) — `make_dir(s)`, `dir_empty(s)` /
|
|
10
|
+
`dir_not_empty(s)` with validation, plus `_unsafe` variants that skip
|
|
11
|
+
pre-checks
|
|
12
|
+
- [`utils`](./utils.py) — `stringify_path(s)`, `wrap_path(s)`,
|
|
13
|
+
`reserve_path(s)`
|
|
14
|
+
- [`staged`](./staged.py) — `StagedFile` / `StagedDirectory`, safe
|
|
15
|
+
(atomic) writers usable as a context manager or explicitly
|
|
16
|
+
- [`exceptions`](./exceptions.py) — `DirectoryNotFoundError`, `NotAFileError`
|
|
17
|
+
- [`types`](./types.py) — `StrPath`, `StrPaths` type aliases
|
|
18
|
+
- [`search/`](./search/) — composable filesystem search (own README)
|
|
19
|
+
|
|
20
|
+
All public symbols are re-exported from the top-level `kaparoo.filesystem`
|
|
21
|
+
namespace.
|
|
22
|
+
|
|
23
|
+
## Existence checks
|
|
24
|
+
|
|
25
|
+
`*_exists` return a bool; `ensure_*` raise on failure and return the
|
|
26
|
+
(optionally stringified) path.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from kaparoo.filesystem import (
|
|
30
|
+
dir_exists, ensure_dir_exists, ensure_files_exist, file_exists,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if file_exists("config.toml"):
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
# Single path: raises FileNotFoundError / NotAFileError / NotADirectoryError
|
|
37
|
+
config = ensure_dir_exists("var/cache", make=True) # create if missing
|
|
38
|
+
report = ensure_dir_exists("var/cache", make=0o755) # mode bits: POSIX only
|
|
39
|
+
|
|
40
|
+
# Bulk with a shared root; each entry is resolved relative to it.
|
|
41
|
+
files = ensure_files_exist(
|
|
42
|
+
["a.txt", "b.txt"],
|
|
43
|
+
root="data",
|
|
44
|
+
)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Exception hierarchy
|
|
48
|
+
|
|
49
|
+
`DirectoryNotFoundError` subclasses `FileNotFoundError`, so callers may
|
|
50
|
+
catch the broader type:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from kaparoo.filesystem import DirectoryNotFoundError, ensure_dir_exists
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
ensure_dir_exists("var/missing")
|
|
57
|
+
except FileNotFoundError: # catches DirectoryNotFoundError too
|
|
58
|
+
...
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Creating and emptying directories
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from kaparoo.filesystem import (
|
|
65
|
+
dir_empty, dir_not_empty, dirs_empty, make_dir, make_dirs,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
cache_dir = make_dir("var/cache", exist_ok=True)
|
|
69
|
+
|
|
70
|
+
# Start from a clean slate: wipe an existing directory's contents and
|
|
71
|
+
# recreate it empty. Destructive, and only ever wipes a *directory* (a
|
|
72
|
+
# non-directory at the path still raises). `clean=True` makes `exist_ok`
|
|
73
|
+
# moot, since the directory is removed and remade.
|
|
74
|
+
run_dir = make_dir("out/run_42", clean=True)
|
|
75
|
+
|
|
76
|
+
# Bulk creation with a shared root
|
|
77
|
+
make_dirs(["logs", "tmp"], root="var", exist_ok=True)
|
|
78
|
+
|
|
79
|
+
# Empty checks (raise if missing or not a directory)
|
|
80
|
+
assert dir_empty(cache_dir)
|
|
81
|
+
assert dirs_empty(["logs", "tmp"], root="var")
|
|
82
|
+
|
|
83
|
+
# ...and their negations
|
|
84
|
+
(cache_dir / "data.bin").touch()
|
|
85
|
+
assert dir_not_empty(cache_dir)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Each check has a negated counterpart (`dir_not_empty`, `dirs_not_empty`);
|
|
89
|
+
`dirs_not_empty` is True only when *every* directory is non-empty. The
|
|
90
|
+
`_unsafe` variants (`dir_empty_unsafe`, `dir_not_empty_unsafe`,
|
|
91
|
+
`dirs_empty_unsafe`, `dirs_not_empty_unsafe`) skip existence/type
|
|
92
|
+
validation and are intended for hot paths where the caller has already
|
|
93
|
+
validated.
|
|
94
|
+
|
|
95
|
+
## Path manipulation
|
|
96
|
+
|
|
97
|
+
`stringify_path(s)` converts to forward-slash strings, optionally
|
|
98
|
+
trimming a leading or trailing portion. `wrap_path(s)` prepends and/or
|
|
99
|
+
appends path components, rejecting absolute inputs where ambiguous.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from pathlib import Path
|
|
103
|
+
from kaparoo.filesystem import stringify_path, stringify_paths, wrap_path
|
|
104
|
+
|
|
105
|
+
# "path/to/file.txt" on every platform (including Windows)
|
|
106
|
+
stringify_path(Path("path") / "to" / "file.txt")
|
|
107
|
+
|
|
108
|
+
# Trim leading or trailing components
|
|
109
|
+
stringify_path("a/b/c", after="a") # "b/c"
|
|
110
|
+
stringify_path("a/b/c", before="c") # "a/b"
|
|
111
|
+
|
|
112
|
+
# Bulk stringify with a shared base.
|
|
113
|
+
stringify_paths(["data/a.txt", "data/b.txt"], after="data") # ["a.txt", "b.txt"]
|
|
114
|
+
|
|
115
|
+
# Compose paths without joining manually
|
|
116
|
+
wrap_path("logs", prepend="var", append="server.log") # var/logs/server.log
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Reserving a destination
|
|
120
|
+
|
|
121
|
+
`reserve_path` guards a path that should *not* yet exist, so you don't
|
|
122
|
+
clobber something when creating a new file or directory there. It only
|
|
123
|
+
checks (and optionally creates the parent) — it never creates or deletes
|
|
124
|
+
the target itself.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from kaparoo.filesystem import reserve_path
|
|
128
|
+
|
|
129
|
+
# Raises FileExistsError if out/run.json exists; otherwise creates the
|
|
130
|
+
# missing parent directory and returns the path ready to write to.
|
|
131
|
+
out = reserve_path("out/run.json", make_parents=True)
|
|
132
|
+
out.write_text("{}")
|
|
133
|
+
|
|
134
|
+
# `exist_ok` (named as in make_dir / Path.mkdir) is a non-destructive
|
|
135
|
+
# bypass: it suppresses the conflict but deletes nothing, so a later write
|
|
136
|
+
# overwrites in place.
|
|
137
|
+
out = reserve_path("out/run.json", exist_ok=True)
|
|
138
|
+
|
|
139
|
+
# `reserve_paths` is the bulk form (fail-fast on the first conflict). It
|
|
140
|
+
# takes no `root`; compose with `wrap_paths` to share a base directory.
|
|
141
|
+
from kaparoo.filesystem import reserve_paths, wrap_paths
|
|
142
|
+
a, b = reserve_paths(wrap_paths(["a.bin", "b.bin"], prepend="out"))
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
For a *directory* destination, `make_dir(..., exist_ok=...)` both guards
|
|
146
|
+
and creates it; for an exclusive *file* create, the stdlib `open(path,
|
|
147
|
+
"x")` raises the same `FileExistsError` directly. Reach for `reserve_path`
|
|
148
|
+
when you want the check (and parent setup) decoupled from the creation.
|
|
149
|
+
|
|
150
|
+
`reserve_path` is intentionally **non-destructive** — it never removes an
|
|
151
|
+
existing target. To start a directory from a clean slate, use the
|
|
152
|
+
`clean` option on `make_dir` / `make_dirs` (see below), which is the only
|
|
153
|
+
destructive operation here and is named to say so.
|
|
154
|
+
|
|
155
|
+
## Safe (atomic) writes
|
|
156
|
+
|
|
157
|
+
`StagedFile` saves a file safely: it stages the content in a temporary file
|
|
158
|
+
in the destination's own directory and moves it into place only on commit.
|
|
159
|
+
A reader never sees a half-written file, and a failed write leaves any
|
|
160
|
+
existing file untouched. It works as a context manager — commit on a clean
|
|
161
|
+
exit, discard on an exception — or explicitly, like a file object.
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from kaparoo.filesystem import StagedFile
|
|
165
|
+
|
|
166
|
+
# Text (the default), as a context manager: commit on success, discard
|
|
167
|
+
# on error.
|
|
168
|
+
with StagedFile("out/report.json", encoding="utf-8") as f:
|
|
169
|
+
f.write(json.dumps(data)) # an exception here leaves out/ untouched
|
|
170
|
+
|
|
171
|
+
# Binary mode, explicitly: write, then commit (or abort to discard).
|
|
172
|
+
f = StagedFile("out/data.bin", binary=True)
|
|
173
|
+
f.write(payload)
|
|
174
|
+
f.commit() # returns the destination Path; idempotent
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The default is text (`StagedFile[str]`) with optional `encoding` / `newline`,
|
|
178
|
+
as with `open`; pass `binary=True` for a binary writer (`StagedFile[bytes]`).
|
|
179
|
+
The type parameter follows the mode, so `write` and `file` are typed `str`
|
|
180
|
+
or `bytes` accordingly.
|
|
181
|
+
|
|
182
|
+
With `overwrite=False` (the default) an existing destination raises
|
|
183
|
+
`FileExistsError` up front, and the commit creates the file atomically —
|
|
184
|
+
never clobbering a file that appeared meanwhile. With `overwrite=True` the
|
|
185
|
+
destination is replaced in one atomic step, keeping its previous
|
|
186
|
+
permissions. Pass `make_parents=True` to create the destination's parent
|
|
187
|
+
directory if it is missing. An uncommitted writer (an explicit instance
|
|
188
|
+
dropped without `commit()`) discards its staged file on garbage collection,
|
|
189
|
+
so a partial write is never promoted by accident.
|
|
190
|
+
|
|
191
|
+
The committed file gets the usual umask-based permissions.
|
|
192
|
+
|
|
193
|
+
`StagedDirectory` is the directory counterpart: you populate its `workdir`
|
|
194
|
+
(a temporary directory in the destination's parent) and it is moved into
|
|
195
|
+
place on commit.
|
|
196
|
+
|
|
197
|
+
```python
|
|
198
|
+
from kaparoo.filesystem import StagedDirectory
|
|
199
|
+
|
|
200
|
+
with StagedDirectory("out/dataset", make_parents=True) as d:
|
|
201
|
+
(d.workdir / "train.json").write_text(payload)
|
|
202
|
+
(d.workdir / "shards").mkdir()
|
|
203
|
+
# out/dataset appears in one step; an exception would leave it absent
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Creating a new directory (`overwrite=False`) is atomic — a single rename.
|
|
207
|
+
Replacing an existing one (`overwrite=True`) is *not* fully atomic: the old
|
|
208
|
+
directory is swapped aside and removed, so there is a brief window where the
|
|
209
|
+
destination is absent and, on a rare failure mid-swap, the previous contents
|
|
210
|
+
remain in a sibling `<name>.old` directory for recovery.
|
|
211
|
+
|
|
212
|
+
## Platform notes
|
|
213
|
+
|
|
214
|
+
- **Directory mode bits**: `mode` (on `make_dir` / `make_dirs`) and
|
|
215
|
+
`make=<int>` (on `ensure_dir_exists` / `ensure_dirs_exist`) are
|
|
216
|
+
validated against the `0o1`–`0o7777` range and applied to the created
|
|
217
|
+
directory on **POSIX systems only**. On Windows, mode values are still
|
|
218
|
+
accepted (so cross-platform code stays clean) but the range check is
|
|
219
|
+
skipped and the OS ignores the bits — see
|
|
220
|
+
[`os.mkdir`](https://docs.python.org/3/library/os.html#os.mkdir).
|
|
221
|
+
- **Path separators**: `stringify_path` and `stringify_paths` normalize
|
|
222
|
+
backslashes to forward slashes on Windows. Functions that return
|
|
223
|
+
strings via `stringify=True` (`make_dir`, `ensure_dir_exists`,
|
|
224
|
+
`wrap_path`, ...) inherit this normalization. If you need a native
|
|
225
|
+
Windows path string, call `str(path)` directly on a `Path`.
|
|
226
|
+
|
|
227
|
+
## See also
|
|
228
|
+
|
|
229
|
+
- [`search/`](./search/) for filesystem traversal with filters
|
|
230
|
+
- [`kaparoo.utils`](../utils/) for `Timer` and Optional helpers
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
__all__ = (
|
|
2
2
|
"DirectoryNotFoundError",
|
|
3
3
|
"NotAFileError",
|
|
4
|
+
"StagedDirectory",
|
|
5
|
+
"StagedFile",
|
|
4
6
|
"dir_empty",
|
|
5
7
|
"dir_empty_unsafe",
|
|
6
8
|
"dir_exists",
|
|
9
|
+
"dir_not_empty",
|
|
10
|
+
"dir_not_empty_unsafe",
|
|
7
11
|
"dirs_empty",
|
|
8
12
|
"dirs_empty_unsafe",
|
|
9
13
|
"dirs_exist",
|
|
14
|
+
"dirs_not_empty",
|
|
15
|
+
"dirs_not_empty_unsafe",
|
|
10
16
|
"ensure_dir_exists",
|
|
11
17
|
"ensure_dirs_exist",
|
|
12
18
|
"ensure_file_exists",
|
|
@@ -22,6 +28,8 @@ __all__ = (
|
|
|
22
28
|
"make_dirs",
|
|
23
29
|
"path_exists",
|
|
24
30
|
"paths_exist",
|
|
31
|
+
"reserve_path",
|
|
32
|
+
"reserve_paths",
|
|
25
33
|
"search_dirs",
|
|
26
34
|
"search_files",
|
|
27
35
|
"search_paths",
|
|
@@ -34,8 +42,12 @@ __all__ = (
|
|
|
34
42
|
from kaparoo.filesystem.directory import (
|
|
35
43
|
dir_empty,
|
|
36
44
|
dir_empty_unsafe,
|
|
45
|
+
dir_not_empty,
|
|
46
|
+
dir_not_empty_unsafe,
|
|
37
47
|
dirs_empty,
|
|
38
48
|
dirs_empty_unsafe,
|
|
49
|
+
dirs_not_empty,
|
|
50
|
+
dirs_not_empty_unsafe,
|
|
39
51
|
make_dir,
|
|
40
52
|
make_dirs,
|
|
41
53
|
)
|
|
@@ -65,7 +77,10 @@ from kaparoo.filesystem.search import (
|
|
|
65
77
|
search_files,
|
|
66
78
|
search_paths,
|
|
67
79
|
)
|
|
80
|
+
from kaparoo.filesystem.staged import StagedDirectory, StagedFile
|
|
68
81
|
from kaparoo.filesystem.utils import (
|
|
82
|
+
reserve_path,
|
|
83
|
+
reserve_paths,
|
|
69
84
|
stringify_path,
|
|
70
85
|
stringify_paths,
|
|
71
86
|
wrap_path,
|
|
@@ -3,13 +3,18 @@ from __future__ import annotations
|
|
|
3
3
|
__all__ = (
|
|
4
4
|
"dir_empty",
|
|
5
5
|
"dir_empty_unsafe",
|
|
6
|
+
"dir_not_empty",
|
|
7
|
+
"dir_not_empty_unsafe",
|
|
6
8
|
"dirs_empty",
|
|
7
9
|
"dirs_empty_unsafe",
|
|
10
|
+
"dirs_not_empty",
|
|
11
|
+
"dirs_not_empty_unsafe",
|
|
8
12
|
"make_dir",
|
|
9
13
|
"make_dirs",
|
|
10
14
|
)
|
|
11
15
|
|
|
12
16
|
import os
|
|
17
|
+
import shutil
|
|
13
18
|
from pathlib import Path
|
|
14
19
|
from typing import TYPE_CHECKING, overload
|
|
15
20
|
|
|
@@ -39,6 +44,7 @@ def make_dir(
|
|
|
39
44
|
*,
|
|
40
45
|
mode: int = 0o777,
|
|
41
46
|
exist_ok: bool = False,
|
|
47
|
+
clean: bool = False,
|
|
42
48
|
stringify: Literal[False] = False,
|
|
43
49
|
) -> Path: ...
|
|
44
50
|
|
|
@@ -49,6 +55,7 @@ def make_dir(
|
|
|
49
55
|
*,
|
|
50
56
|
mode: int = 0o777,
|
|
51
57
|
exist_ok: bool = False,
|
|
58
|
+
clean: bool = False,
|
|
52
59
|
stringify: Literal[True],
|
|
53
60
|
) -> str: ...
|
|
54
61
|
|
|
@@ -59,6 +66,7 @@ def make_dir(
|
|
|
59
66
|
*,
|
|
60
67
|
mode: int = 0o777,
|
|
61
68
|
exist_ok: bool = False,
|
|
69
|
+
clean: bool = False,
|
|
62
70
|
stringify: bool,
|
|
63
71
|
) -> Path | str: ...
|
|
64
72
|
|
|
@@ -68,6 +76,7 @@ def make_dir(
|
|
|
68
76
|
*,
|
|
69
77
|
mode: int = 0o777,
|
|
70
78
|
exist_ok: bool = False,
|
|
79
|
+
clean: bool = False,
|
|
71
80
|
stringify: bool = False,
|
|
72
81
|
) -> Path | str:
|
|
73
82
|
"""Recursively create a directory.
|
|
@@ -77,6 +86,11 @@ def make_dir(
|
|
|
77
86
|
mode: The mode to use when creating the directory. Defaults to 0o777.
|
|
78
87
|
exist_ok: Whether to suppress OSError if the path already exists.
|
|
79
88
|
Defaults to False.
|
|
89
|
+
clean: Whether to recreate the directory empty when it already exists,
|
|
90
|
+
removing its contents first. Only an existing *directory* is wiped;
|
|
91
|
+
a non-directory still raises. Because the directory is removed and
|
|
92
|
+
remade, `clean=True` makes `exist_ok` moot. **Destructive.**
|
|
93
|
+
Defaults to False.
|
|
80
94
|
stringify: Whether to return the path as a string. Defaults to False.
|
|
81
95
|
|
|
82
96
|
Returns:
|
|
@@ -87,13 +101,16 @@ def make_dir(
|
|
|
87
101
|
ValueError: If `mode` is outside the range 0o1-0o7777
|
|
88
102
|
(not checked on Windows, where the mode is ignored).
|
|
89
103
|
NotADirectoryError: If the path exists but is not a directory.
|
|
90
|
-
OSError: If `exist_ok` is False and the path
|
|
104
|
+
OSError: If `exist_ok` is False, `clean` is False, and the path
|
|
105
|
+
already exists.
|
|
91
106
|
"""
|
|
92
107
|
_validate_mode(mode)
|
|
93
108
|
path = Path(path)
|
|
94
109
|
if path.exists() and not path.is_dir():
|
|
95
110
|
msg = f"not a directory: {path}"
|
|
96
111
|
raise NotADirectoryError(msg)
|
|
112
|
+
if clean and path.is_dir():
|
|
113
|
+
shutil.rmtree(path)
|
|
97
114
|
path.mkdir(mode=mode, parents=True, exist_ok=exist_ok)
|
|
98
115
|
return stringify_path(path) if stringify else path
|
|
99
116
|
|
|
@@ -105,6 +122,7 @@ def make_dirs(
|
|
|
105
122
|
root: StrPath | None = None,
|
|
106
123
|
mode: int = 0o777,
|
|
107
124
|
exist_ok: bool = False,
|
|
125
|
+
clean: bool = False,
|
|
108
126
|
stringify: Literal[False] = False,
|
|
109
127
|
) -> Sequence[Path]: ...
|
|
110
128
|
|
|
@@ -116,6 +134,7 @@ def make_dirs(
|
|
|
116
134
|
root: StrPath | None = None,
|
|
117
135
|
mode: int = 0o777,
|
|
118
136
|
exist_ok: bool = False,
|
|
137
|
+
clean: bool = False,
|
|
119
138
|
stringify: Literal[True],
|
|
120
139
|
) -> Sequence[str]: ...
|
|
121
140
|
|
|
@@ -127,6 +146,7 @@ def make_dirs(
|
|
|
127
146
|
root: StrPath | None = None,
|
|
128
147
|
mode: int = 0o777,
|
|
129
148
|
exist_ok: bool = False,
|
|
149
|
+
clean: bool = False,
|
|
130
150
|
stringify: bool,
|
|
131
151
|
) -> Sequence[Path] | Sequence[str]: ...
|
|
132
152
|
|
|
@@ -137,6 +157,7 @@ def make_dirs(
|
|
|
137
157
|
root: StrPath | None = None,
|
|
138
158
|
mode: int = 0o777,
|
|
139
159
|
exist_ok: bool = False,
|
|
160
|
+
clean: bool = False,
|
|
140
161
|
stringify: bool = False,
|
|
141
162
|
) -> Sequence[Path] | Sequence[str]:
|
|
142
163
|
"""Recursively create directories.
|
|
@@ -147,6 +168,11 @@ def make_dirs(
|
|
|
147
168
|
mode: The mode to use when creating the directories. Defaults to 0o777.
|
|
148
169
|
exist_ok: Whether to suppress OSError if any of the paths already exist.
|
|
149
170
|
Defaults to False.
|
|
171
|
+
clean: Whether to recreate each directory empty when it already exists,
|
|
172
|
+
removing its contents first. Only an existing *directory* is wiped;
|
|
173
|
+
a non-directory still raises. Because the directory is removed and
|
|
174
|
+
remade, `clean=True` makes `exist_ok` moot. **Destructive.**
|
|
175
|
+
Defaults to False.
|
|
150
176
|
stringify: Whether to return the paths as strings. Defaults to False.
|
|
151
177
|
|
|
152
178
|
Returns:
|
|
@@ -159,13 +185,16 @@ def make_dirs(
|
|
|
159
185
|
DirectoryNotFoundError: If `root` is provided and does not exist.
|
|
160
186
|
NotADirectoryError: If `root` is provided and is not a directory.
|
|
161
187
|
ValueError: If `root` is provided and any of the paths are absolute.
|
|
162
|
-
OSError: If `exist_ok` is False and any of the
|
|
188
|
+
OSError: If `exist_ok` is False, `clean` is False, and any of the
|
|
189
|
+
paths already exist.
|
|
163
190
|
OSError: If any of the paths are not directories.
|
|
164
191
|
"""
|
|
165
192
|
_validate_mode(mode)
|
|
166
193
|
paths = _join_root_if_provided(paths, root)
|
|
167
194
|
directories = [Path(p) for p in paths]
|
|
168
195
|
for directory in directories:
|
|
196
|
+
if clean and directory.is_dir():
|
|
197
|
+
shutil.rmtree(directory)
|
|
169
198
|
directory.mkdir(mode=mode, parents=True, exist_ok=exist_ok)
|
|
170
199
|
return stringify_paths(directories) if stringify else directories
|
|
171
200
|
|
|
@@ -224,3 +253,53 @@ def dirs_empty(paths: StrPaths, *, root: StrPath | None = None) -> bool:
|
|
|
224
253
|
"""
|
|
225
254
|
paths = ensure_dirs_exist(paths, root=root)
|
|
226
255
|
return all(dir_empty_unsafe(p) for p in paths)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def dir_not_empty_unsafe(path: StrPath) -> bool:
|
|
259
|
+
"""Check if a directory is not empty without existence checks."""
|
|
260
|
+
return not dir_empty_unsafe(path)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def dirs_not_empty_unsafe(paths: StrPaths, *, root: StrPath | None = None) -> bool:
|
|
264
|
+
"""Check if directories are not empty without existence checks."""
|
|
265
|
+
if root is not None:
|
|
266
|
+
paths = [Path(root) / p for p in paths]
|
|
267
|
+
return all(dir_not_empty_unsafe(p) for p in paths)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def dir_not_empty(path: StrPath) -> bool:
|
|
271
|
+
"""Check if a directory is not empty.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
path: The directory path to check.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
True if the directory is not empty, False otherwise.
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
DirectoryNotFoundError: If the path does not exist.
|
|
281
|
+
NotADirectoryError: If the path is not a directory.
|
|
282
|
+
"""
|
|
283
|
+
path = ensure_dir_exists(path)
|
|
284
|
+
return dir_not_empty_unsafe(path)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def dirs_not_empty(paths: StrPaths, *, root: StrPath | None = None) -> bool:
|
|
288
|
+
"""Check if directories are not empty.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
paths: A sequence of directory paths to check.
|
|
292
|
+
root: The root directory to prepend to each path. Defaults to None.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
True if all directories are not empty, False otherwise.
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
DirectoryNotFoundError: If `root` is provided and does not exist.
|
|
299
|
+
DirectoryNotFoundError: If any of the paths do not exist.
|
|
300
|
+
NotADirectoryError: If `root` is provided and is not a directory.
|
|
301
|
+
NotADirectoryError: If any of the paths are not directories.
|
|
302
|
+
ValueError: If `root` is provided and any of the paths are absolute.
|
|
303
|
+
"""
|
|
304
|
+
paths = ensure_dirs_exist(paths, root=root)
|
|
305
|
+
return all(dir_not_empty_unsafe(p) for p in paths)
|
|
@@ -169,7 +169,7 @@ class Search(ABC):
|
|
|
169
169
|
|
|
170
170
|
for dirpath, dirnames, filenames in root.walk():
|
|
171
171
|
child_depth = len(dirpath.parts) - root_depth + 1
|
|
172
|
-
part = stringify_path(dirpath
|
|
172
|
+
part = stringify_path(dirpath, after=root)
|
|
173
173
|
|
|
174
174
|
if child_depth >= min_depth and cls._filter_part(part, part_filter):
|
|
175
175
|
names = cls._select_names(dirnames, filenames)
|