stdl 0.6.7__py3-none-any.whl
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.
- stdl/__init__.py +2 -0
- stdl/color.py +1505 -0
- stdl/decorators.py +96 -0
- stdl/dt.py +355 -0
- stdl/fs.py +1711 -0
- stdl/import_lazy.py +123 -0
- stdl/log.py +142 -0
- stdl/lst.py +166 -0
- stdl/net.py +80 -0
- stdl/st.py +422 -0
- stdl-0.6.7.dist-info/METADATA +141 -0
- stdl-0.6.7.dist-info/RECORD +14 -0
- stdl-0.6.7.dist-info/WHEEL +4 -0
- stdl-0.6.7.dist-info/licenses/LICENSE +21 -0
stdl/fs.py
ADDED
|
@@ -0,0 +1,1711 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import json
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import pickle
|
|
8
|
+
import platform
|
|
9
|
+
import random
|
|
10
|
+
import re
|
|
11
|
+
import shlex
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from collections.abc import Callable, Generator, Iterable
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from os import PathLike
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from queue import Queue
|
|
21
|
+
from typing import IO, Any, Literal
|
|
22
|
+
|
|
23
|
+
if sys.version_info >= (3, 11):
|
|
24
|
+
from typing import Self
|
|
25
|
+
else:
|
|
26
|
+
from typing_extensions import Self
|
|
27
|
+
|
|
28
|
+
from urllib.request import pathname2url
|
|
29
|
+
|
|
30
|
+
import toml
|
|
31
|
+
import yaml
|
|
32
|
+
|
|
33
|
+
from stdl import st
|
|
34
|
+
|
|
35
|
+
stat = os.stat
|
|
36
|
+
link = os.link
|
|
37
|
+
getcwd = os.getcwd
|
|
38
|
+
|
|
39
|
+
SEP = os.sep
|
|
40
|
+
HOME = os.path.expanduser("~")
|
|
41
|
+
abspath = os.path.abspath
|
|
42
|
+
basename = os.path.basename
|
|
43
|
+
dirname = os.path.dirname
|
|
44
|
+
joinpath = os.path.join
|
|
45
|
+
fspath = os.fspath
|
|
46
|
+
splitpath = os.path.split
|
|
47
|
+
isdir = os.path.isdir
|
|
48
|
+
isfile = os.path.isfile
|
|
49
|
+
islink = os.path.islink
|
|
50
|
+
exists = os.path.exists
|
|
51
|
+
|
|
52
|
+
chdir = os.chdir
|
|
53
|
+
chmod = os.chmod
|
|
54
|
+
remove = os.remove
|
|
55
|
+
rename = os.rename
|
|
56
|
+
|
|
57
|
+
copy = shutil.copy2
|
|
58
|
+
move = shutil.move
|
|
59
|
+
chown = shutil.chown
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def pickle_load(filepath: str | PathLike) -> Any:
|
|
63
|
+
"""Loads a pickled file."""
|
|
64
|
+
with open(filepath, "rb") as f:
|
|
65
|
+
return pickle.load(f)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def pickle_dump(data: Any, filepath: str | PathLike) -> None:
|
|
69
|
+
"""Dumps an object to the specified filepath."""
|
|
70
|
+
with open(filepath, "wb") as f:
|
|
71
|
+
pickle.dump(data, f)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def json_load(
|
|
75
|
+
path: str | PathLike, encoding: str = "utf-8"
|
|
76
|
+
) -> dict[Any, Any] | list[dict[Any, Any]]:
|
|
77
|
+
"""
|
|
78
|
+
Load a JSON file from the given path.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
path (str | PathLike): The path of the JSON file to load.
|
|
82
|
+
encoding (str, optional): The encoding of the file. Defaults to "utf-8".
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
dict | list[dict]: The JSON data loaded from the file.
|
|
86
|
+
"""
|
|
87
|
+
with open(os.fspath(path), encoding=encoding) as f:
|
|
88
|
+
return json.load(f)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def json_append(
|
|
92
|
+
data: dict[Any, Any] | list[dict[Any, Any]],
|
|
93
|
+
filepath: str | PathLike,
|
|
94
|
+
encoding: str = "utf-8",
|
|
95
|
+
default: Callable[[Any], str] = str,
|
|
96
|
+
indent: int = 4,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Append data to a JSON file.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
data (dict | list[dict]): The data to be appended
|
|
103
|
+
filepath (str | PathLike): The path of the JSON file
|
|
104
|
+
encoding (str, optional): The encoding of the file. Defaults to "utf-8".
|
|
105
|
+
default : A function that gets called for objects that can't otherwise be serialized.
|
|
106
|
+
See json.dump() documentation for more information.
|
|
107
|
+
indent (int, optional): The number of spaces to use for indentation. Defaults to 4.
|
|
108
|
+
"""
|
|
109
|
+
file = File(filepath)
|
|
110
|
+
path = os.fspath(filepath)
|
|
111
|
+
if not file.exists or file.size == 0:
|
|
112
|
+
json_dump([data], filepath, encoding=encoding, indent=indent, default=default)
|
|
113
|
+
return
|
|
114
|
+
with open(path, "a+", encoding=encoding) as f:
|
|
115
|
+
f.seek(0)
|
|
116
|
+
first_char = f.read(1)
|
|
117
|
+
if first_char == "[":
|
|
118
|
+
f.seek(0, os.SEEK_END)
|
|
119
|
+
f.seek(f.tell() - 2, os.SEEK_SET)
|
|
120
|
+
f.truncate()
|
|
121
|
+
f.write(",\n")
|
|
122
|
+
json.dump(data, f, indent=indent, default=default)
|
|
123
|
+
f.write("]\n")
|
|
124
|
+
elif first_char == "{":
|
|
125
|
+
file_data = first_char + f.read()
|
|
126
|
+
f.seek(0)
|
|
127
|
+
f.truncate()
|
|
128
|
+
f.seek(0)
|
|
129
|
+
f.write("[\n")
|
|
130
|
+
f.write(file_data)
|
|
131
|
+
f.write(",\n")
|
|
132
|
+
json.dump(data, f, indent=indent, default=default)
|
|
133
|
+
f.write("]\n")
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError(f"Cannot parse '{path}' as JSON.")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def json_dump(
|
|
139
|
+
data: Any,
|
|
140
|
+
path: str | PathLike,
|
|
141
|
+
encoding: str = "utf-8",
|
|
142
|
+
default: Callable[[Any], str] = str,
|
|
143
|
+
indent: int = 4,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""
|
|
146
|
+
Dumps data to a JSON file.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
data: data to be dumped
|
|
150
|
+
path (str | PathLike): path to the output file
|
|
151
|
+
encoding (str): encoding of the output file. Default: 'utf-8'
|
|
152
|
+
default: A function that gets called on objects that cannot be serialized. Default: str
|
|
153
|
+
indent (int): number of spaces to use when indenting the output json. Default: 4
|
|
154
|
+
"""
|
|
155
|
+
with open(os.fspath(path), "w", encoding=encoding) as f:
|
|
156
|
+
json.dump(data, f, indent=indent, default=default)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def yaml_load(
|
|
160
|
+
path: str | PathLike, encoding: str = "utf-8"
|
|
161
|
+
) -> dict[Any, Any] | list[dict[Any, Any]]:
|
|
162
|
+
"""
|
|
163
|
+
Load a YAML file from the given path.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
path (str | PathLike): The path of the YAML file to load.
|
|
167
|
+
encoding (str, optional): The encoding of the file. Defaults to "utf-8".
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
dict | list[dict]: The YAML data loaded from the file.
|
|
171
|
+
"""
|
|
172
|
+
with open(path, encoding=encoding) as f:
|
|
173
|
+
return yaml.safe_load(f)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def yaml_dump(data: Any, path: str | PathLike, encoding: str = "utf-8") -> None:
|
|
177
|
+
"""
|
|
178
|
+
Dumps data to a YAML file
|
|
179
|
+
Args:
|
|
180
|
+
data: data to be dumped
|
|
181
|
+
path (Pathlike): path to the output file
|
|
182
|
+
encoding (str): encoding of the output file. Default: 'utf-8'
|
|
183
|
+
"""
|
|
184
|
+
with open(path, "w", encoding=encoding) as f:
|
|
185
|
+
yaml.safe_dump(data, f)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def toml_load(path: str | PathLike, encoding: str = "utf-8") -> dict[str, Any]:
|
|
189
|
+
with open(path, encoding=encoding) as f:
|
|
190
|
+
return toml.load(f)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def toml_dump(data: dict[str, Any], path: str | PathLike, encoding: str = "utf-8") -> None:
|
|
194
|
+
with open(path, "w", encoding=encoding) as f:
|
|
195
|
+
toml.dump(data, f)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_dir_size(directory: str | PathLike, *, readable: bool = False) -> str | int:
|
|
199
|
+
"""
|
|
200
|
+
Moves files to a specified directory
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
directory (str, Path): target directory
|
|
204
|
+
readable (bool, optional): Return the size in human-readable format
|
|
205
|
+
"""
|
|
206
|
+
total_size = 0
|
|
207
|
+
for dirpath, _, filenames in os.walk(directory):
|
|
208
|
+
for f in filenames:
|
|
209
|
+
fp = os.path.join(dirpath, f)
|
|
210
|
+
# Skip if it's symbolic link.
|
|
211
|
+
if not os.path.islink(fp):
|
|
212
|
+
total_size += os.path.getsize(fp)
|
|
213
|
+
if readable:
|
|
214
|
+
return bytes_readable(total_size)
|
|
215
|
+
return total_size
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def move_files(
|
|
219
|
+
files: list[str | PathLike], directory: str | PathLike, *, mkdir: bool = False
|
|
220
|
+
) -> None:
|
|
221
|
+
"""
|
|
222
|
+
Moves files to a specified directory
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
files (list[str | PathLike]): List of files to be moved
|
|
226
|
+
directory (str | PathLike): target directory
|
|
227
|
+
mkdir (bool): whether to create the directory if it doesn't exist. Default: False
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
FileNotFoundError : if the target directory does not exist and mkdir is False.
|
|
231
|
+
"""
|
|
232
|
+
directory = os.fspath(directory)
|
|
233
|
+
if exists(directory):
|
|
234
|
+
if mkdir:
|
|
235
|
+
os.makedirs(directory, exist_ok=True)
|
|
236
|
+
else:
|
|
237
|
+
raise FileNotFoundError(f"{directory} is not a directory")
|
|
238
|
+
for file in files:
|
|
239
|
+
os.rename(os.fspath(file), f"{directory}{SEP}{os.path.basename(file)}")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def rand_filename(prefix: str = "file", ext: str = "", include_datetime: bool = False) -> str:
|
|
243
|
+
"""
|
|
244
|
+
Generates a random filename with the given prefix and extension.
|
|
245
|
+
Optionally includes current date and time in the filename.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
prefix (str, optional): Filename prefix.
|
|
249
|
+
ext (str, optional): Filename extension.
|
|
250
|
+
include_datetime (bool, optional): Whether to include date and time.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
str: The generated random filename.
|
|
254
|
+
"""
|
|
255
|
+
if ext and not ext.startswith("."):
|
|
256
|
+
ext = f".{ext}"
|
|
257
|
+
num = str(random.randrange(1000000000, 9999999999)).zfill(10)
|
|
258
|
+
if include_datetime:
|
|
259
|
+
creation_time = datetime.now().strftime("%Y-%m-%d.%H-%M-%S-%f")[:-3]
|
|
260
|
+
filename = f"{prefix}.{num}.{creation_time}{ext}"
|
|
261
|
+
else:
|
|
262
|
+
filename = f"{prefix}.{num}{ext}"
|
|
263
|
+
return safe_filename(filename)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def bytes_readable(size_bytes: int) -> str:
|
|
267
|
+
"""
|
|
268
|
+
Convert bytes to a human-readable string.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
size_bytes (int): The number of bytes
|
|
272
|
+
Returns:
|
|
273
|
+
str : The number of bytes in a human-readable format
|
|
274
|
+
"""
|
|
275
|
+
if size_bytes < 0:
|
|
276
|
+
raise ValueError(size_bytes)
|
|
277
|
+
if size_bytes == 0:
|
|
278
|
+
return "0B"
|
|
279
|
+
i = int(math.floor(math.log(size_bytes, 1024)))
|
|
280
|
+
p = math.pow(1024, i)
|
|
281
|
+
s = round(size_bytes / p, 2)
|
|
282
|
+
size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
|
|
283
|
+
return f"{s} {size_name[i]}"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def readable_size_to_bytes(size: str, kb_size: Literal[1000, 1024] = 1024) -> int:
|
|
287
|
+
"""
|
|
288
|
+
Convert human-readable string to bytes.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
size (str): The number of bytes in human-readable format
|
|
292
|
+
kb_size (int, optional): The byte size of a kilobyte (1000 or 1024). Defaults to 1024.
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
int: The number of bytes
|
|
296
|
+
"""
|
|
297
|
+
if kb_size not in (1000, 1024):
|
|
298
|
+
raise ValueError(f"Invalid kb_size: {kb_size}. Must be 1000 or 1024.")
|
|
299
|
+
|
|
300
|
+
size = size.upper().replace(" ", "")
|
|
301
|
+
if size.isdigit():
|
|
302
|
+
return int(size)
|
|
303
|
+
|
|
304
|
+
match = re.match(r"^(\d+(\.\d+)?)(B|KB|MB|GB|TB)$", size)
|
|
305
|
+
if not match:
|
|
306
|
+
raise ValueError(f"Invalid size format: {size}")
|
|
307
|
+
|
|
308
|
+
number, unit = float(match.group(1)), match.group(3)
|
|
309
|
+
|
|
310
|
+
units = {"B": 1, "KB": kb_size, "MB": kb_size**2, "GB": kb_size**3, "TB": kb_size**4}
|
|
311
|
+
|
|
312
|
+
if unit not in units:
|
|
313
|
+
raise ValueError(f"Invalid unit: {unit}")
|
|
314
|
+
|
|
315
|
+
return int(number * units[unit])
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def windows_has_drive(letter: str) -> bool:
|
|
319
|
+
"""
|
|
320
|
+
Check if a drive letter exists on Windows.
|
|
321
|
+
Will always return False if the platform is not Windows.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
letter (str): The letter of the drive.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
bool: Whether the drive exists.
|
|
328
|
+
"""
|
|
329
|
+
if sys.platform != "win32":
|
|
330
|
+
return False
|
|
331
|
+
return os.path.exists(f"{letter}:{SEP}")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def is_wsl() -> bool:
|
|
335
|
+
"""Check if the current platform is Windows Subsystem for Linux (WSL)."""
|
|
336
|
+
return sys.platform == "linux" and "microsoft" in platform.platform()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def mkdir(path: str | PathLike, mode: int = 511, exist_ok: bool = True) -> None:
|
|
340
|
+
"""
|
|
341
|
+
Creates a directory.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
path (str | Path): The path of the directory to create.
|
|
345
|
+
mode (int, optional): The mode to set for the directory. Defaults to 511 (octal 0777).
|
|
346
|
+
exist_ok (bool, optional): Whether to raise an exception if the directory already exists. Defaults to True.
|
|
347
|
+
"""
|
|
348
|
+
os.makedirs(os.fspath(path), exist_ok=exist_ok, mode=mode)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def mkdirs(dest: str | PathLike, names: list[str]) -> None:
|
|
352
|
+
"""
|
|
353
|
+
Creates directories inside a destination directory.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
dest (str | Path): The destination directory.
|
|
357
|
+
names (list[str]): A list of directory names to be created in the destination directory.
|
|
358
|
+
"""
|
|
359
|
+
if not exists(dest):
|
|
360
|
+
mkdir(dest)
|
|
361
|
+
for i in names:
|
|
362
|
+
if not exists(i):
|
|
363
|
+
path = f"{dest}{SEP}{i}"
|
|
364
|
+
mkdir(path)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def yield_files_in(
|
|
368
|
+
directory: str | PathLike,
|
|
369
|
+
ext: str | tuple[str, ...] | None = None,
|
|
370
|
+
*,
|
|
371
|
+
recursive: bool = True,
|
|
372
|
+
abs: bool = True,
|
|
373
|
+
) -> Generator[str, None, None]:
|
|
374
|
+
"""
|
|
375
|
+
Yields the paths of files in a directory.
|
|
376
|
+
|
|
377
|
+
This function searches for files in a directory and yields their paths.
|
|
378
|
+
If the `ext` parameter is provided, only files with that extension are yielded. The `ext` parameter is case-insensitive.
|
|
379
|
+
If the `recursive` parameter is set to `True`, the function will search for files in subdirectories recursively.
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
directory (str | Path): The directory to search.
|
|
383
|
+
ext (str | tuple[str, ...], optional): If provided, only yield files with provided extensions.
|
|
384
|
+
recursive (bool, optional): Whether to search recursively.
|
|
385
|
+
abs (bool, optional): Whether to convert paths to absolute paths.
|
|
386
|
+
|
|
387
|
+
Yields:
|
|
388
|
+
Generator[str, None, None]: The absolute paths of the files in the directory, matching the provided extension.
|
|
389
|
+
"""
|
|
390
|
+
if not recursive:
|
|
391
|
+
for entry in os.scandir(directory):
|
|
392
|
+
if not entry.is_file():
|
|
393
|
+
continue
|
|
394
|
+
path = entry.path
|
|
395
|
+
if abs:
|
|
396
|
+
path = os.path.abspath(entry.path)
|
|
397
|
+
if ext is None:
|
|
398
|
+
yield path
|
|
399
|
+
else:
|
|
400
|
+
if path.lower().endswith(ext):
|
|
401
|
+
yield path
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
queue = Queue()
|
|
405
|
+
queue.put(directory)
|
|
406
|
+
|
|
407
|
+
if ext is None:
|
|
408
|
+
while not queue.empty():
|
|
409
|
+
next_dir = queue.get()
|
|
410
|
+
for entry in os.scandir(next_dir):
|
|
411
|
+
if entry.is_dir():
|
|
412
|
+
queue.put(entry.path)
|
|
413
|
+
elif entry.is_file():
|
|
414
|
+
yield os.path.abspath(entry.path) if abs else entry.path
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
while not queue.empty():
|
|
418
|
+
next_dir = queue.get()
|
|
419
|
+
for entry in os.scandir(next_dir):
|
|
420
|
+
if entry.is_dir():
|
|
421
|
+
queue.put(entry.path)
|
|
422
|
+
elif entry.is_file():
|
|
423
|
+
if entry.path.lower().endswith(ext):
|
|
424
|
+
yield os.path.abspath(entry.path) if abs else entry.path
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def get_files_in(
|
|
428
|
+
directory: str | PathLike,
|
|
429
|
+
ext: str | tuple[str, ...] | None = None,
|
|
430
|
+
*,
|
|
431
|
+
recursive: bool = True,
|
|
432
|
+
abs: bool = True,
|
|
433
|
+
) -> list[str]:
|
|
434
|
+
"""
|
|
435
|
+
Returns the paths of files in a directory.
|
|
436
|
+
|
|
437
|
+
This function searches for files in a directory and yields their paths.
|
|
438
|
+
If the `ext` parameter is provided, only files with that extension are returned. The `ext` parameter is case-insensitive.
|
|
439
|
+
If the `recursive` parameter is set to `True`, the function will search for files in subdirectories recursively.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
directory (str | Path): The directory to search.
|
|
443
|
+
ext (str | tuple[str, ...], optional): If provided, only yield files with provided extensions. Defaults to None.
|
|
444
|
+
recursive (bool, optional): Whether to search recursively. Defaults to True.
|
|
445
|
+
abs (bool, optional): Whether to convert paths to absolute paths.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
list[str]: The absolute path of the files in the directory, matching the provided extension.
|
|
449
|
+
"""
|
|
450
|
+
return list(yield_files_in(directory, ext, recursive=recursive, abs=abs))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def yield_dirs_in(
|
|
454
|
+
directory: str | PathLike, *, recursive: bool = True, abs: bool = True
|
|
455
|
+
) -> Generator[str, None, None]:
|
|
456
|
+
"""
|
|
457
|
+
Yields paths to all directories in the specified directory.
|
|
458
|
+
Yielded paths are converted to absolute paths.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
directory (str | Path): The directory to search.
|
|
462
|
+
recursive (bool, optional): Whether to search recursively.
|
|
463
|
+
abs (bool, optional): Whether to convert paths to absolute paths.
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
Yields:
|
|
467
|
+
Generator[str, None, None]: The paths of the directories that are found during travelsal.
|
|
468
|
+
"""
|
|
469
|
+
queue = Queue()
|
|
470
|
+
queue.put(directory)
|
|
471
|
+
while not queue.empty():
|
|
472
|
+
next_dir = queue.get()
|
|
473
|
+
for entry in os.scandir(next_dir):
|
|
474
|
+
if entry.is_dir():
|
|
475
|
+
if recursive:
|
|
476
|
+
queue.put(entry.path)
|
|
477
|
+
yield os.path.abspath(entry.path) if abs else entry.path
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def get_dirs_in(
|
|
481
|
+
directory: str | PathLike, *, recursive: bool = True, abs: bool = True
|
|
482
|
+
) -> list[str]:
|
|
483
|
+
"""
|
|
484
|
+
Returns all directories in the specified directory.
|
|
485
|
+
Returned paths are converted to absolute paths.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
directory (str | Path): The directory to search.
|
|
489
|
+
recursive (bool, optional): Whether to search recursively.
|
|
490
|
+
abs (bool, optional): Whether to convert paths to absolute paths.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
list[str]: The paths of the directories that are found during travelsal.
|
|
494
|
+
"""
|
|
495
|
+
return list(yield_dirs_in(directory, recursive=recursive, abs=abs))
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def ensure_paths_exist(*args: str | PathLike | Iterable[str | PathLike]) -> None:
|
|
499
|
+
"""
|
|
500
|
+
Ensures that the specified paths exist.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
*args : one or more strings representing the paths to check. Can also include an Iterable of paths.
|
|
504
|
+
|
|
505
|
+
Raises:
|
|
506
|
+
FileNotFoundError : if one of the provided paths does not exist.
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
def check_path(path: str | PathLike) -> None:
|
|
510
|
+
if not os.path.exists(os.fspath(path)):
|
|
511
|
+
raise FileNotFoundError(f"Path does not exist: '{path}'")
|
|
512
|
+
|
|
513
|
+
for path in args:
|
|
514
|
+
if isinstance(path, (str, bytes, PathLike)):
|
|
515
|
+
check_path(path)
|
|
516
|
+
elif isinstance(path, Iterable):
|
|
517
|
+
for i in path:
|
|
518
|
+
check_path(i)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def ensure_paths_dont_exist(*args: str | PathLike | Iterable[str | PathLike]) -> None:
|
|
522
|
+
"""
|
|
523
|
+
Ensures that the specified paths don't exist.
|
|
524
|
+
|
|
525
|
+
Args:
|
|
526
|
+
*args : one or more strings representing the paths to check. Can also include an Iterable of paths.
|
|
527
|
+
|
|
528
|
+
Raises:
|
|
529
|
+
FileNotFoundError : if one of the provided paths exists.
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
def check_path(path: str | PathLike) -> None:
|
|
533
|
+
if os.path.exists(os.fspath(path)):
|
|
534
|
+
raise FileExistsError(f"Path already exists: '{path}'")
|
|
535
|
+
|
|
536
|
+
for path in args:
|
|
537
|
+
if isinstance(path, (str, bytes, PathLike)):
|
|
538
|
+
check_path(path)
|
|
539
|
+
elif isinstance(path, Iterable):
|
|
540
|
+
for i in path:
|
|
541
|
+
check_path(i)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def safe_filename(name: str) -> str:
|
|
545
|
+
newname = name.replace('"', "'")
|
|
546
|
+
newname = st.remove(newname, chars='<>:"/|?*\\“”;')
|
|
547
|
+
return newname
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def safe_filepath(path: str) -> str:
|
|
551
|
+
return st.remove(path, chars='<>"|?*“”;')
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
class CompletedCommand(subprocess.CompletedProcess):
|
|
555
|
+
def __init__(
|
|
556
|
+
self,
|
|
557
|
+
args: str | list[str],
|
|
558
|
+
returncode: int,
|
|
559
|
+
time_taken: float,
|
|
560
|
+
stdout: Any | None = None,
|
|
561
|
+
stderr: Any | None = None,
|
|
562
|
+
) -> None:
|
|
563
|
+
super().__init__(args, returncode, stdout, stderr)
|
|
564
|
+
self.time_taken = time_taken
|
|
565
|
+
|
|
566
|
+
def __repr__(self) -> str:
|
|
567
|
+
args = [
|
|
568
|
+
f"args={self.args!r}",
|
|
569
|
+
f"returncode={self.returncode!r}",
|
|
570
|
+
f"time_taken={self.time_taken}s",
|
|
571
|
+
]
|
|
572
|
+
if self.stdout is not None:
|
|
573
|
+
args.append(f"stdout={self.stdout!r}")
|
|
574
|
+
if self.stderr is not None:
|
|
575
|
+
args.append(f"stderr={self.stderr!r}")
|
|
576
|
+
return "{}({})".format(type(self).__name__, ", ".join(args))
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def stdout_lines(self) -> list[str]:
|
|
580
|
+
"""Returns the stdout as a list of lines."""
|
|
581
|
+
if self.stdout is None:
|
|
582
|
+
return []
|
|
583
|
+
return self.stdout.splitlines()
|
|
584
|
+
|
|
585
|
+
@property
|
|
586
|
+
def stderr_lines(self) -> list[str]:
|
|
587
|
+
"""Returns the stderr as a list of lines."""
|
|
588
|
+
if self.stderr is None:
|
|
589
|
+
return []
|
|
590
|
+
return self.stderr.splitlines()
|
|
591
|
+
|
|
592
|
+
def dict(self) -> dict[str, Any]:
|
|
593
|
+
return {
|
|
594
|
+
"args": self.args,
|
|
595
|
+
"returncode": self.returncode,
|
|
596
|
+
"time_taken": self.time_taken,
|
|
597
|
+
"stdout": self.stdout,
|
|
598
|
+
"stderr": self.stderr,
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def exec_cmd(
|
|
603
|
+
cmd: list[str] | str,
|
|
604
|
+
timeout: float | None = None, # type:ignore
|
|
605
|
+
shell: bool = False,
|
|
606
|
+
capture_output: bool = True,
|
|
607
|
+
check: bool = False,
|
|
608
|
+
cwd: str | None = None, # type:ignore
|
|
609
|
+
stdin: IO | None = None, # type:ignore
|
|
610
|
+
stdout: IO | None = None, # type:ignore
|
|
611
|
+
stderr: IO | None = None, # type:ignore
|
|
612
|
+
input: str | None | bytes = None, # type:ignore
|
|
613
|
+
env: dict[str, str] | None = None, # type:ignore
|
|
614
|
+
text: bool = True,
|
|
615
|
+
*args: Any,
|
|
616
|
+
**kwargs: Any,
|
|
617
|
+
) -> CompletedCommand:
|
|
618
|
+
"""
|
|
619
|
+
Wrapper for subprocess.run with nicer default arguments.
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
cmd (str | list[str]): command to run. A list of strings or a single string.
|
|
623
|
+
timeout (float, optional): the time after which the command is killed.
|
|
624
|
+
shell (bool, optional): whether or not to run the command in a shell.
|
|
625
|
+
capture_output (bool, optional): whether or not to capture the output to stdout and stderr.
|
|
626
|
+
check (bool, optional): whether or not to raise an exception on a non-zero exit code.
|
|
627
|
+
cwd (str, optional): current working directory to run the command in.
|
|
628
|
+
stdin (IO, optional): file object to read stdin from.
|
|
629
|
+
stdout (IO, optional): file object to write stdout to.
|
|
630
|
+
stderr (IO, optional): file object to write stderr to.
|
|
631
|
+
input (str | bytes): input to send to the command.
|
|
632
|
+
env (dict, optional): environment variables to pass to the new process.
|
|
633
|
+
text (bool): whether or not to return output as text or bytes.
|
|
634
|
+
*args : additional arguments to pass to subprocess.run.
|
|
635
|
+
**kwargs : additional keyword arguments to pass to subprocess.run.
|
|
636
|
+
|
|
637
|
+
Returns:
|
|
638
|
+
subprocess.CompletedProcess : the completed process.
|
|
639
|
+
"""
|
|
640
|
+
if isinstance(cmd, str):
|
|
641
|
+
cmd = shlex.split(cmd)
|
|
642
|
+
|
|
643
|
+
start_time = time.time()
|
|
644
|
+
|
|
645
|
+
result = subprocess.run(
|
|
646
|
+
cmd,
|
|
647
|
+
timeout=timeout,
|
|
648
|
+
shell=shell,
|
|
649
|
+
check=check,
|
|
650
|
+
stdin=stdin,
|
|
651
|
+
stdout=stdout,
|
|
652
|
+
stderr=stderr,
|
|
653
|
+
capture_output=capture_output,
|
|
654
|
+
text=text,
|
|
655
|
+
input=input,
|
|
656
|
+
env=env,
|
|
657
|
+
cwd=cwd,
|
|
658
|
+
**kwargs,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
return CompletedCommand(
|
|
662
|
+
result.args,
|
|
663
|
+
result.returncode,
|
|
664
|
+
time.time() - start_time,
|
|
665
|
+
result.stdout,
|
|
666
|
+
result.stderr,
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def read_piped() -> str:
|
|
671
|
+
"""Reads piped input from stdin."""
|
|
672
|
+
if sys.stdin.isatty():
|
|
673
|
+
return ""
|
|
674
|
+
return sys.stdin.read().strip()
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def start_file(path: str | PathLike) -> None:
|
|
678
|
+
"""
|
|
679
|
+
Open the file with your OS's default application.
|
|
680
|
+
|
|
681
|
+
This function determines the current operating system and uses the appropriate command
|
|
682
|
+
to open the specified file with the default application. It supports Windows, macOS,
|
|
683
|
+
and Linux, including Windows Subsystem for Linux (WSL).
|
|
684
|
+
"""
|
|
685
|
+
path = os.fspath(path)
|
|
686
|
+
if is_wsl():
|
|
687
|
+
exec_cmd(f"cmd.exe /C start '{path}'", check=True)
|
|
688
|
+
elif sys.platform == "win32":
|
|
689
|
+
os.startfile(path)
|
|
690
|
+
elif sys.platform == "darwin":
|
|
691
|
+
exec_cmd(f"open {path}", check=True)
|
|
692
|
+
elif sys.platform == "linux":
|
|
693
|
+
exec_cmd(f"xdg-open '{path}'", check=True)
|
|
694
|
+
else:
|
|
695
|
+
raise NotImplementedError(f"Unsupported platform: {sys.platform}")
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
class PathBase(PathLike):
|
|
699
|
+
def __init__(
|
|
700
|
+
self,
|
|
701
|
+
path: str | PathLike,
|
|
702
|
+
*,
|
|
703
|
+
abs: bool = False,
|
|
704
|
+
) -> None:
|
|
705
|
+
"""
|
|
706
|
+
Initialize a PathBase object.
|
|
707
|
+
|
|
708
|
+
Args:
|
|
709
|
+
path (str | PathLike): The path.
|
|
710
|
+
abs (bool): Whether to use the absolute path.
|
|
711
|
+
"""
|
|
712
|
+
self.path = os.fspath(path).replace("\\", SEP).replace("/", SEP)
|
|
713
|
+
if abs:
|
|
714
|
+
self.path = os.path.abspath(self.path)
|
|
715
|
+
|
|
716
|
+
def __fspath__(self) -> str:
|
|
717
|
+
return self.path
|
|
718
|
+
|
|
719
|
+
def __str__(self) -> str:
|
|
720
|
+
return self.path
|
|
721
|
+
|
|
722
|
+
def resolve(self, strict: bool = False) -> PathBase:
|
|
723
|
+
"""
|
|
724
|
+
Make the path absolute, resolving any symlinks.
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
strict: If True and path doesn't exist, raises FileNotFoundError. Defaults to False.
|
|
728
|
+
|
|
729
|
+
Returns:
|
|
730
|
+
PathBase: The object with resolved path.
|
|
731
|
+
"""
|
|
732
|
+
self.path = os.path.realpath(self.path)
|
|
733
|
+
if strict and not self.exists:
|
|
734
|
+
raise FileNotFoundError(f"No such file: '{self.path}'")
|
|
735
|
+
return self
|
|
736
|
+
|
|
737
|
+
@property
|
|
738
|
+
def nodes(self) -> list[str]:
|
|
739
|
+
"""The path as a list of nodes."""
|
|
740
|
+
return self.abspath.split(SEP)
|
|
741
|
+
|
|
742
|
+
@property
|
|
743
|
+
def created(self) -> float:
|
|
744
|
+
"""The time when the path was created as a UNIX timestamp."""
|
|
745
|
+
return os.path.getctime(self.path)
|
|
746
|
+
|
|
747
|
+
@property
|
|
748
|
+
def modified(self) -> float:
|
|
749
|
+
"""The time when the path was last modified as a UNIX timestamp."""
|
|
750
|
+
return os.path.getmtime(self.path)
|
|
751
|
+
|
|
752
|
+
@property
|
|
753
|
+
def accessed(self) -> float:
|
|
754
|
+
"""The time when the path was last accessed as a UNIX timestamp."""
|
|
755
|
+
return os.path.getatime(self.path)
|
|
756
|
+
|
|
757
|
+
ctime = created
|
|
758
|
+
mtime = modified
|
|
759
|
+
atime = accessed
|
|
760
|
+
|
|
761
|
+
@property
|
|
762
|
+
def basename(self) -> str:
|
|
763
|
+
"""The base name of the path (without the parent directory)."""
|
|
764
|
+
return os.path.basename(self.path)
|
|
765
|
+
|
|
766
|
+
@property
|
|
767
|
+
def abspath(self) -> str:
|
|
768
|
+
"""The absolute path."""
|
|
769
|
+
return os.path.abspath(self.path)
|
|
770
|
+
|
|
771
|
+
@property
|
|
772
|
+
def parents(self) -> tuple[Directory, ...]:
|
|
773
|
+
"""All parent directories from immediate parent to root."""
|
|
774
|
+
result: list[Directory] = []
|
|
775
|
+
current = os.path.dirname(os.path.abspath(self.path))
|
|
776
|
+
while current:
|
|
777
|
+
result.append(Directory(current))
|
|
778
|
+
parent = os.path.dirname(current)
|
|
779
|
+
if parent == current:
|
|
780
|
+
break
|
|
781
|
+
current = parent
|
|
782
|
+
return tuple(result)
|
|
783
|
+
|
|
784
|
+
@property
|
|
785
|
+
def is_absolute(self) -> bool:
|
|
786
|
+
"""Whether the path is absolute."""
|
|
787
|
+
return os.path.isabs(self.path)
|
|
788
|
+
|
|
789
|
+
@property
|
|
790
|
+
def is_symlink(self) -> bool:
|
|
791
|
+
"""Whether the path is a symbolic link."""
|
|
792
|
+
return os.path.islink(self.path)
|
|
793
|
+
|
|
794
|
+
def rename(self, name: str) -> Self:
|
|
795
|
+
"""
|
|
796
|
+
Rename the path.
|
|
797
|
+
|
|
798
|
+
Args:
|
|
799
|
+
name (str): The new name.
|
|
800
|
+
"""
|
|
801
|
+
new_path = os.path.join(self.parent.path, name)
|
|
802
|
+
os.rename(self.path, new_path)
|
|
803
|
+
self.path = new_path
|
|
804
|
+
return self
|
|
805
|
+
|
|
806
|
+
def chmod(self, mode: int) -> Self:
|
|
807
|
+
"""
|
|
808
|
+
Change the path's permissions.
|
|
809
|
+
|
|
810
|
+
Args:
|
|
811
|
+
mode (int): The new permissions mode.
|
|
812
|
+
"""
|
|
813
|
+
os.chmod(self.path, mode)
|
|
814
|
+
return self
|
|
815
|
+
|
|
816
|
+
def chown(self, user: str, group: str) -> Self:
|
|
817
|
+
"""
|
|
818
|
+
Change the path's owner and group.
|
|
819
|
+
|
|
820
|
+
Args:
|
|
821
|
+
user (str): The new owner.
|
|
822
|
+
group (str): The new group.
|
|
823
|
+
"""
|
|
824
|
+
shutil.chown(self.path, user, group)
|
|
825
|
+
return self
|
|
826
|
+
|
|
827
|
+
def should_exist(self) -> Self:
|
|
828
|
+
"""Raise FileNotFoundError if the path does not exist."""
|
|
829
|
+
if not self.exists:
|
|
830
|
+
raise FileNotFoundError(f"No such path: '{self.path}'")
|
|
831
|
+
return self
|
|
832
|
+
|
|
833
|
+
def should_not_exist(self) -> Self:
|
|
834
|
+
"""Raise FileExistsError if the path exists."""
|
|
835
|
+
if self.exists:
|
|
836
|
+
raise FileExistsError(f"Path already exists: '{self.path}'")
|
|
837
|
+
return self
|
|
838
|
+
|
|
839
|
+
def to_path(self) -> Path:
|
|
840
|
+
"""
|
|
841
|
+
Convert to pathlib.Path.
|
|
842
|
+
|
|
843
|
+
Returns:
|
|
844
|
+
Path: The pathlib.Path object.
|
|
845
|
+
"""
|
|
846
|
+
return Path(self.path)
|
|
847
|
+
|
|
848
|
+
def to_str(self) -> str:
|
|
849
|
+
"""
|
|
850
|
+
Convert to string.
|
|
851
|
+
|
|
852
|
+
Returns:
|
|
853
|
+
str: The path as a string.
|
|
854
|
+
"""
|
|
855
|
+
return str(self)
|
|
856
|
+
|
|
857
|
+
def stat(self, *, follow_symlinks: bool = True) -> os.stat_result:
|
|
858
|
+
"""
|
|
859
|
+
Return the stat info for this path.
|
|
860
|
+
|
|
861
|
+
Args:
|
|
862
|
+
follow_symlinks: If True, follow symlinks. If False, return info about the link itself.
|
|
863
|
+
|
|
864
|
+
Returns:
|
|
865
|
+
os.stat_result: The stat information.
|
|
866
|
+
"""
|
|
867
|
+
return os.stat(self.path, follow_symlinks=follow_symlinks)
|
|
868
|
+
|
|
869
|
+
def relative_to(self, other: str | PathBase) -> str:
|
|
870
|
+
"""
|
|
871
|
+
Make this path relative to another path.
|
|
872
|
+
|
|
873
|
+
Args:
|
|
874
|
+
other: The base path to make this path relative to.
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
str: The relative path.
|
|
878
|
+
|
|
879
|
+
Raises:
|
|
880
|
+
ValueError: If this path is not relative to the other path.
|
|
881
|
+
"""
|
|
882
|
+
if isinstance(other, PathBase):
|
|
883
|
+
other = other.path
|
|
884
|
+
other = os.path.abspath(other)
|
|
885
|
+
self_abs = os.path.abspath(self.path)
|
|
886
|
+
if not self_abs.startswith(other.rstrip(SEP) + SEP) and self_abs != other:
|
|
887
|
+
raise ValueError(f"'{self.path}' is not relative to '{other}'")
|
|
888
|
+
return os.path.relpath(self_abs, other)
|
|
889
|
+
|
|
890
|
+
def expanduser(self) -> PathBase:
|
|
891
|
+
"""
|
|
892
|
+
Expand ~ and ~user constructs in the path.
|
|
893
|
+
|
|
894
|
+
Returns:
|
|
895
|
+
PathBase: The object with the expanded path.
|
|
896
|
+
"""
|
|
897
|
+
self.path = os.path.expanduser(self.path)
|
|
898
|
+
return self
|
|
899
|
+
|
|
900
|
+
def as_posix(self) -> str:
|
|
901
|
+
"""Return the path with forward slashes."""
|
|
902
|
+
return self.path.replace("\\", "/")
|
|
903
|
+
|
|
904
|
+
def as_uri(self) -> str:
|
|
905
|
+
"""
|
|
906
|
+
Return the path as a file URI.
|
|
907
|
+
|
|
908
|
+
Returns:
|
|
909
|
+
str: The file URI (e.g., 'file:///path/to/file').
|
|
910
|
+
|
|
911
|
+
Raises:
|
|
912
|
+
ValueError: If the path is not absolute.
|
|
913
|
+
"""
|
|
914
|
+
if not self.is_absolute:
|
|
915
|
+
raise ValueError("relative path can't be expressed as a file URI")
|
|
916
|
+
return "file://" + pathname2url(os.path.abspath(self.path))
|
|
917
|
+
|
|
918
|
+
def match(self, pattern: str) -> bool:
|
|
919
|
+
"""
|
|
920
|
+
Check if the path matches the given glob pattern.
|
|
921
|
+
|
|
922
|
+
Args:
|
|
923
|
+
pattern: A glob pattern to match against.
|
|
924
|
+
|
|
925
|
+
Returns:
|
|
926
|
+
bool: True if the path matches the pattern.
|
|
927
|
+
"""
|
|
928
|
+
return fnmatch.fnmatch(self.basename, pattern)
|
|
929
|
+
|
|
930
|
+
def with_name(self, name: str) -> PathBase:
|
|
931
|
+
"""Return a new path with the name changed."""
|
|
932
|
+
parent = os.path.dirname(self.path)
|
|
933
|
+
if parent:
|
|
934
|
+
return self.__class__(f"{parent}{SEP}{name}")
|
|
935
|
+
return self.__class__(name)
|
|
936
|
+
|
|
937
|
+
@property
|
|
938
|
+
def exists(self) -> bool:
|
|
939
|
+
"""Check if the path exists."""
|
|
940
|
+
raise NotImplementedError
|
|
941
|
+
|
|
942
|
+
@property
|
|
943
|
+
def parent(self) -> Directory:
|
|
944
|
+
"""The parent directory."""
|
|
945
|
+
raise NotImplementedError
|
|
946
|
+
|
|
947
|
+
@property
|
|
948
|
+
def size(self) -> int:
|
|
949
|
+
raise NotImplementedError
|
|
950
|
+
|
|
951
|
+
@property
|
|
952
|
+
def size_readable(self) -> str:
|
|
953
|
+
raise NotImplementedError
|
|
954
|
+
|
|
955
|
+
def remove(self) -> Self:
|
|
956
|
+
raise NotImplementedError
|
|
957
|
+
|
|
958
|
+
def delete(self) -> Self:
|
|
959
|
+
return self.remove()
|
|
960
|
+
|
|
961
|
+
def create(self) -> Self:
|
|
962
|
+
raise NotImplementedError
|
|
963
|
+
|
|
964
|
+
def clear(self) -> Self:
|
|
965
|
+
raise NotImplementedError
|
|
966
|
+
|
|
967
|
+
def move_to(self, directory: str | Directory | PathLike, *, overwrite: bool = True) -> PathBase:
|
|
968
|
+
"""
|
|
969
|
+
Move the path to a new directory.
|
|
970
|
+
|
|
971
|
+
Args:
|
|
972
|
+
directory: The destination directory
|
|
973
|
+
overwrite: Whether to overwrite files if they already exist. Defaults to True.
|
|
974
|
+
|
|
975
|
+
Returns:
|
|
976
|
+
The updated PathBase object
|
|
977
|
+
"""
|
|
978
|
+
raise NotImplementedError
|
|
979
|
+
|
|
980
|
+
def copy_to(
|
|
981
|
+
self,
|
|
982
|
+
directory: str | Directory | PathLike,
|
|
983
|
+
*,
|
|
984
|
+
overwrite: bool = True,
|
|
985
|
+
mkdir: bool = False,
|
|
986
|
+
) -> PathBase:
|
|
987
|
+
"""
|
|
988
|
+
Copy the path to a new directory.
|
|
989
|
+
|
|
990
|
+
Args:
|
|
991
|
+
directory: The destination directory
|
|
992
|
+
overwrite: Whether to overwrite files if they already exist. Defaults to True.
|
|
993
|
+
mkdir: Whether to create the destination directory if it doesn't exist. Defaults to False.
|
|
994
|
+
|
|
995
|
+
Returns:
|
|
996
|
+
The updated PathBase object
|
|
997
|
+
"""
|
|
998
|
+
raise NotImplementedError
|
|
999
|
+
|
|
1000
|
+
if sys.platform != "win32":
|
|
1001
|
+
|
|
1002
|
+
def get_xattr(self, name: str, group: str = "user") -> str:
|
|
1003
|
+
"""
|
|
1004
|
+
Retrieve the value of an extended attribute.
|
|
1005
|
+
|
|
1006
|
+
Args:
|
|
1007
|
+
name: The name of the extended attribute.
|
|
1008
|
+
group: The group of the extended attribute. Defaults to "user".
|
|
1009
|
+
|
|
1010
|
+
Returns:
|
|
1011
|
+
The value of the extended attribute.
|
|
1012
|
+
"""
|
|
1013
|
+
return os.getxattr(self.path, f"{group}.{name}").decode()
|
|
1014
|
+
|
|
1015
|
+
def set_xattr(self, value: str | bytes, name: str, group: str = "user") -> PathBase:
|
|
1016
|
+
"""
|
|
1017
|
+
Set an extended attribute.
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
value: The value of the extended attribute.
|
|
1021
|
+
name: The name of the extended attribute.
|
|
1022
|
+
group: The group of the extended attribute. Defaults to "user".
|
|
1023
|
+
"""
|
|
1024
|
+
if isinstance(value, str):
|
|
1025
|
+
value = value.encode()
|
|
1026
|
+
os.setxattr(self.path, f"{group}.{name}", value)
|
|
1027
|
+
return self
|
|
1028
|
+
|
|
1029
|
+
def remove_xattr(self, name: str, group: str = "user") -> PathBase:
|
|
1030
|
+
"""
|
|
1031
|
+
Remove an extended attribute.
|
|
1032
|
+
|
|
1033
|
+
Args:
|
|
1034
|
+
name: The name of the extended attribute.
|
|
1035
|
+
group: The group of the extended attribute. Defaults to "user".
|
|
1036
|
+
"""
|
|
1037
|
+
os.removexattr(self.path, f"{group}.{name}")
|
|
1038
|
+
return self
|
|
1039
|
+
|
|
1040
|
+
|
|
1041
|
+
class File(PathBase):
|
|
1042
|
+
def __init__(
|
|
1043
|
+
self,
|
|
1044
|
+
path: str | PathLike,
|
|
1045
|
+
encoding: str = "utf-8",
|
|
1046
|
+
*,
|
|
1047
|
+
abs: bool = False,
|
|
1048
|
+
) -> None:
|
|
1049
|
+
"""
|
|
1050
|
+
Initialize a File object.
|
|
1051
|
+
|
|
1052
|
+
Args:
|
|
1053
|
+
path (str | PathLike): File path.
|
|
1054
|
+
encoding (str, optional): The file's encoding.
|
|
1055
|
+
abs (bool): Whether to use the absolute path.
|
|
1056
|
+
"""
|
|
1057
|
+
super().__init__(path, abs=abs)
|
|
1058
|
+
self.encoding = encoding
|
|
1059
|
+
|
|
1060
|
+
@property
|
|
1061
|
+
def exists(self) -> bool:
|
|
1062
|
+
return os.path.isfile(self.path)
|
|
1063
|
+
|
|
1064
|
+
@property
|
|
1065
|
+
def parent(self) -> Directory:
|
|
1066
|
+
"""The parent directory."""
|
|
1067
|
+
return Directory(os.path.dirname(self.path))
|
|
1068
|
+
|
|
1069
|
+
@property
|
|
1070
|
+
def dirname(self) -> str:
|
|
1071
|
+
"""The file's directory name."""
|
|
1072
|
+
return os.path.dirname(self.path)
|
|
1073
|
+
|
|
1074
|
+
@property
|
|
1075
|
+
def ext(self) -> str:
|
|
1076
|
+
"""
|
|
1077
|
+
The file's extension (without the dot).
|
|
1078
|
+
Returns empty string if the file has no extension.
|
|
1079
|
+
"""
|
|
1080
|
+
if "." in self.basename:
|
|
1081
|
+
return self.basename.split(".")[-1]
|
|
1082
|
+
return ""
|
|
1083
|
+
|
|
1084
|
+
@property
|
|
1085
|
+
def suffix(self) -> str:
|
|
1086
|
+
"""The file extension WITH the dot (e.g., '.txt'). Empty string if no extension."""
|
|
1087
|
+
base = os.path.basename(self.path)
|
|
1088
|
+
if "." in base and not base.startswith("."):
|
|
1089
|
+
return "." + base.rsplit(".", 1)[-1]
|
|
1090
|
+
if base.startswith(".") and base.count(".") > 1:
|
|
1091
|
+
return "." + base.rsplit(".", 1)[-1]
|
|
1092
|
+
return ""
|
|
1093
|
+
|
|
1094
|
+
@property
|
|
1095
|
+
def stem(self) -> str:
|
|
1096
|
+
"""The file's stem (base name without extension)."""
|
|
1097
|
+
base = self.basename
|
|
1098
|
+
if "." not in base:
|
|
1099
|
+
return base
|
|
1100
|
+
return ".".join(base.split(".")[:-1])
|
|
1101
|
+
|
|
1102
|
+
@property
|
|
1103
|
+
def size(self) -> int:
|
|
1104
|
+
"""The file's size in bytes."""
|
|
1105
|
+
return os.path.getsize(self.path)
|
|
1106
|
+
|
|
1107
|
+
@property
|
|
1108
|
+
def size_readable(self) -> str:
|
|
1109
|
+
"""The file's size in a human-readable format if readable is set to True."""
|
|
1110
|
+
return bytes_readable(self.size)
|
|
1111
|
+
|
|
1112
|
+
def create(self) -> File:
|
|
1113
|
+
"""Create an empty file if it doesn't exist."""
|
|
1114
|
+
if self.exists:
|
|
1115
|
+
return self
|
|
1116
|
+
open(self.path, "a", encoding=self.encoding).close()
|
|
1117
|
+
return self
|
|
1118
|
+
|
|
1119
|
+
def remove(self) -> File:
|
|
1120
|
+
"""Remove the file."""
|
|
1121
|
+
if not self.exists:
|
|
1122
|
+
return self
|
|
1123
|
+
os.remove(self.path)
|
|
1124
|
+
return self
|
|
1125
|
+
|
|
1126
|
+
def clear(self) -> File:
|
|
1127
|
+
"""Clear the contents of a file if it exists."""
|
|
1128
|
+
if not self.exists:
|
|
1129
|
+
return self
|
|
1130
|
+
open(self.path, "w", encoding=self.encoding).close()
|
|
1131
|
+
return self
|
|
1132
|
+
|
|
1133
|
+
def read(self, mode: Literal["r", "rb"] = "r") -> str | bytes:
|
|
1134
|
+
"""
|
|
1135
|
+
Read the contents of a file.
|
|
1136
|
+
|
|
1137
|
+
Args:
|
|
1138
|
+
mode: The mode to open the file in. Use 'r' for text, 'rb' for binary.
|
|
1139
|
+
|
|
1140
|
+
Returns:
|
|
1141
|
+
str | bytes: The file contents as string (text mode) or bytes (binary mode).
|
|
1142
|
+
"""
|
|
1143
|
+
if mode == "rb":
|
|
1144
|
+
with open(self.path, mode) as f:
|
|
1145
|
+
return f.read()
|
|
1146
|
+
with open(self.path, mode, encoding=self.encoding) as f:
|
|
1147
|
+
return f.read()
|
|
1148
|
+
|
|
1149
|
+
def _write(self, data: str | bytes, mode: str, *, newline: bool = True) -> None:
|
|
1150
|
+
if "b" in mode:
|
|
1151
|
+
with open(self.path, mode) as f:
|
|
1152
|
+
f.write(data)
|
|
1153
|
+
else:
|
|
1154
|
+
with open(self.path, mode, encoding=self.encoding) as f:
|
|
1155
|
+
f.write(data)
|
|
1156
|
+
if newline:
|
|
1157
|
+
f.write("\n")
|
|
1158
|
+
|
|
1159
|
+
def _write_iter(self, data: Iterable[Any], mode: str, sep: str = "\n") -> None:
|
|
1160
|
+
with open(self.path, mode, encoding=self.encoding) as f:
|
|
1161
|
+
for entry in data:
|
|
1162
|
+
f.write(f"{entry}{sep}")
|
|
1163
|
+
|
|
1164
|
+
def write(
|
|
1165
|
+
self,
|
|
1166
|
+
data: str | bytes,
|
|
1167
|
+
*,
|
|
1168
|
+
mode: Literal["w", "wb"] = "w",
|
|
1169
|
+
newline: bool = True,
|
|
1170
|
+
) -> File:
|
|
1171
|
+
"""
|
|
1172
|
+
Write data to a file, overwriting any existing data.
|
|
1173
|
+
|
|
1174
|
+
Args:
|
|
1175
|
+
data: The data to write.
|
|
1176
|
+
mode: The mode to open the file in. Use 'w' for text, 'wb' for binary.
|
|
1177
|
+
newline: Whether to add a newline at the end. Ignored in binary mode.
|
|
1178
|
+
|
|
1179
|
+
Returns:
|
|
1180
|
+
File: The File object for method chaining.
|
|
1181
|
+
"""
|
|
1182
|
+
self._write(data, mode, newline=newline)
|
|
1183
|
+
return self
|
|
1184
|
+
|
|
1185
|
+
def append(
|
|
1186
|
+
self,
|
|
1187
|
+
data: str | bytes,
|
|
1188
|
+
*,
|
|
1189
|
+
mode: Literal["a", "ab"] = "a",
|
|
1190
|
+
newline: bool = True,
|
|
1191
|
+
) -> File:
|
|
1192
|
+
"""
|
|
1193
|
+
Append data to a file.
|
|
1194
|
+
|
|
1195
|
+
Args:
|
|
1196
|
+
data: The data to append.
|
|
1197
|
+
mode: The mode to open the file in. Use 'a' for text, 'ab' for binary.
|
|
1198
|
+
newline: Whether to add a newline at the end. Ignored in binary mode.
|
|
1199
|
+
|
|
1200
|
+
Returns:
|
|
1201
|
+
File: The File object for method chaining.
|
|
1202
|
+
"""
|
|
1203
|
+
self._write(data, mode, newline=newline)
|
|
1204
|
+
return self
|
|
1205
|
+
|
|
1206
|
+
def open(self, mode: str = "r", encoding: str | None = None, **kwargs: Any) -> IO:
|
|
1207
|
+
"""
|
|
1208
|
+
Open the file and return a file handle.
|
|
1209
|
+
|
|
1210
|
+
Args:
|
|
1211
|
+
mode: The mode to open the file in.
|
|
1212
|
+
encoding: The encoding to use. Defaults to self.encoding for text modes.
|
|
1213
|
+
**kwargs: Additional arguments passed to the built-in open().
|
|
1214
|
+
|
|
1215
|
+
Returns:
|
|
1216
|
+
IO: A file handle.
|
|
1217
|
+
"""
|
|
1218
|
+
if "b" in mode:
|
|
1219
|
+
return open(self.path, mode, **kwargs)
|
|
1220
|
+
if encoding is None:
|
|
1221
|
+
encoding = self.encoding
|
|
1222
|
+
return open(self.path, mode, encoding=encoding, **kwargs)
|
|
1223
|
+
|
|
1224
|
+
def write_iter(self, data: Iterable[Any], sep: str = "\n") -> None:
|
|
1225
|
+
"""
|
|
1226
|
+
Write data from an iterable to a file, overwriting any existing data.
|
|
1227
|
+
|
|
1228
|
+
Args:
|
|
1229
|
+
data (Iterable): The data to write.
|
|
1230
|
+
sep (str, optional): The separator to use between items.
|
|
1231
|
+
"""
|
|
1232
|
+
self._write_iter(data, "w", sep=sep)
|
|
1233
|
+
|
|
1234
|
+
def append_iter(self, data: Iterable[Any], sep: str = "\n") -> None:
|
|
1235
|
+
"""
|
|
1236
|
+
Append data from an iterable to a file.
|
|
1237
|
+
|
|
1238
|
+
Args:
|
|
1239
|
+
data (Iterable): The data to append.
|
|
1240
|
+
sep (str, optional): The separator to use between items.
|
|
1241
|
+
"""
|
|
1242
|
+
self._write_iter(data, "a", sep=sep)
|
|
1243
|
+
|
|
1244
|
+
def readlines(self) -> list[str]:
|
|
1245
|
+
"""Equivalent to TextIOWrapper.readlines()"""
|
|
1246
|
+
with open(self.path, encoding=self.encoding) as f:
|
|
1247
|
+
return f.readlines()
|
|
1248
|
+
|
|
1249
|
+
def splitlines(self) -> list[str]:
|
|
1250
|
+
"""Equivalent to File.read().splitlines()"""
|
|
1251
|
+
return self.read(mode="r").splitlines() # type:ignore
|
|
1252
|
+
|
|
1253
|
+
def move_to(
|
|
1254
|
+
self,
|
|
1255
|
+
directory: str | Directory | PathLike,
|
|
1256
|
+
*,
|
|
1257
|
+
mkdir: bool = False,
|
|
1258
|
+
overwrite: bool = True,
|
|
1259
|
+
) -> File:
|
|
1260
|
+
"""
|
|
1261
|
+
Move the file to a new directory.
|
|
1262
|
+
|
|
1263
|
+
Args:
|
|
1264
|
+
directory (str): The destination directory.
|
|
1265
|
+
mkdir (bool, optional): Whether to create the directory if it doesn't exist. Defaults to False.
|
|
1266
|
+
overwrite (bool, optional): Whether to overwrite the file if it already exists in the destination directory. Defaults to True.
|
|
1267
|
+
"""
|
|
1268
|
+
directory = os.fspath(directory)
|
|
1269
|
+
if not os.path.isdir(directory):
|
|
1270
|
+
if mkdir:
|
|
1271
|
+
os.mkdir(directory)
|
|
1272
|
+
else:
|
|
1273
|
+
raise FileNotFoundError(f"No such directory: '{directory}'")
|
|
1274
|
+
|
|
1275
|
+
move_path = f"{directory}{SEP}{self.basename}"
|
|
1276
|
+
if os.path.exists(move_path) and not overwrite:
|
|
1277
|
+
raise FileExistsError(move_path)
|
|
1278
|
+
os.rename(self.path, move_path)
|
|
1279
|
+
self.path = move_path
|
|
1280
|
+
return self
|
|
1281
|
+
|
|
1282
|
+
def copy_to(
|
|
1283
|
+
self,
|
|
1284
|
+
directory: str | Directory | PathLike,
|
|
1285
|
+
*,
|
|
1286
|
+
mkdir: bool = False,
|
|
1287
|
+
overwrite: bool = True,
|
|
1288
|
+
) -> File:
|
|
1289
|
+
"""
|
|
1290
|
+
Copy the file to a new directory.
|
|
1291
|
+
|
|
1292
|
+
Args:
|
|
1293
|
+
directory (str): The destination directory.
|
|
1294
|
+
mkdir (bool, optional): Whether to create the directory if it doesn't exist. Defaults to False.
|
|
1295
|
+
overwrite (bool, optional): Whether to overwrite the file if it already exists in the destination directory. Defaults to True.
|
|
1296
|
+
"""
|
|
1297
|
+
directory = os.fspath(directory)
|
|
1298
|
+
if not os.path.isdir(directory):
|
|
1299
|
+
if mkdir:
|
|
1300
|
+
os.mkdir(directory)
|
|
1301
|
+
else:
|
|
1302
|
+
raise FileNotFoundError(f"No such directory: '{directory}'")
|
|
1303
|
+
|
|
1304
|
+
copy_path = f"{directory}{SEP}{self.basename}"
|
|
1305
|
+
if os.path.exists(copy_path) and not overwrite:
|
|
1306
|
+
raise FileExistsError(copy_path)
|
|
1307
|
+
self.path = shutil.copy2(self.path, directory)
|
|
1308
|
+
return self
|
|
1309
|
+
|
|
1310
|
+
def with_dir(self, directory: str) -> File:
|
|
1311
|
+
"""
|
|
1312
|
+
Change the directory of the file object. This will not move the actual file to that directory.
|
|
1313
|
+
Use File.move_to for that.
|
|
1314
|
+
"""
|
|
1315
|
+
self.path = f"{directory}{SEP}{self.basename}"
|
|
1316
|
+
return self
|
|
1317
|
+
|
|
1318
|
+
def with_ext(self, ext: str) -> File:
|
|
1319
|
+
"""
|
|
1320
|
+
Change the extension of the file and return the new File object
|
|
1321
|
+
|
|
1322
|
+
Args:
|
|
1323
|
+
ext (str): The new extension of the file.
|
|
1324
|
+
|
|
1325
|
+
Returns:
|
|
1326
|
+
File: The File object with the new extension.
|
|
1327
|
+
"""
|
|
1328
|
+
if not ext.startswith("."):
|
|
1329
|
+
ext = f".{ext}"
|
|
1330
|
+
self.path = f"{self.dirname}{SEP}{self.stem}{ext}"
|
|
1331
|
+
return self
|
|
1332
|
+
|
|
1333
|
+
def with_suffix(self, suffix: str) -> File:
|
|
1334
|
+
"""Add a suffix to the file's name and return the new File object."""
|
|
1335
|
+
ext = self.ext
|
|
1336
|
+
if ext:
|
|
1337
|
+
ext = f".{ext}"
|
|
1338
|
+
filename = f"{self.stem}{suffix}{ext}"
|
|
1339
|
+
self.path = f"{self.dirname}{SEP}{filename}"
|
|
1340
|
+
return self
|
|
1341
|
+
|
|
1342
|
+
def with_prefix(self, prefix: str) -> File:
|
|
1343
|
+
"""Add a prefix to the file's name and return the new File object."""
|
|
1344
|
+
ext = self.ext
|
|
1345
|
+
if ext:
|
|
1346
|
+
ext = f".{ext}"
|
|
1347
|
+
filename = f"{prefix}{self.stem}{ext}"
|
|
1348
|
+
self.path = f"{self.dirname}{SEP}{filename}"
|
|
1349
|
+
return self
|
|
1350
|
+
|
|
1351
|
+
def with_stem(self, stem: str) -> File:
|
|
1352
|
+
"""Return a new File with the stem changed, keeping the suffix."""
|
|
1353
|
+
suffix = self.suffix
|
|
1354
|
+
return File(f"{self.dirname}{SEP}{stem}{suffix}", encoding=self.encoding)
|
|
1355
|
+
|
|
1356
|
+
def rename(self, name: str) -> File:
|
|
1357
|
+
"""Rename the file and return the new File object."""
|
|
1358
|
+
new_path = f"{self.dirname}{SEP}{name}"
|
|
1359
|
+
os.rename(self.path, new_path)
|
|
1360
|
+
self.path = new_path
|
|
1361
|
+
return self
|
|
1362
|
+
|
|
1363
|
+
def link(self, target: str, follow_symlinks: bool = True) -> File:
|
|
1364
|
+
"""Create a hard link to the file."""
|
|
1365
|
+
os.link(self.path, target, follow_symlinks=follow_symlinks)
|
|
1366
|
+
return self
|
|
1367
|
+
|
|
1368
|
+
def symlink(self, target: str) -> File:
|
|
1369
|
+
"""Create a symbolic link to the file."""
|
|
1370
|
+
os.symlink(self.path, target)
|
|
1371
|
+
return self
|
|
1372
|
+
|
|
1373
|
+
@classmethod
|
|
1374
|
+
def rand(cls, prefix: str = "file", ext: str = "") -> File:
|
|
1375
|
+
"""
|
|
1376
|
+
Create a new random file with a specified prefix and extension.
|
|
1377
|
+
|
|
1378
|
+
Args:
|
|
1379
|
+
prefix (str, optional): The prefix for the file name.
|
|
1380
|
+
ext (str, optional): The extension for the file name.
|
|
1381
|
+
|
|
1382
|
+
Returns:
|
|
1383
|
+
File: A new File object with a random name.
|
|
1384
|
+
"""
|
|
1385
|
+
return File(rand_filename(prefix, ext))
|
|
1386
|
+
|
|
1387
|
+
|
|
1388
|
+
class Directory(PathBase):
|
|
1389
|
+
def __init__(
|
|
1390
|
+
self,
|
|
1391
|
+
path: str | PathLike,
|
|
1392
|
+
*,
|
|
1393
|
+
abs: bool = False,
|
|
1394
|
+
) -> None:
|
|
1395
|
+
"""
|
|
1396
|
+
Initialize a Directory object.
|
|
1397
|
+
|
|
1398
|
+
Args:
|
|
1399
|
+
path (str | PathLike): Directory path.
|
|
1400
|
+
abs (bool): Whether to use the absolute path.
|
|
1401
|
+
"""
|
|
1402
|
+
super().__init__(path, abs=abs)
|
|
1403
|
+
|
|
1404
|
+
def __truediv__(self, name: str) -> Directory:
|
|
1405
|
+
return self.directory(name)
|
|
1406
|
+
|
|
1407
|
+
def __floordiv__(self, name: str) -> File:
|
|
1408
|
+
return self.file(name)
|
|
1409
|
+
|
|
1410
|
+
@property
|
|
1411
|
+
def size(self) -> int:
|
|
1412
|
+
return get_dir_size(self.path, readable=False) # type:ignore
|
|
1413
|
+
|
|
1414
|
+
@property
|
|
1415
|
+
def exists(self) -> bool:
|
|
1416
|
+
"""Check if the directory exists."""
|
|
1417
|
+
return os.path.isdir(self.path)
|
|
1418
|
+
|
|
1419
|
+
def create(self, mode: int = 0o777, exist_ok: bool = True) -> Directory:
|
|
1420
|
+
"""Create directory (including parents)."""
|
|
1421
|
+
os.makedirs(self.path, mode=mode, exist_ok=exist_ok)
|
|
1422
|
+
return self
|
|
1423
|
+
|
|
1424
|
+
@property
|
|
1425
|
+
def parent(self) -> Directory:
|
|
1426
|
+
return Directory(os.path.dirname(self.path))
|
|
1427
|
+
|
|
1428
|
+
@classmethod
|
|
1429
|
+
def rand(cls, prefix: str = "dir") -> Directory:
|
|
1430
|
+
return Directory(joinpath(os.getcwd(), safe_filename(rand_filename(prefix))))
|
|
1431
|
+
|
|
1432
|
+
def move_to(
|
|
1433
|
+
self,
|
|
1434
|
+
directory: str | PathLike | Directory,
|
|
1435
|
+
*,
|
|
1436
|
+
overwrite: bool = True,
|
|
1437
|
+
) -> Directory:
|
|
1438
|
+
"""
|
|
1439
|
+
Move this directory to become a child of the target location.
|
|
1440
|
+
|
|
1441
|
+
Args:
|
|
1442
|
+
directory: The destination directory where this directory will be moved
|
|
1443
|
+
overwrite: Whether to overwrite if target path already exists. Defaults to True.
|
|
1444
|
+
|
|
1445
|
+
Returns:
|
|
1446
|
+
The updated Directory object
|
|
1447
|
+
"""
|
|
1448
|
+
directory = os.fspath(directory)
|
|
1449
|
+
if not os.path.exists(directory):
|
|
1450
|
+
raise FileNotFoundError(f"Destination directory does not exist: {directory}")
|
|
1451
|
+
|
|
1452
|
+
dest = f"{directory}{SEP}{self.basename}"
|
|
1453
|
+
shutil.move(self.path, dest)
|
|
1454
|
+
self.path = dest
|
|
1455
|
+
return self
|
|
1456
|
+
|
|
1457
|
+
def copy_to(
|
|
1458
|
+
self,
|
|
1459
|
+
directory: str | PathLike | Directory,
|
|
1460
|
+
*,
|
|
1461
|
+
overwrite: bool = True,
|
|
1462
|
+
mkdir: bool = False,
|
|
1463
|
+
) -> Directory:
|
|
1464
|
+
"""
|
|
1465
|
+
Copy this directory to become a child of the target location.
|
|
1466
|
+
|
|
1467
|
+
Args:
|
|
1468
|
+
directory: The destination directory where this directory will be copied
|
|
1469
|
+
overwrite: Whether to overwrite if target path already exists. Defaults to True.
|
|
1470
|
+
mkdir: Whether to create the destination directory if it doesn't exist. Defaults to False.
|
|
1471
|
+
|
|
1472
|
+
Returns:
|
|
1473
|
+
The updated Directory object
|
|
1474
|
+
"""
|
|
1475
|
+
directory = os.fspath(directory)
|
|
1476
|
+
if not os.path.exists(directory):
|
|
1477
|
+
if mkdir:
|
|
1478
|
+
os.makedirs(directory)
|
|
1479
|
+
else:
|
|
1480
|
+
raise FileNotFoundError(f"Destination directory does not exist: {directory}")
|
|
1481
|
+
|
|
1482
|
+
dest = f"{directory}{SEP}{self.basename}"
|
|
1483
|
+
shutil.copytree(self.path, dest)
|
|
1484
|
+
self.path = dest
|
|
1485
|
+
return self
|
|
1486
|
+
|
|
1487
|
+
def remove(self) -> Directory:
|
|
1488
|
+
"""Remove the directory and all its contents."""
|
|
1489
|
+
if not self.exists:
|
|
1490
|
+
return self
|
|
1491
|
+
shutil.rmtree(self.path)
|
|
1492
|
+
return self
|
|
1493
|
+
|
|
1494
|
+
def yield_files(
|
|
1495
|
+
self,
|
|
1496
|
+
ext: str | tuple[str, ...] | None = None,
|
|
1497
|
+
glob: str | re.Pattern[str] | None = None,
|
|
1498
|
+
regex: str | re.Pattern[str] | None = None,
|
|
1499
|
+
recursive: bool = True,
|
|
1500
|
+
) -> Generator[File, None, None]:
|
|
1501
|
+
"""
|
|
1502
|
+
Yield files in the directory.
|
|
1503
|
+
|
|
1504
|
+
All filter parameters use AND logic - files must match all specified filters.
|
|
1505
|
+
|
|
1506
|
+
Args:
|
|
1507
|
+
ext: If provided, only yield files with provided extensions.
|
|
1508
|
+
glob: Glob pattern as string or compiled regex from fnmatch.translate().
|
|
1509
|
+
regex: Regex pattern as string or compiled re.Pattern.
|
|
1510
|
+
recursive: Whether to search recursively.
|
|
1511
|
+
|
|
1512
|
+
Yields:
|
|
1513
|
+
Generator[File, None, None]: The files in the directory.
|
|
1514
|
+
"""
|
|
1515
|
+
glob_pattern = re.compile(fnmatch.translate(glob)) if isinstance(glob, str) else glob
|
|
1516
|
+
regex_pattern = re.compile(regex) if isinstance(regex, str) else regex
|
|
1517
|
+
for file_path in yield_files_in(self.path, ext=ext, recursive=recursive):
|
|
1518
|
+
basename = os.path.basename(file_path)
|
|
1519
|
+
if glob_pattern and not glob_pattern.match(basename):
|
|
1520
|
+
continue
|
|
1521
|
+
if regex_pattern and not regex_pattern.search(basename):
|
|
1522
|
+
continue
|
|
1523
|
+
yield File(file_path)
|
|
1524
|
+
|
|
1525
|
+
def get_files(
|
|
1526
|
+
self,
|
|
1527
|
+
ext: str | tuple[str, ...] | None = None,
|
|
1528
|
+
glob: str | re.Pattern[str] | None = None,
|
|
1529
|
+
regex: str | re.Pattern[str] | None = None,
|
|
1530
|
+
recursive: bool = True,
|
|
1531
|
+
) -> list[File]:
|
|
1532
|
+
"""
|
|
1533
|
+
Get files in the directory.
|
|
1534
|
+
|
|
1535
|
+
All filter parameters use AND logic - files must match all specified filters.
|
|
1536
|
+
|
|
1537
|
+
Args:
|
|
1538
|
+
ext: If provided, only yield files with provided extensions.
|
|
1539
|
+
glob: Glob pattern as string or compiled regex from fnmatch.translate().
|
|
1540
|
+
regex: Regex pattern as string or compiled re.Pattern.
|
|
1541
|
+
recursive: Whether to search recursively.
|
|
1542
|
+
|
|
1543
|
+
Returns:
|
|
1544
|
+
list[File]: The files in the directory.
|
|
1545
|
+
"""
|
|
1546
|
+
return list(self.yield_files(ext=ext, glob=glob, regex=regex, recursive=recursive))
|
|
1547
|
+
|
|
1548
|
+
def yield_subdirs(
|
|
1549
|
+
self,
|
|
1550
|
+
glob: str | re.Pattern[str] | None = None,
|
|
1551
|
+
regex: str | re.Pattern[str] | None = None,
|
|
1552
|
+
recursive: bool = True,
|
|
1553
|
+
) -> Generator[Directory, None, None]:
|
|
1554
|
+
"""
|
|
1555
|
+
Yield subdirectories in the directory.
|
|
1556
|
+
|
|
1557
|
+
All filter parameters use AND logic - directories must match all specified filters.
|
|
1558
|
+
|
|
1559
|
+
Args:
|
|
1560
|
+
glob: Glob pattern as string or compiled regex from fnmatch.translate().
|
|
1561
|
+
regex: Regex pattern as string or compiled re.Pattern.
|
|
1562
|
+
recursive: Whether to search recursively.
|
|
1563
|
+
|
|
1564
|
+
Yields:
|
|
1565
|
+
Generator[Directory, None, None]: The subdirectories in the directory.
|
|
1566
|
+
"""
|
|
1567
|
+
glob_pattern = re.compile(fnmatch.translate(glob)) if isinstance(glob, str) else glob
|
|
1568
|
+
regex_pattern = re.compile(regex) if isinstance(regex, str) else regex
|
|
1569
|
+
for dir_path in yield_dirs_in(self.path, recursive=recursive):
|
|
1570
|
+
basename = os.path.basename(dir_path)
|
|
1571
|
+
if glob_pattern and not glob_pattern.match(basename):
|
|
1572
|
+
continue
|
|
1573
|
+
if regex_pattern and not regex_pattern.search(basename):
|
|
1574
|
+
continue
|
|
1575
|
+
yield Directory(dir_path)
|
|
1576
|
+
|
|
1577
|
+
def get_subdirs(
|
|
1578
|
+
self,
|
|
1579
|
+
glob: str | re.Pattern[str] | None = None,
|
|
1580
|
+
regex: str | re.Pattern[str] | None = None,
|
|
1581
|
+
recursive: bool = True,
|
|
1582
|
+
) -> list[Directory]:
|
|
1583
|
+
"""
|
|
1584
|
+
Get subdirectories in the directory.
|
|
1585
|
+
|
|
1586
|
+
All filter parameters use AND logic - directories must match all specified filters.
|
|
1587
|
+
|
|
1588
|
+
Args:
|
|
1589
|
+
glob: Glob pattern as string or compiled regex from fnmatch.translate().
|
|
1590
|
+
regex: Regex pattern as string or compiled re.Pattern.
|
|
1591
|
+
recursive: Whether to search recursively.
|
|
1592
|
+
|
|
1593
|
+
Returns:
|
|
1594
|
+
list[Directory]: The subdirectories in the directory.
|
|
1595
|
+
"""
|
|
1596
|
+
return list(self.yield_subdirs(glob=glob, regex=regex, recursive=recursive))
|
|
1597
|
+
|
|
1598
|
+
def file(self, name: str) -> File:
|
|
1599
|
+
return File(joinpath(self.path, safe_filename(name)))
|
|
1600
|
+
|
|
1601
|
+
def directory(self, name: str) -> Directory:
|
|
1602
|
+
return Directory(joinpath(self.path, safe_filename(name)))
|
|
1603
|
+
|
|
1604
|
+
@classmethod
|
|
1605
|
+
def home(cls) -> Directory:
|
|
1606
|
+
return Directory(HOME)
|
|
1607
|
+
|
|
1608
|
+
@classmethod
|
|
1609
|
+
def cwd(cls) -> Directory:
|
|
1610
|
+
return Directory(os.getcwd())
|
|
1611
|
+
|
|
1612
|
+
|
|
1613
|
+
class EXT:
|
|
1614
|
+
AUDIO = (
|
|
1615
|
+
".mp3",
|
|
1616
|
+
".aac",
|
|
1617
|
+
".ogg",
|
|
1618
|
+
".flac",
|
|
1619
|
+
".wav",
|
|
1620
|
+
".aiff",
|
|
1621
|
+
".dsd",
|
|
1622
|
+
".pcm",
|
|
1623
|
+
)
|
|
1624
|
+
IMAGE = (
|
|
1625
|
+
".jpg",
|
|
1626
|
+
".png",
|
|
1627
|
+
".jpeg",
|
|
1628
|
+
".webp",
|
|
1629
|
+
".gif",
|
|
1630
|
+
".bmp",
|
|
1631
|
+
".tif",
|
|
1632
|
+
".tiff",
|
|
1633
|
+
".jfif",
|
|
1634
|
+
".heic",
|
|
1635
|
+
".dib",
|
|
1636
|
+
".jp2",
|
|
1637
|
+
".jpx",
|
|
1638
|
+
".j2k",
|
|
1639
|
+
".jxl",
|
|
1640
|
+
)
|
|
1641
|
+
VIDEO = (
|
|
1642
|
+
".mp4",
|
|
1643
|
+
".mkv",
|
|
1644
|
+
".avi",
|
|
1645
|
+
".flv",
|
|
1646
|
+
".mov",
|
|
1647
|
+
".webm",
|
|
1648
|
+
".mpg",
|
|
1649
|
+
".mpeg",
|
|
1650
|
+
".mpe",
|
|
1651
|
+
".mpv",
|
|
1652
|
+
".ogg",
|
|
1653
|
+
".m4p",
|
|
1654
|
+
".m4v",
|
|
1655
|
+
".wmv",
|
|
1656
|
+
".f4v",
|
|
1657
|
+
".swf",
|
|
1658
|
+
)
|
|
1659
|
+
|
|
1660
|
+
|
|
1661
|
+
__all__ = [
|
|
1662
|
+
"EXT",
|
|
1663
|
+
"File",
|
|
1664
|
+
"Directory",
|
|
1665
|
+
"PathLike",
|
|
1666
|
+
"mkdir",
|
|
1667
|
+
"pickle_load",
|
|
1668
|
+
"pickle_dump",
|
|
1669
|
+
"json_load",
|
|
1670
|
+
"json_append",
|
|
1671
|
+
"yaml_load",
|
|
1672
|
+
"json_dump",
|
|
1673
|
+
"yaml_dump",
|
|
1674
|
+
"get_dir_size",
|
|
1675
|
+
"move_files",
|
|
1676
|
+
"rand_filename",
|
|
1677
|
+
"bytes_readable",
|
|
1678
|
+
"readable_size_to_bytes",
|
|
1679
|
+
"windows_has_drive",
|
|
1680
|
+
"is_wsl",
|
|
1681
|
+
"mkdirs",
|
|
1682
|
+
"yield_files_in",
|
|
1683
|
+
"get_files_in",
|
|
1684
|
+
"yield_dirs_in",
|
|
1685
|
+
"get_dirs_in",
|
|
1686
|
+
"ensure_paths_exist",
|
|
1687
|
+
"exec_cmd",
|
|
1688
|
+
"SEP",
|
|
1689
|
+
"HOME",
|
|
1690
|
+
"abspath",
|
|
1691
|
+
"basename",
|
|
1692
|
+
"dirname",
|
|
1693
|
+
"joinpath",
|
|
1694
|
+
"splitpath",
|
|
1695
|
+
"start_file",
|
|
1696
|
+
"read_piped",
|
|
1697
|
+
"isdir",
|
|
1698
|
+
"isfile",
|
|
1699
|
+
"islink",
|
|
1700
|
+
"exists",
|
|
1701
|
+
"stat",
|
|
1702
|
+
"link",
|
|
1703
|
+
"getcwd",
|
|
1704
|
+
"chdir",
|
|
1705
|
+
"chmod",
|
|
1706
|
+
"remove",
|
|
1707
|
+
"rename",
|
|
1708
|
+
"copy",
|
|
1709
|
+
"move",
|
|
1710
|
+
"chown",
|
|
1711
|
+
]
|