kaparoo-python 0.8.0__tar.gz → 0.9.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.8.0 → kaparoo_python-0.9.0}/PKG-INFO +3 -2
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/README.md +2 -1
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/data/sequences/composers.py +5 -4
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/data/sequences/templates.py +2 -2
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/README.md +41 -10
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/__init__.py +12 -1
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/directory.py +8 -8
- kaparoo_python-0.9.0/kaparoo/filesystem/exceptions.py +67 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/existence.py +6 -6
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/README.md +26 -3
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/entry.py +11 -1
- kaparoo_python-0.9.0/kaparoo/filesystem/hierarchy/scaffold.py +269 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/traverse/_utils.py +1 -4
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/traverse/validate.py +9 -3
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/utils.py +63 -30
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filters/enumerable.py +5 -5
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filters/logical.py +3 -3
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/utils/timer.py +25 -20
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/pyproject.toml +1 -1
- kaparoo_python-0.8.0/kaparoo/filesystem/exceptions.py +0 -18
- kaparoo_python-0.8.0/kaparoo/filesystem/hierarchy/scaffold.py +0 -227
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/LICENSE +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/__init__.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/data/README.md +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/data/__init__.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/data/sequences/__init__.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/data/sequences/base.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/data/sequences/utils.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/exclude.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/__init__.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/base.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/conditions.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/group.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/traverse/__init__.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/traverse/locate.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/hierarchy/utils.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/search/README.md +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/search/__init__.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/search/classes.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/search/wrappers.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/staged.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/types.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filesystem/units.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filters/README.md +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filters/__init__.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filters/base.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filters/multi_pattern.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filters/pattern.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filters/types.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/filters/utils.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/py.typed +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/utils/README.md +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/utils/__init__.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/utils/aggregate.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.0}/kaparoo/utils/checks.py +0 -0
- {kaparoo_python-0.8.0 → kaparoo_python-0.9.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.9.0
|
|
4
4
|
Summary: Personally common and useful Python features
|
|
5
5
|
Keywords: filesystem,pathlib,paths,glob,filters,pattern-matching,dataset,sequence,batching,timer,aggregation,metrics,utilities,typed
|
|
6
6
|
Author: Jaewoo Park
|
|
@@ -61,7 +61,8 @@ Each submodule ships its own README with focused examples.
|
|
|
61
61
|
`ensure_*` validators, `make_dir(s)` (with a destructive `clean` reset
|
|
62
62
|
option), `dir_empty(s)`, `reserve_path(s)` guards for not-yet-existing
|
|
63
63
|
destinations, `StagedFile` / `StagedDirectory` for safe (atomic) writes,
|
|
64
|
-
path stringification,
|
|
64
|
+
path stringification, extension helpers (`ensure_file_extension`,
|
|
65
|
+
`file_extension`, `normalize_extension(s)`), and a small exception hierarchy.
|
|
65
66
|
|
|
66
67
|
### [`kaparoo.filesystem.search`](https://github.com/kaparoo/kaparoo-python/tree/main/kaparoo/filesystem/search)
|
|
67
68
|
|
|
@@ -33,7 +33,8 @@ Each submodule ships its own README with focused examples.
|
|
|
33
33
|
`ensure_*` validators, `make_dir(s)` (with a destructive `clean` reset
|
|
34
34
|
option), `dir_empty(s)`, `reserve_path(s)` guards for not-yet-existing
|
|
35
35
|
destinations, `StagedFile` / `StagedDirectory` for safe (atomic) writes,
|
|
36
|
-
path stringification,
|
|
36
|
+
path stringification, extension helpers (`ensure_file_extension`,
|
|
37
|
+
`file_extension`, `normalize_extension(s)`), and a small exception hierarchy.
|
|
37
38
|
|
|
38
39
|
### [`kaparoo.filesystem.search`](https://github.com/kaparoo/kaparoo-python/tree/main/kaparoo/filesystem/search)
|
|
39
40
|
|
|
@@ -150,10 +150,11 @@ class TransformedSequence[T_in, M_in, T_out = T_in, M_out = M_in](
|
|
|
150
150
|
|
|
151
151
|
@override
|
|
152
152
|
def get_meta(self, index: int) -> M_out:
|
|
153
|
-
"""Pass `source`'s metadata through unchanged
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
"""Pass `source`'s metadata through unchanged.
|
|
154
|
+
|
|
155
|
+
A subclass whose `M_out` differs from `M_in` must override this.
|
|
156
|
+
"""
|
|
157
|
+
# The cast cannot catch a missing override -- generics are erased at runtime.
|
|
157
158
|
return cast("M_out", self._source.get_meta(index))
|
|
158
159
|
|
|
159
160
|
|
|
@@ -46,7 +46,7 @@ class FileListSequence[T, M = Path](DataSequence[T, M]):
|
|
|
46
46
|
|
|
47
47
|
@cached_property
|
|
48
48
|
def files(self) -> tuple[Path, ...]:
|
|
49
|
-
"""Immutable snapshot of the full file paths, in order
|
|
49
|
+
"""Immutable snapshot of the full file paths, in order."""
|
|
50
50
|
return tuple(self.get_file(i) for i in range(len(self)))
|
|
51
51
|
|
|
52
52
|
def get_file(self, index: int) -> Path:
|
|
@@ -130,7 +130,7 @@ class FileFolderSequence[T, M = Path](FileListSequence[T, M]):
|
|
|
130
130
|
|
|
131
131
|
@abstractmethod
|
|
132
132
|
def list_files(self, root: Path) -> list[Path]:
|
|
133
|
-
"""Return the full Path of every file to expose, in order.
|
|
133
|
+
"""Return the full `Path` of every file to expose, in order.
|
|
134
134
|
|
|
135
135
|
Called once from `__init__` after `root` has been validated.
|
|
136
136
|
Every returned path must be under `root`; construction raises
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
- [Exception hierarchy](#exception-hierarchy)
|
|
10
10
|
- [Creating and emptying directories](#creating-and-emptying-directories)
|
|
11
11
|
- [Path manipulation](#path-manipulation)
|
|
12
|
+
- [Extensions](#extensions)
|
|
12
13
|
- [Reserving a destination](#reserving-a-destination)
|
|
13
14
|
- [Safe (atomic) writes](#safe-atomic-writes)
|
|
14
15
|
- [Platform notes](#platform-notes)
|
|
@@ -22,10 +23,12 @@
|
|
|
22
23
|
`dir_not_empty(s)` with validation, plus `_unsafe` variants that skip
|
|
23
24
|
pre-checks
|
|
24
25
|
- [`utils`](./utils.py) — `stringify_path(s)`, `wrap_path(s)`,
|
|
25
|
-
`reserve_path(s)`, `ensure_file_extension`
|
|
26
|
+
`reserve_path(s)`, and the extension helpers `ensure_file_extension` /
|
|
27
|
+
`file_extension` / `normalize_extension(s)`
|
|
26
28
|
- [`staged`](./staged.py) — `StagedFile` / `StagedDirectory`, safe
|
|
27
29
|
(atomic) writers usable as a context manager or explicitly
|
|
28
|
-
- [`exceptions`](./exceptions.py) — `DirectoryNotFoundError`, `NotAFileError
|
|
30
|
+
- [`exceptions`](./exceptions.py) — `DirectoryNotFoundError`, `NotAFileError`,
|
|
31
|
+
`UnsupportedExtensionError`
|
|
29
32
|
- [`types`](./types.py) — `StrPath`, `StrPaths` type aliases
|
|
30
33
|
- [`units`](./units.py) — byte-size multipliers (`KB` / `MB` / `GB` / `TB`,
|
|
31
34
|
`KIB` / `MIB` / `GIB` / `TIB`)
|
|
@@ -78,6 +81,10 @@ except FileNotFoundError: # catches DirectoryNotFoundError too
|
|
|
78
81
|
...
|
|
79
82
|
```
|
|
80
83
|
|
|
84
|
+
`UnsupportedExtensionError` (raised by `ensure_file_extension`, see
|
|
85
|
+
[Extensions](#extensions)) likewise subclasses `ValueError`, so an
|
|
86
|
+
`except ValueError` catches it.
|
|
87
|
+
|
|
81
88
|
## Creating and emptying directories
|
|
82
89
|
|
|
83
90
|
```python
|
|
@@ -136,25 +143,49 @@ stringify_paths(["data/a.txt", "data/b.txt"], after="data") # ["a.txt", "b.txt"
|
|
|
136
143
|
wrap_path("logs", prepend="var", append="server.log") # var/logs/server.log
|
|
137
144
|
```
|
|
138
145
|
|
|
146
|
+
## Extensions
|
|
147
|
+
|
|
148
|
+
`normalize_extension` cleans up an extension string — it strips surrounding
|
|
149
|
+
whitespace and leading dots, keeping case unless `lowercase=True`.
|
|
150
|
+
`normalize_extensions` maps it over an iterable (empties and duplicates are
|
|
151
|
+
deliberately kept — that policy is the caller's). `file_extension` reads a
|
|
152
|
+
path's trailing suffix(es): the last `level` of them, dot-joined and
|
|
153
|
+
normalized (lower-cased by default).
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from kaparoo.filesystem import (
|
|
157
|
+
file_extension, normalize_extension, normalize_extensions,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
normalize_extension(" .TAR ") # "TAR" (case kept)
|
|
161
|
+
normalize_extension(".PNG", lowercase=True) # "png"
|
|
162
|
+
normalize_extensions([".jpg", "PNG"], lowercase=True) # ["jpg", "png"]
|
|
163
|
+
|
|
164
|
+
file_extension("photo.JPG") # "jpg" (lower-cased)
|
|
165
|
+
file_extension("data.tar.gz", level=2) # "tar.gz"
|
|
166
|
+
file_extension("README") # "" (no suffix)
|
|
167
|
+
```
|
|
168
|
+
|
|
139
169
|
`ensure_file_extension` is a pure (no filesystem) extension check: it
|
|
140
|
-
requires a `.<ext>` final suffix, raising `
|
|
141
|
-
be a single extension or an
|
|
142
|
-
|
|
143
|
-
`
|
|
144
|
-
|
|
170
|
+
requires a `.<ext>` final suffix, raising `UnsupportedExtensionError` (a
|
|
171
|
+
`ValueError` subclass) otherwise. `ext` may be a single extension or an
|
|
172
|
+
iterable of acceptable ones; the leading dot is optional and the match is
|
|
173
|
+
case-insensitive. `add=True` mirrors `make` on `ensure_dir_exists` — it
|
|
174
|
+
appends the (first) extension when the path has no suffix (`np.save`-style)
|
|
175
|
+
instead of raising; a *wrong* suffix still raises.
|
|
145
176
|
|
|
146
177
|
```python
|
|
147
178
|
from kaparoo.filesystem import ensure_file_extension
|
|
148
179
|
|
|
149
180
|
ensure_file_extension("data.bin", "bin") # Path("data.bin")
|
|
150
|
-
ensure_file_extension("data.txt", "bin") #
|
|
151
|
-
ensure_file_extension("out/00000_phase", "bin") #
|
|
181
|
+
ensure_file_extension("data.txt", "bin") # UnsupportedExtensionError
|
|
182
|
+
ensure_file_extension("out/00000_phase", "bin") # UnsupportedExtensionError (no suffix)
|
|
152
183
|
|
|
153
184
|
# Any of several accepted extensions:
|
|
154
185
|
ensure_file_extension("img.jpeg", ("jpg", "jpeg", "png")) # Path("img.jpeg")
|
|
155
186
|
|
|
156
187
|
ensure_file_extension("out/00000_phase", "bin", add=True) # Path("out/00000_phase.bin")
|
|
157
|
-
ensure_file_extension("out/data.txt", "bin", add=True) #
|
|
188
|
+
ensure_file_extension("out/data.txt", "bin", add=True) # UnsupportedExtensionError (wrong suffix)
|
|
158
189
|
```
|
|
159
190
|
|
|
160
191
|
## Reserving a destination
|
|
@@ -3,6 +3,7 @@ __all__ = (
|
|
|
3
3
|
"NotAFileError",
|
|
4
4
|
"StagedDirectory",
|
|
5
5
|
"StagedFile",
|
|
6
|
+
"UnsupportedExtensionError",
|
|
6
7
|
"dir_empty",
|
|
7
8
|
"dir_empty_unsafe",
|
|
8
9
|
"dir_exists",
|
|
@@ -21,9 +22,12 @@ __all__ = (
|
|
|
21
22
|
"ensure_path_exists",
|
|
22
23
|
"ensure_paths_exist",
|
|
23
24
|
"file_exists",
|
|
25
|
+
"file_extension",
|
|
24
26
|
"files_exist",
|
|
25
27
|
"make_dir",
|
|
26
28
|
"make_dirs",
|
|
29
|
+
"normalize_extension",
|
|
30
|
+
"normalize_extensions",
|
|
27
31
|
"path_exists",
|
|
28
32
|
"paths_exist",
|
|
29
33
|
"reserve_path",
|
|
@@ -49,7 +53,11 @@ from kaparoo.filesystem.directory import (
|
|
|
49
53
|
make_dir,
|
|
50
54
|
make_dirs,
|
|
51
55
|
)
|
|
52
|
-
from kaparoo.filesystem.exceptions import
|
|
56
|
+
from kaparoo.filesystem.exceptions import (
|
|
57
|
+
DirectoryNotFoundError,
|
|
58
|
+
NotAFileError,
|
|
59
|
+
UnsupportedExtensionError,
|
|
60
|
+
)
|
|
53
61
|
from kaparoo.filesystem.existence import (
|
|
54
62
|
dir_exists,
|
|
55
63
|
dirs_exist,
|
|
@@ -68,6 +76,9 @@ from kaparoo.filesystem.search import search_dirs, search_files, search_paths
|
|
|
68
76
|
from kaparoo.filesystem.staged import StagedDirectory, StagedFile
|
|
69
77
|
from kaparoo.filesystem.utils import (
|
|
70
78
|
ensure_file_extension,
|
|
79
|
+
file_extension,
|
|
80
|
+
normalize_extension,
|
|
81
|
+
normalize_extensions,
|
|
71
82
|
reserve_path,
|
|
72
83
|
reserve_paths,
|
|
73
84
|
stringify_path,
|
|
@@ -243,7 +243,7 @@ def make_dirs(
|
|
|
243
243
|
|
|
244
244
|
|
|
245
245
|
def dir_empty_unsafe(path: StrPath) -> bool:
|
|
246
|
-
"""
|
|
246
|
+
"""Test whether a directory is empty, skipping the existence check.
|
|
247
247
|
|
|
248
248
|
The caller must guarantee `path` is an existing directory; otherwise the
|
|
249
249
|
underlying `os.scandir` raises (`FileNotFoundError`, `NotADirectoryError`).
|
|
@@ -253,7 +253,7 @@ def dir_empty_unsafe(path: StrPath) -> bool:
|
|
|
253
253
|
|
|
254
254
|
|
|
255
255
|
def dirs_empty_unsafe(paths: StrPaths, *, root: StrPath | None = None) -> bool:
|
|
256
|
-
"""
|
|
256
|
+
"""Test whether directories are empty, skipping existence checks.
|
|
257
257
|
|
|
258
258
|
Each path carries the same caller obligation as `dir_empty_unsafe`.
|
|
259
259
|
"""
|
|
@@ -261,7 +261,7 @@ def dirs_empty_unsafe(paths: StrPaths, *, root: StrPath | None = None) -> bool:
|
|
|
261
261
|
|
|
262
262
|
|
|
263
263
|
def dir_empty(path: StrPath) -> bool:
|
|
264
|
-
"""
|
|
264
|
+
"""Test whether a directory is empty.
|
|
265
265
|
|
|
266
266
|
Args:
|
|
267
267
|
path: The directory path to check.
|
|
@@ -278,7 +278,7 @@ def dir_empty(path: StrPath) -> bool:
|
|
|
278
278
|
|
|
279
279
|
|
|
280
280
|
def dirs_empty(paths: StrPaths, *, root: StrPath | None = None) -> bool:
|
|
281
|
-
"""
|
|
281
|
+
"""Test whether directories are empty.
|
|
282
282
|
|
|
283
283
|
Args:
|
|
284
284
|
paths: A sequence of directory paths to check.
|
|
@@ -299,7 +299,7 @@ def dirs_empty(paths: StrPaths, *, root: StrPath | None = None) -> bool:
|
|
|
299
299
|
|
|
300
300
|
|
|
301
301
|
def dir_not_empty_unsafe(path: StrPath) -> bool:
|
|
302
|
-
"""
|
|
302
|
+
"""Test whether a directory is not empty, skipping the existence check.
|
|
303
303
|
|
|
304
304
|
Same caller obligation as `dir_empty_unsafe`.
|
|
305
305
|
"""
|
|
@@ -307,7 +307,7 @@ def dir_not_empty_unsafe(path: StrPath) -> bool:
|
|
|
307
307
|
|
|
308
308
|
|
|
309
309
|
def dirs_not_empty_unsafe(paths: StrPaths, *, root: StrPath | None = None) -> bool:
|
|
310
|
-
"""
|
|
310
|
+
"""Test whether directories are not empty, skipping existence checks.
|
|
311
311
|
|
|
312
312
|
Each path carries the same caller obligation as `dir_empty_unsafe`.
|
|
313
313
|
"""
|
|
@@ -315,7 +315,7 @@ def dirs_not_empty_unsafe(paths: StrPaths, *, root: StrPath | None = None) -> bo
|
|
|
315
315
|
|
|
316
316
|
|
|
317
317
|
def dir_not_empty(path: StrPath) -> bool:
|
|
318
|
-
"""
|
|
318
|
+
"""Test whether a directory is not empty.
|
|
319
319
|
|
|
320
320
|
Args:
|
|
321
321
|
path: The directory path to check.
|
|
@@ -332,7 +332,7 @@ def dir_not_empty(path: StrPath) -> bool:
|
|
|
332
332
|
|
|
333
333
|
|
|
334
334
|
def dirs_not_empty(paths: StrPaths, *, root: StrPath | None = None) -> bool:
|
|
335
|
-
"""
|
|
335
|
+
"""Test whether directories are not empty.
|
|
336
336
|
|
|
337
337
|
Args:
|
|
338
338
|
paths: A sequence of directory paths to check.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Filesystem exception types (`NotAFileError`, `UnsupportedExtensionError`, ...)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = (
|
|
6
|
+
"DirectoryNotFoundError",
|
|
7
|
+
"NotAFileError",
|
|
8
|
+
"UnsupportedExtensionError",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Iterable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DirectoryNotFoundError(FileNotFoundError):
|
|
18
|
+
"""Raised when a directory does not exist.
|
|
19
|
+
|
|
20
|
+
Note:
|
|
21
|
+
Since this inherits from `FileNotFoundError`, catch it before
|
|
22
|
+
`FileNotFoundError` in any combined `except` block.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NotAFileError(OSError):
|
|
27
|
+
"""Raised when a path exists but is not a file."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class UnsupportedExtensionError(ValueError):
|
|
31
|
+
"""Raised when an extension is none of the supported ones.
|
|
32
|
+
|
|
33
|
+
A `ValueError` subclass, so existing `except ValueError` handlers still
|
|
34
|
+
catch it. `ext` and each entry of `supported` are normalized alike --
|
|
35
|
+
surrounding whitespace and leading dots are stripped, with case preserved
|
|
36
|
+
-- after which `supported` is de-duplicated and empty entries are dropped
|
|
37
|
+
(so `".jpg"`, `"jpg "`, and `"jpg"` collapse to a single `"jpg"`). The
|
|
38
|
+
optional `kind` (whitespace-stripped) names what the extension is for and,
|
|
39
|
+
when given, is woven into the message as `for <kind>`.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: If `supported` names no usable extension -- empty, or
|
|
43
|
+
every entry normalizes to "".
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, ext: str, supported: Iterable[str], kind: str = "") -> None:
|
|
47
|
+
def normalize(ext: str) -> str:
|
|
48
|
+
return ext.strip().lstrip(".")
|
|
49
|
+
|
|
50
|
+
supported = (normalize(ext) for ext in supported)
|
|
51
|
+
supported = tuple(dict.fromkeys(ext for ext in supported if ext))
|
|
52
|
+
if not supported:
|
|
53
|
+
msg = "supported must list at least one extension"
|
|
54
|
+
raise ValueError(msg)
|
|
55
|
+
|
|
56
|
+
self.ext = normalize(ext)
|
|
57
|
+
self.supported = supported
|
|
58
|
+
self.kind = kind.strip()
|
|
59
|
+
|
|
60
|
+
msg = f"unsupported extension {self.ext!r}"
|
|
61
|
+
|
|
62
|
+
if self.kind:
|
|
63
|
+
msg += f" for {self.kind}"
|
|
64
|
+
|
|
65
|
+
msg += f" (supported: {', '.join(repr(ext) for ext in self.supported)})"
|
|
66
|
+
|
|
67
|
+
super().__init__(msg)
|
|
@@ -63,7 +63,7 @@ def ensure_path_exists(path: StrPath, *, stringify: bool) -> Path | str: ...
|
|
|
63
63
|
|
|
64
64
|
|
|
65
65
|
def ensure_path_exists(path: StrPath, *, stringify: bool = False) -> Path | str:
|
|
66
|
-
"""
|
|
66
|
+
"""Ensure a given path exists and return it.
|
|
67
67
|
|
|
68
68
|
Args:
|
|
69
69
|
path: The path to check for existence.
|
|
@@ -94,7 +94,7 @@ def ensure_file_exists(path: StrPath, *, stringify: bool) -> Path | str: ...
|
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
def ensure_file_exists(path: StrPath, *, stringify: bool = False) -> Path | str:
|
|
97
|
-
"""
|
|
97
|
+
"""Ensure a given path exists and is a file, and return it.
|
|
98
98
|
|
|
99
99
|
Args:
|
|
100
100
|
path: The file path to check for existence.
|
|
@@ -146,7 +146,7 @@ def ensure_dir_exists(
|
|
|
146
146
|
def ensure_dir_exists(
|
|
147
147
|
path: StrPath, *, make: bool | int = False, stringify: bool = False
|
|
148
148
|
) -> Path | str:
|
|
149
|
-
"""
|
|
149
|
+
"""Ensure a given path exists and is a directory, and return it.
|
|
150
150
|
|
|
151
151
|
Args:
|
|
152
152
|
path: The directory path to check for existence.
|
|
@@ -269,7 +269,7 @@ def ensure_paths_exist(
|
|
|
269
269
|
def ensure_paths_exist(
|
|
270
270
|
paths: StrPaths, *, root: StrPath | None = None, stringify: bool = False
|
|
271
271
|
) -> list[Path] | list[str]:
|
|
272
|
-
"""
|
|
272
|
+
"""Ensure all of the given paths exist and return them.
|
|
273
273
|
|
|
274
274
|
Args:
|
|
275
275
|
paths: The paths to check for existence.
|
|
@@ -311,7 +311,7 @@ def ensure_files_exist(
|
|
|
311
311
|
def ensure_files_exist(
|
|
312
312
|
paths: StrPaths, *, root: StrPath | None = None, stringify: bool = False
|
|
313
313
|
) -> list[Path] | list[str]:
|
|
314
|
-
"""
|
|
314
|
+
"""Ensure all of the given paths exist and are files, and return them.
|
|
315
315
|
|
|
316
316
|
Args:
|
|
317
317
|
paths: The file paths to check for existence.
|
|
@@ -370,7 +370,7 @@ def ensure_dirs_exist(
|
|
|
370
370
|
make: bool | int = False,
|
|
371
371
|
stringify: bool = False,
|
|
372
372
|
) -> list[Path] | list[str]:
|
|
373
|
-
"""
|
|
373
|
+
"""Ensure all of the given paths exist and are directories, and return them.
|
|
374
374
|
|
|
375
375
|
Args:
|
|
376
376
|
paths: The directory paths to check for existence.
|
|
@@ -630,9 +630,32 @@ cannot be satisfied and raises. `Together` creates all its members
|
|
|
630
630
|
Files are created **empty** (the skeleton, not its contents). Creation is
|
|
631
631
|
**idempotent**: an existing directory is descended unchanged and an existing
|
|
632
632
|
file is never clobbered, so only newly created paths are returned and a
|
|
633
|
-
re-run is a no-op. A path that exists with the wrong kind
|
|
634
|
-
|
|
635
|
-
|
|
633
|
+
re-run is a no-op. A path that exists with the wrong kind raises
|
|
634
|
+
`NotADirectoryError` (a file where a directory is described) or `NotAFileError`
|
|
635
|
+
(a directory where a file is described). `dry_run` runs every check but no
|
|
636
|
+
write, returning the paths that *would* be created.
|
|
637
|
+
|
|
638
|
+
Failure is **best-effort**: a mid-run raise — a conflict, an unsatisfiable
|
|
639
|
+
`required` node, or an `on_create` callback that raises — leaves the paths
|
|
640
|
+
already created in place. There is no rollback, but because creation is
|
|
641
|
+
idempotent a re-run resumes safely.
|
|
642
|
+
|
|
643
|
+
Two options shape what gets written. **`on_create`** is a callback run once
|
|
644
|
+
for each file *actually* created — `on_create(path, file_node)`, the seam for
|
|
645
|
+
writing a file's content (scaffold only makes the skeleton). It is not called
|
|
646
|
+
for an untouched existing file, under `dry_run`, or with `dirs_only`.
|
|
647
|
+
**`dirs_only=True`** writes only the directory skeleton, skipping every file
|
|
648
|
+
(including `required` ones); pairing it with `on_create` raises `ValueError`.
|
|
649
|
+
|
|
650
|
+
```python
|
|
651
|
+
# Fill each created file from its spec node, then build the tree.
|
|
652
|
+
def fill(path, node):
|
|
653
|
+
if path.name == "README.md":
|
|
654
|
+
path.write_text("# Project\n")
|
|
655
|
+
|
|
656
|
+
scaffold(spec, "/tmp/out", on_create=fill)
|
|
657
|
+
scaffold(spec, "/tmp/out", dirs_only=True) # directories only, no files
|
|
658
|
+
```
|
|
636
659
|
|
|
637
660
|
## See also
|
|
638
661
|
|
|
@@ -205,6 +205,16 @@ class Entry(Node, ABC):
|
|
|
205
205
|
"""The deepest level below the parent (`None` is unbounded)."""
|
|
206
206
|
return self._depth[1]
|
|
207
207
|
|
|
208
|
+
@property
|
|
209
|
+
def is_direct_child(self) -> bool:
|
|
210
|
+
"""Whether the entry is pinned to exactly depth 1 (a direct child).
|
|
211
|
+
|
|
212
|
+
True only when `min_depth` and `max_depth` are both 1 -- the default,
|
|
213
|
+
unranged position. Any wider or deeper depth (`depth=2`, `depth=None`,
|
|
214
|
+
`depth=(1, 3)`, ...) is False.
|
|
215
|
+
"""
|
|
216
|
+
return self.min_depth == 1 and self.max_depth == 1
|
|
217
|
+
|
|
208
218
|
@property
|
|
209
219
|
def required(self) -> bool:
|
|
210
220
|
"""Whether this entry must be present (vs optional)."""
|
|
@@ -256,7 +266,7 @@ class Entry(Node, ABC):
|
|
|
256
266
|
`Filter` / `Condition` `_payload`, which is the *whole* field set.
|
|
257
267
|
"""
|
|
258
268
|
payload: dict[str, Any] = {}
|
|
259
|
-
if self.
|
|
269
|
+
if not self.is_direct_child:
|
|
260
270
|
payload["depth"] = list(self._depth)
|
|
261
271
|
if self._required:
|
|
262
272
|
payload["required"] = True
|