jetpytools 1.2.3__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.
Potentially problematic release.
This version of jetpytools might be problematic. Click here for more details.
- jetpytools/__init__.py +5 -0
- jetpytools/_metadata.py +12 -0
- jetpytools/enums/__init__.py +2 -0
- jetpytools/enums/base.py +78 -0
- jetpytools/enums/other.py +59 -0
- jetpytools/exceptions/__init__.py +5 -0
- jetpytools/exceptions/base.py +213 -0
- jetpytools/exceptions/enum.py +11 -0
- jetpytools/exceptions/file.py +38 -0
- jetpytools/exceptions/generic.py +45 -0
- jetpytools/exceptions/module.py +39 -0
- jetpytools/functions/__init__.py +3 -0
- jetpytools/functions/funcs.py +152 -0
- jetpytools/functions/normalize.py +254 -0
- jetpytools/functions/other.py +18 -0
- jetpytools/py.typed +0 -0
- jetpytools/types/__init__.py +6 -0
- jetpytools/types/builtins.py +77 -0
- jetpytools/types/file.py +193 -0
- jetpytools/types/funcs.py +109 -0
- jetpytools/types/generic.py +52 -0
- jetpytools/types/supports.py +127 -0
- jetpytools/types/utils.py +669 -0
- jetpytools/utils/__init__.py +4 -0
- jetpytools/utils/file.py +256 -0
- jetpytools/utils/funcs.py +35 -0
- jetpytools/utils/math.py +158 -0
- jetpytools/utils/ranges.py +89 -0
- jetpytools-1.2.3.dist-info/LICENSE +21 -0
- jetpytools-1.2.3.dist-info/METADATA +48 -0
- jetpytools-1.2.3.dist-info/RECORD +33 -0
- jetpytools-1.2.3.dist-info/WHEEL +5 -0
- jetpytools-1.2.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from fractions import Fraction
|
|
4
|
+
from typing import Any, Iterable, Iterator, Sequence, overload
|
|
5
|
+
|
|
6
|
+
from ..types import F, SupportsString, T, SoftRange, SoftRangeN, SoftRangesN, StrictRange
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
'normalize_seq',
|
|
10
|
+
'to_arr',
|
|
11
|
+
'flatten',
|
|
12
|
+
'normalize_list_to_ranges',
|
|
13
|
+
'normalize_ranges_to_list',
|
|
14
|
+
'normalize_range',
|
|
15
|
+
'normalize_ranges',
|
|
16
|
+
'invert_ranges',
|
|
17
|
+
'norm_func_name', 'norm_display_name'
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@overload
|
|
22
|
+
def normalize_seq(val: Sequence[T], length: int) -> list[T]:
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@overload
|
|
27
|
+
def normalize_seq(val: T | Sequence[T], length: int) -> list[T]:
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def normalize_seq(val: T | Sequence[T], length: int) -> list[T]:
|
|
32
|
+
"""
|
|
33
|
+
Normalize a sequence of values.
|
|
34
|
+
|
|
35
|
+
:param val: Input value.
|
|
36
|
+
:param length: Amount of items in the output.
|
|
37
|
+
If original sequence length is less that this,
|
|
38
|
+
the last item will be repeated.
|
|
39
|
+
|
|
40
|
+
:return: List of normalized values with a set amount of items.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
val = to_arr(val)
|
|
44
|
+
|
|
45
|
+
val += [val[-1]] * (length - len(val))
|
|
46
|
+
|
|
47
|
+
return val[:length]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_iterables_t = (list, tuple, range, zip, set, map, enumerate)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def to_arr(val: list[T], *, sub: bool = False) -> list[T]:
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
def to_arr(val: T | Sequence[T], *, sub: bool = False) -> list[T]:
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def to_arr(val: T | Sequence[T], *, sub: bool = False) -> list[T]:
|
|
64
|
+
"""Normalize any value into an iterable."""
|
|
65
|
+
|
|
66
|
+
if sub:
|
|
67
|
+
return list(val) if any(isinstance(val, x) for x in _iterables_t) else [val] # type: ignore
|
|
68
|
+
|
|
69
|
+
return list(val) if type(val) in _iterables_t else [val] # type: ignore
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@overload
|
|
73
|
+
def flatten(items: T | Iterable[T | Iterable[T | Iterable[T]]]) -> Iterable[T]:
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@overload
|
|
78
|
+
def flatten(items: T | Iterable[T | Iterable[T]]) -> Iterable[T]: # type: ignore
|
|
79
|
+
...
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@overload
|
|
83
|
+
def flatten(items: T | Iterable[T]) -> Iterable[T]: # type: ignore
|
|
84
|
+
...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def flatten(items: Any) -> Any:
|
|
88
|
+
"""Flatten an array of values."""
|
|
89
|
+
|
|
90
|
+
for val in items:
|
|
91
|
+
if isinstance(val, Iterable) and not isinstance(val, (str, bytes)):
|
|
92
|
+
for sub_x in flatten(val):
|
|
93
|
+
yield sub_x
|
|
94
|
+
else:
|
|
95
|
+
yield val
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def normalize_range(ranges: SoftRange, /) -> Iterable[int]:
|
|
99
|
+
"""
|
|
100
|
+
Normalize ranges represented by a tuple to an iterable of frame numbers.
|
|
101
|
+
|
|
102
|
+
:param ranges: Ranges to normalize.
|
|
103
|
+
|
|
104
|
+
:return: List of positive frame ranges.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
if isinstance(ranges, int):
|
|
108
|
+
return [ranges]
|
|
109
|
+
|
|
110
|
+
if isinstance(ranges, tuple):
|
|
111
|
+
start, stop = ranges
|
|
112
|
+
step = -1 if stop < start else 1
|
|
113
|
+
|
|
114
|
+
return range(start, stop + step, step)
|
|
115
|
+
|
|
116
|
+
return ranges
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def normalize_list_to_ranges(flist: Iterable[int], min_length: int = 0) -> list[StrictRange]:
|
|
120
|
+
flist2 = list[list[int]]()
|
|
121
|
+
flist3 = list[int]()
|
|
122
|
+
|
|
123
|
+
prev_n = -1
|
|
124
|
+
|
|
125
|
+
for n in sorted(set(flist)):
|
|
126
|
+
if prev_n + 1 != n:
|
|
127
|
+
if flist3:
|
|
128
|
+
flist2.append(flist3)
|
|
129
|
+
flist3 = []
|
|
130
|
+
flist3.append(n)
|
|
131
|
+
prev_n = n
|
|
132
|
+
|
|
133
|
+
if flist3:
|
|
134
|
+
flist2.append(flist3)
|
|
135
|
+
|
|
136
|
+
flist4 = [i for i in flist2 if len(i) > min_length]
|
|
137
|
+
|
|
138
|
+
return list(zip(
|
|
139
|
+
[i[0] for i in flist4],
|
|
140
|
+
[i[-1] for j, i in enumerate(flist4)]
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def normalize_ranges_to_list(ranges: Iterable[SoftRange]) -> list[int]:
|
|
145
|
+
out = list[int]()
|
|
146
|
+
|
|
147
|
+
for srange in ranges:
|
|
148
|
+
out.extend(normalize_range(srange))
|
|
149
|
+
|
|
150
|
+
return out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def normalize_ranges(ranges: SoftRangeN | SoftRangesN, end: int) -> list[StrictRange]:
|
|
154
|
+
"""
|
|
155
|
+
Normalize ranges to a list of positive ranges.
|
|
156
|
+
|
|
157
|
+
Frame ranges can include None and negative values.
|
|
158
|
+
None will be converted to either 0 if it's the first value in a SoftRange, or the end if it's the second item.
|
|
159
|
+
Negative values will be subtracted from the end.
|
|
160
|
+
|
|
161
|
+
Examples:
|
|
162
|
+
|
|
163
|
+
.. code-block:: python
|
|
164
|
+
|
|
165
|
+
>>> normalize_ranges((None, None), end=1000)
|
|
166
|
+
[(0, 999)]
|
|
167
|
+
>>> normalize_ranges((24, -24), end=1000)
|
|
168
|
+
[(24, 975)]
|
|
169
|
+
>>> normalize_ranges([(24, 100), (80, 150)], end=1000)
|
|
170
|
+
[(24, 150)]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
:param clip: Input clip.
|
|
174
|
+
:param franges: Frame range or list of frame ranges.
|
|
175
|
+
|
|
176
|
+
:return: List of positive frame ranges.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
ranges = ranges if isinstance(ranges, list) else [ranges] # type:ignore
|
|
180
|
+
|
|
181
|
+
out = []
|
|
182
|
+
|
|
183
|
+
for r in ranges:
|
|
184
|
+
if r is None:
|
|
185
|
+
r = (None, None)
|
|
186
|
+
|
|
187
|
+
if isinstance(r, tuple):
|
|
188
|
+
start, endd = r
|
|
189
|
+
if start is None:
|
|
190
|
+
start = 0
|
|
191
|
+
if endd is None:
|
|
192
|
+
endd = end - 1
|
|
193
|
+
else:
|
|
194
|
+
start = r
|
|
195
|
+
endd = r
|
|
196
|
+
|
|
197
|
+
if start < 0:
|
|
198
|
+
start = end - 1 + start
|
|
199
|
+
|
|
200
|
+
if endd < 0:
|
|
201
|
+
endd = end - 1 + endd
|
|
202
|
+
|
|
203
|
+
out.append((start, endd))
|
|
204
|
+
|
|
205
|
+
return normalize_list_to_ranges([
|
|
206
|
+
x for start, endd in out for x in range(start, endd + 1)
|
|
207
|
+
])
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def invert_ranges(ranges: SoftRangeN | SoftRangesN, enda: int, endb: int | None) -> list[StrictRange]:
|
|
211
|
+
norm_ranges = normalize_ranges(ranges, enda if endb is None else endb)
|
|
212
|
+
|
|
213
|
+
b_frames = {*normalize_ranges_to_list(norm_ranges)}
|
|
214
|
+
|
|
215
|
+
return normalize_list_to_ranges({*range(enda)} - b_frames)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def norm_func_name(func_name: SupportsString | F) -> str:
|
|
219
|
+
"""Normalize a class, function, or other object to obtain its name"""
|
|
220
|
+
|
|
221
|
+
if isinstance(func_name, str):
|
|
222
|
+
return func_name.strip()
|
|
223
|
+
|
|
224
|
+
if not isinstance(func_name, type) and not callable(func_name):
|
|
225
|
+
return str(func_name).strip()
|
|
226
|
+
|
|
227
|
+
func = func_name
|
|
228
|
+
|
|
229
|
+
if hasattr(func_name, '__name__'):
|
|
230
|
+
func_name = func.__name__
|
|
231
|
+
elif hasattr(func_name, '__qualname__'):
|
|
232
|
+
func_name = func.__qualname__
|
|
233
|
+
|
|
234
|
+
if callable(func):
|
|
235
|
+
if hasattr(func, '__self__'):
|
|
236
|
+
func = func.__self__ if isinstance(func.__self__, type) else func.__self__.__class__
|
|
237
|
+
func_name = f'{func.__name__}.{func_name}'
|
|
238
|
+
|
|
239
|
+
return str(func_name).strip()
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def norm_display_name(obj: object) -> str:
|
|
243
|
+
"""Get a fancy name from any object."""
|
|
244
|
+
|
|
245
|
+
if isinstance(obj, Iterator):
|
|
246
|
+
return ', '.join(norm_display_name(v) for v in obj).strip()
|
|
247
|
+
|
|
248
|
+
if isinstance(obj, Fraction):
|
|
249
|
+
return f'{obj.numerator}/{obj.denominator}'
|
|
250
|
+
|
|
251
|
+
if isinstance(obj, dict):
|
|
252
|
+
return '(' + ', '.join(f'{k}={v}' for k, v in obj.items()) + ')'
|
|
253
|
+
|
|
254
|
+
return norm_func_name(obj)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
'deepmerge'
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def deepmerge(source: dict[Any, Any], destination: dict[Any, Any]) -> dict[Any, Any]:
|
|
11
|
+
for key, value in source.items():
|
|
12
|
+
if isinstance(value, dict):
|
|
13
|
+
node = destination.setdefault(key, {})
|
|
14
|
+
deepmerge(value, node)
|
|
15
|
+
else:
|
|
16
|
+
destination[key] = value
|
|
17
|
+
|
|
18
|
+
return destination
|
jetpytools/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
TYPE_CHECKING, Any, Callable, ParamSpec, Sequence, SupportsFloat, SupportsIndex, TypeAlias, TypeVar, Union
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'T', 'T0', 'T1', 'T2', 'T_contra',
|
|
9
|
+
|
|
10
|
+
'F', 'F0', 'F1', 'F2',
|
|
11
|
+
|
|
12
|
+
'P', 'P0', 'P1', 'P2',
|
|
13
|
+
'R', 'R0', 'R1', 'R2', 'R_contra',
|
|
14
|
+
|
|
15
|
+
'Nb',
|
|
16
|
+
|
|
17
|
+
'StrictRange', 'SoftRange', 'SoftRangeN', 'SoftRangesN',
|
|
18
|
+
|
|
19
|
+
'Self',
|
|
20
|
+
|
|
21
|
+
'SingleOrArr', 'SingleOrArrOpt',
|
|
22
|
+
'SingleOrSeq', 'SingleOrSeqOpt',
|
|
23
|
+
|
|
24
|
+
'SimpleByteData', 'SimpleByteDataArray',
|
|
25
|
+
'ByteData',
|
|
26
|
+
|
|
27
|
+
'KwargsT'
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
Nb = TypeVar('Nb', float, int)
|
|
31
|
+
|
|
32
|
+
T = TypeVar('T')
|
|
33
|
+
T0 = TypeVar('T0')
|
|
34
|
+
T1 = TypeVar('T1')
|
|
35
|
+
T2 = TypeVar('T2')
|
|
36
|
+
|
|
37
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
38
|
+
F0 = TypeVar('F0', bound=Callable[..., Any])
|
|
39
|
+
F1 = TypeVar('F1', bound=Callable[..., Any])
|
|
40
|
+
F2 = TypeVar('F2', bound=Callable[..., Any])
|
|
41
|
+
|
|
42
|
+
P = ParamSpec('P')
|
|
43
|
+
P0 = ParamSpec('P0')
|
|
44
|
+
P1 = ParamSpec('P1')
|
|
45
|
+
P2 = ParamSpec('P2')
|
|
46
|
+
|
|
47
|
+
R = TypeVar('R')
|
|
48
|
+
R0 = TypeVar('R0')
|
|
49
|
+
R1 = TypeVar('R1')
|
|
50
|
+
R2 = TypeVar('R2')
|
|
51
|
+
|
|
52
|
+
T_contra = TypeVar('T_contra', contravariant=True)
|
|
53
|
+
R_contra = TypeVar('R_contra', contravariant=True)
|
|
54
|
+
|
|
55
|
+
Self = TypeVar('Self')
|
|
56
|
+
|
|
57
|
+
StrictRange: TypeAlias = tuple[int, int]
|
|
58
|
+
SoftRange: TypeAlias = int | StrictRange | Sequence[int]
|
|
59
|
+
|
|
60
|
+
SoftRangeN: TypeAlias = int | tuple[int | None, int | None] | None
|
|
61
|
+
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
SoftRangesN: TypeAlias = Sequence[SoftRangeN]
|
|
64
|
+
else:
|
|
65
|
+
SoftRangesN: TypeAlias = list[SoftRangeN]
|
|
66
|
+
|
|
67
|
+
SingleOrArr = Union[T, list[T]]
|
|
68
|
+
SingleOrSeq = Union[T, Sequence[T]]
|
|
69
|
+
SingleOrArrOpt = Union[SingleOrArr[T], None]
|
|
70
|
+
SingleOrSeqOpt = Union[SingleOrSeq[T], None]
|
|
71
|
+
|
|
72
|
+
SimpleByteData: TypeAlias = str | bytes | bytearray
|
|
73
|
+
SimpleByteDataArray = Union[SimpleByteData, Sequence[SimpleByteData]]
|
|
74
|
+
|
|
75
|
+
ByteData: TypeAlias = SupportsFloat | SupportsIndex | SimpleByteData | memoryview
|
|
76
|
+
|
|
77
|
+
KwargsT = dict[str, Any]
|
jetpytools/types/file.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import shutil
|
|
5
|
+
from os import PathLike, listdir, path, walk
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, TypeAlias, Union
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
'FilePathType', 'FileDescriptor',
|
|
11
|
+
'FileOpener',
|
|
12
|
+
|
|
13
|
+
'OpenTextModeUpdating',
|
|
14
|
+
'OpenTextModeWriting',
|
|
15
|
+
'OpenTextModeReading',
|
|
16
|
+
|
|
17
|
+
'OpenBinaryModeUpdating',
|
|
18
|
+
'OpenBinaryModeWriting',
|
|
19
|
+
'OpenBinaryModeReading',
|
|
20
|
+
|
|
21
|
+
'OpenTextMode',
|
|
22
|
+
'OpenBinaryMode',
|
|
23
|
+
|
|
24
|
+
'SPath', 'SPathLike'
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
FileDescriptor: TypeAlias = int
|
|
28
|
+
|
|
29
|
+
FilePathType: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes]
|
|
30
|
+
|
|
31
|
+
FileOpener: TypeAlias = Callable[[str, int], int]
|
|
32
|
+
|
|
33
|
+
OpenTextModeUpdating: TypeAlias = Literal[
|
|
34
|
+
'r+', '+r', 'rt+', 'r+t', '+rt', 'tr+', 't+r', '+tr', 'w+', '+w', 'wt+', 'w+t', '+wt', 'tw+', 't+w', '+tw',
|
|
35
|
+
'a+', '+a', 'at+', 'a+t', '+at', 'ta+', 't+a', '+ta', 'x+', '+x', 'xt+', 'x+t', '+xt', 'tx+', 't+x', '+tx',
|
|
36
|
+
]
|
|
37
|
+
OpenTextModeWriting: TypeAlias = Literal[
|
|
38
|
+
'w', 'wt', 'tw', 'a', 'at', 'ta', 'x', 'xt', 'tx'
|
|
39
|
+
]
|
|
40
|
+
OpenTextModeReading: TypeAlias = Literal[
|
|
41
|
+
'r', 'rt', 'tr', 'U', 'rU', 'Ur', 'rtU', 'rUt', 'Urt', 'trU', 'tUr', 'Utr'
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
OpenBinaryModeUpdating: TypeAlias = Literal[
|
|
45
|
+
'rb+', 'r+b', '+rb', 'br+', 'b+r', '+br', 'wb+', 'w+b', '+wb', 'bw+', 'b+w', '+bw',
|
|
46
|
+
'ab+', 'a+b', '+ab', 'ba+', 'b+a', '+ba', 'xb+', 'x+b', '+xb', 'bx+', 'b+x', '+bx'
|
|
47
|
+
]
|
|
48
|
+
OpenBinaryModeWriting: TypeAlias = Literal[
|
|
49
|
+
'wb', 'bw', 'ab', 'ba', 'xb', 'bx'
|
|
50
|
+
]
|
|
51
|
+
OpenBinaryModeReading: TypeAlias = Literal[
|
|
52
|
+
'rb', 'br', 'rbU', 'rUb', 'Urb', 'brU', 'bUr', 'Ubr'
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
OpenTextMode: TypeAlias = OpenTextModeUpdating | OpenTextModeWriting | OpenTextModeReading
|
|
56
|
+
OpenBinaryMode: TypeAlias = OpenBinaryModeUpdating | OpenBinaryModeReading | OpenBinaryModeWriting
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class SPath(Path):
|
|
60
|
+
"""Modified version of pathlib.Path"""
|
|
61
|
+
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
def __new__(cls, *args: SPathLike, **kwargs: Any) -> SPath:
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def format(self, *args: Any, **kwargs: Any) -> SPath:
|
|
67
|
+
"""Format the path with the given arguments."""
|
|
68
|
+
|
|
69
|
+
return SPath(self.to_str().format(*args, **kwargs))
|
|
70
|
+
|
|
71
|
+
def to_str(self) -> str:
|
|
72
|
+
"""Cast the path to a string."""
|
|
73
|
+
|
|
74
|
+
return str(self)
|
|
75
|
+
|
|
76
|
+
def get_folder(self) -> SPath:
|
|
77
|
+
"""Get the folder of the path."""
|
|
78
|
+
|
|
79
|
+
folder_path = self.resolve()
|
|
80
|
+
|
|
81
|
+
if folder_path.is_dir():
|
|
82
|
+
return folder_path
|
|
83
|
+
|
|
84
|
+
return SPath(path.dirname(folder_path))
|
|
85
|
+
|
|
86
|
+
def mkdirp(self, mode: int = 0o777) -> None:
|
|
87
|
+
"""Make the dir path with its parents."""
|
|
88
|
+
|
|
89
|
+
return self.get_folder().mkdir(mode, True, True)
|
|
90
|
+
|
|
91
|
+
def rmdirs(self, missing_ok: bool = False, ignore_errors: bool = True) -> None:
|
|
92
|
+
"""Remove the dir path with its contents."""
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
return shutil.rmtree(str(self.get_folder()), ignore_errors)
|
|
96
|
+
except FileNotFoundError:
|
|
97
|
+
if not missing_ok:
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
def read_lines(
|
|
101
|
+
self, encoding: str | None = None, errors: str | None = None, keepends: bool = False
|
|
102
|
+
) -> list[str]:
|
|
103
|
+
"""Read the file and return its lines."""
|
|
104
|
+
|
|
105
|
+
return super().read_text(encoding, errors).splitlines(keepends)
|
|
106
|
+
|
|
107
|
+
def write_lines(
|
|
108
|
+
self, data: Iterable[str], encoding: str | None = None,
|
|
109
|
+
errors: str | None = None, newline: str | None = None
|
|
110
|
+
) -> int:
|
|
111
|
+
"""Open the file and write the given lines."""
|
|
112
|
+
|
|
113
|
+
return super().write_text('\n'.join(data), encoding, errors, newline)
|
|
114
|
+
|
|
115
|
+
def append_to_stem(self, suffixes: str | Iterable[str], sep: str = '_') -> SPath:
|
|
116
|
+
"""Append a suffix to the stem of the path"""
|
|
117
|
+
|
|
118
|
+
from ..functions import to_arr
|
|
119
|
+
|
|
120
|
+
return self.with_stem(sep.join([self.stem, *to_arr(suffixes)])) # type:ignore[list-item]
|
|
121
|
+
|
|
122
|
+
def is_empty_dir(self) -> bool:
|
|
123
|
+
"""Check if the directory is empty."""
|
|
124
|
+
|
|
125
|
+
return self.is_dir() and not any(self.iterdir())
|
|
126
|
+
|
|
127
|
+
def move_dir(self, dst: SPath, *, mode: int = 0o777) -> None:
|
|
128
|
+
"""Move the directory to the specified destination."""
|
|
129
|
+
|
|
130
|
+
dst.mkdir(mode, True, True)
|
|
131
|
+
|
|
132
|
+
for file in listdir(self):
|
|
133
|
+
src_file = self / file
|
|
134
|
+
dst_file = dst / file
|
|
135
|
+
|
|
136
|
+
if dst_file.exists():
|
|
137
|
+
src_file.unlink()
|
|
138
|
+
else:
|
|
139
|
+
src_file.rename(dst_file)
|
|
140
|
+
|
|
141
|
+
self.rmdir()
|
|
142
|
+
|
|
143
|
+
def copy_dir(self, dst: SPath) -> SPath:
|
|
144
|
+
"""Copy the directory to the specified destination."""
|
|
145
|
+
|
|
146
|
+
if not self.is_dir():
|
|
147
|
+
from ..exceptions import PathIsNotADirectoryError
|
|
148
|
+
raise PathIsNotADirectoryError('The given path, \"{self}\" is not a directory!', self.copy_dir)
|
|
149
|
+
|
|
150
|
+
dst.mkdirp()
|
|
151
|
+
shutil.copytree(self, dst, dirs_exist_ok=True)
|
|
152
|
+
|
|
153
|
+
return SPath(dst)
|
|
154
|
+
|
|
155
|
+
def lglob(self, pattern: str = '*') -> list[SPath]:
|
|
156
|
+
"""Glob the path and return the list of paths."""
|
|
157
|
+
|
|
158
|
+
return list(map(SPath, self.glob(pattern)))
|
|
159
|
+
|
|
160
|
+
def fglob(self, pattern: str = '*') -> SPath | None:
|
|
161
|
+
"""Glob the path and return the first match."""
|
|
162
|
+
|
|
163
|
+
for root, dirs, files in walk(self):
|
|
164
|
+
for name in dirs + files:
|
|
165
|
+
if fnmatch.fnmatch(name, pattern):
|
|
166
|
+
return SPath(path.join(root, name))
|
|
167
|
+
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def find_newest_file(self, pattern: str = '*') -> SPath | None:
|
|
171
|
+
"""Find the most recently modified file matching the given pattern in the directory."""
|
|
172
|
+
|
|
173
|
+
matching_files = self.get_folder().glob(pattern)
|
|
174
|
+
|
|
175
|
+
if not matching_files:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
return max(matching_files, key=lambda p: p.stat().st_mtime, default=None) # type:ignore
|
|
179
|
+
|
|
180
|
+
def get_size(self) -> int:
|
|
181
|
+
"""Get the size of the file or directory in bytes."""
|
|
182
|
+
|
|
183
|
+
if not self.exists():
|
|
184
|
+
from ..exceptions import FileNotExistsError
|
|
185
|
+
raise FileNotExistsError('The given path, \"{self}\" is not a file or directory!', self.get_size)
|
|
186
|
+
|
|
187
|
+
if self.is_file():
|
|
188
|
+
return self.stat().st_size
|
|
189
|
+
|
|
190
|
+
return sum(f.stat().st_size for f in self.rglob('*') if f.is_file())
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
SPathLike = Union[str, Path, SPath]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from functools import wraps
|
|
3
|
+
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Callable, Iterable, List, SupportsIndex, TypeAlias, TypeVar, overload
|
|
5
|
+
|
|
6
|
+
from .builtins import T, P
|
|
7
|
+
from .supports import SupportsString
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
'StrList',
|
|
11
|
+
'Sentinel'
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StrList(List[SupportsString]):
|
|
16
|
+
"""Custom class for representing a recursively "stringable" list."""
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
@overload
|
|
20
|
+
def __init__(self, __iterable: Iterable[SupportsString | None] = []) -> None:
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
@overload
|
|
24
|
+
def __init__(self, __iterable: Iterable[Iterable[SupportsString | None] | None] = []) -> None:
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
def __init__(self, __iterable: Any = []) -> None:
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def string(self) -> str:
|
|
32
|
+
return self.to_str()
|
|
33
|
+
|
|
34
|
+
def to_str(self) -> str:
|
|
35
|
+
return str(self)
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
from ..functions import flatten
|
|
39
|
+
|
|
40
|
+
return ' '.join(
|
|
41
|
+
filter(
|
|
42
|
+
None,
|
|
43
|
+
(str(x).strip() for x in flatten(self) if x is not None) # type: ignore[var-annotated,arg-type]
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __add__(self, __x: list[SupportsString]) -> StrList: # type: ignore[override]
|
|
48
|
+
return StrList(super().__add__(__x))
|
|
49
|
+
|
|
50
|
+
def __mul__(self, __n: SupportsIndex) -> StrList:
|
|
51
|
+
return StrList(super().__mul__(__n))
|
|
52
|
+
|
|
53
|
+
def __rmul__(self, __n: SupportsIndex) -> StrList:
|
|
54
|
+
return StrList(super().__rmul__(__n))
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def mlength(self) -> int:
|
|
58
|
+
return len(self) - 1
|
|
59
|
+
|
|
60
|
+
def append(self, *__object: SupportsString) -> None:
|
|
61
|
+
for __obj in __object:
|
|
62
|
+
super().append(__obj)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SentinelDispatcher:
|
|
66
|
+
def check(self, ret_value: T, cond: bool) -> T | SentinelDispatcher:
|
|
67
|
+
return ret_value if cond else self
|
|
68
|
+
|
|
69
|
+
def check_cb(self, callback: Callable[P, tuple[T, bool]]) -> Callable[P, T | SentinelDispatcher]:
|
|
70
|
+
@wraps(callback)
|
|
71
|
+
def _wrap(*args: P.args, **kwargs: P.kwargs) -> T | SentinelDispatcher:
|
|
72
|
+
return self.check(*callback(*args, **kwargs))
|
|
73
|
+
|
|
74
|
+
return _wrap
|
|
75
|
+
|
|
76
|
+
def filter(self: SelfSentinel, items: Iterable[T | SelfSentinel]) -> Iterable[T]:
|
|
77
|
+
for item in items:
|
|
78
|
+
if item is self:
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
yield item # type: ignore
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def filter_multi(cls, items: Iterable[T | SelfSentinel], *sentinels: SelfSentinel) -> Iterable[T]:
|
|
85
|
+
for item in items:
|
|
86
|
+
if item in sentinels:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
yield item # type: ignore
|
|
90
|
+
|
|
91
|
+
def __getattr__(self, name: str) -> SentinelDispatcher:
|
|
92
|
+
if name not in _sentinels:
|
|
93
|
+
_sentinels[name] = SentinelDispatcher()
|
|
94
|
+
return _sentinels[name]
|
|
95
|
+
|
|
96
|
+
def __setattr__(self, __name: str, __value: Any) -> None:
|
|
97
|
+
raise NameError
|
|
98
|
+
|
|
99
|
+
def __call__(self) -> SentinelDispatcher:
|
|
100
|
+
return SentinelDispatcher()
|
|
101
|
+
|
|
102
|
+
Type: TypeAlias = 'SentinelDispatcher'
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
Sentinel = SentinelDispatcher()
|
|
106
|
+
|
|
107
|
+
_sentinels = dict[str, SentinelDispatcher]()
|
|
108
|
+
|
|
109
|
+
SelfSentinel = TypeVar('SelfSentinel', bound=SentinelDispatcher)
|