kisstdlib 0.0.9__tar.gz → 0.0.10__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.
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/PKG-INFO +1 -1
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/__init__.py +22 -6
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/argparse_ext.py +40 -6
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/base.py +1 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/fs.py +129 -91
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/io/stdio.py +8 -5
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/logging_ext.py +10 -17
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/signal_ext.py +31 -15
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/string_ext.py +7 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/PKG-INFO +1 -1
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/pyproject.toml +1 -1
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/test/test_fs.py +32 -32
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/LICENSE.txt +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/README.md +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/compression.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/failure.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/getpass_ext.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/io/__init__.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/io/base.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/io/encoder.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/io/wrapper.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/itertools_ext.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/parsing.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/py.typed +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/pyrepr.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/sorted.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/sqlite3_ext.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/tariter.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/time.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/SOURCES.txt +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/dependency_links.txt +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/entry_points.txt +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/requires.txt +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/top_level.txt +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib_bin/describe_forest.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/setup.cfg +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/setup.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/test/test_basics.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/test/test_io.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/test/test_sorted.py +0 -0
- {kisstdlib-0.0.9 → kisstdlib-0.0.10}/test/test_time.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: kisstdlib
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: System programming with conceptually and algebraically simple primitives.
|
|
5
5
|
Author-email: Jan Malakhovski <oxij@oxij.org>
|
|
6
6
|
Project-URL: Homepage, https://oxij.org/software/kisstdlib/
|
|
@@ -36,11 +36,15 @@ from .io.stdio import *
|
|
|
36
36
|
from .logging_ext import *
|
|
37
37
|
from .signal_ext import *
|
|
38
38
|
|
|
39
|
+
from .fs import setup_fs as _setup_fs
|
|
40
|
+
|
|
39
41
|
|
|
40
42
|
def setup_kisstdlib(
|
|
41
43
|
prog: str | None = None,
|
|
44
|
+
*,
|
|
42
45
|
do_setup_stdio: bool = True,
|
|
43
|
-
|
|
46
|
+
do_setup_delay_signals: bool = True,
|
|
47
|
+
do_setup_fs: bool = True,
|
|
44
48
|
**kwargs: _t.Any,
|
|
45
49
|
) -> tuple[LogCounter, ANSILogHandler]:
|
|
46
50
|
"""Run all `setup_*` functions of all other `kisstdlib.*` modules and setup
|
|
@@ -50,9 +54,12 @@ def setup_kisstdlib(
|
|
|
50
54
|
prog = _op.basename(_sys.argv[0])
|
|
51
55
|
if do_setup_stdio:
|
|
52
56
|
setup_stdio()
|
|
53
|
-
res = setup_logging(prog, **kwargs)
|
|
54
57
|
setup_delay_signal_messages(prog + ": ")
|
|
55
|
-
|
|
58
|
+
if do_setup_delay_signals:
|
|
59
|
+
setup_delay_signals()
|
|
60
|
+
if do_setup_fs:
|
|
61
|
+
_setup_fs(prog)
|
|
62
|
+
res = setup_logging(prog, **kwargs)
|
|
56
63
|
return res
|
|
57
64
|
|
|
58
65
|
|
|
@@ -102,9 +109,18 @@ def run_kisstdlib_main(
|
|
|
102
109
|
for be in flat_exceptions(excs):
|
|
103
110
|
critical(gettext("Uncaught exception: %s"), str(be), exc_info=be)
|
|
104
111
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
112
|
+
try:
|
|
113
|
+
logger.flush()
|
|
114
|
+
except BrokenPipeError:
|
|
115
|
+
pass
|
|
116
|
+
try:
|
|
117
|
+
stdout.flush()
|
|
118
|
+
except BrokenPipeError:
|
|
119
|
+
pass
|
|
120
|
+
try:
|
|
121
|
+
stderr.flush()
|
|
122
|
+
except BrokenPipeError:
|
|
123
|
+
pass
|
|
108
124
|
|
|
109
125
|
num_warnings = counter.warnings
|
|
110
126
|
num_errors = counter.errors
|
|
@@ -159,16 +159,16 @@ class MarkdownBetterHelpFormatter(BetterHelpFormatter):
|
|
|
159
159
|
return join(["\n", heading, item_help, "\n"])
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
class
|
|
162
|
+
class _OptionallyMarkdownHelpAction(Action):
|
|
163
163
|
def __init__(
|
|
164
164
|
self,
|
|
165
165
|
option_strings: list[str],
|
|
166
166
|
dest: str = SUPPRESS,
|
|
167
167
|
default: _t.Any = SUPPRESS,
|
|
168
|
-
|
|
168
|
+
**kwargs: _t.Any,
|
|
169
169
|
) -> None:
|
|
170
170
|
super().__init__(
|
|
171
|
-
option_strings=option_strings, dest=dest, default=default, nargs=0,
|
|
171
|
+
option_strings=option_strings, dest=dest, default=default, nargs=0, **kwargs
|
|
172
172
|
)
|
|
173
173
|
|
|
174
174
|
def show_help(self, parser: "BetterArgumentParser", namespace: Namespace) -> None:
|
|
@@ -194,6 +194,38 @@ class OptionallyMarkdownHelpAction(Action):
|
|
|
194
194
|
parser.post_parse_args = self.show_help
|
|
195
195
|
|
|
196
196
|
|
|
197
|
+
class _StoreMapAction(Action):
|
|
198
|
+
def __init__(self, func: _t.Callable[[_t.Any], _t.Any] = _identity, **kwargs: _t.Any) -> None:
|
|
199
|
+
super().__init__(**kwargs)
|
|
200
|
+
self.func = func
|
|
201
|
+
|
|
202
|
+
def __call__(
|
|
203
|
+
self,
|
|
204
|
+
parser: _t.Any,
|
|
205
|
+
namespace: Namespace,
|
|
206
|
+
values: _t.Any,
|
|
207
|
+
option_string: str | None = None,
|
|
208
|
+
) -> None:
|
|
209
|
+
setattr(namespace, self.dest, self.func(values))
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class _AppendMapAction(Action):
|
|
213
|
+
def __init__(self, func: _t.Callable[[_t.Any], _t.Any] = _identity, **kwargs: _t.Any) -> None:
|
|
214
|
+
super().__init__(default=[], **kwargs)
|
|
215
|
+
self.func = func
|
|
216
|
+
|
|
217
|
+
def __call__(
|
|
218
|
+
self,
|
|
219
|
+
parser: _t.Any,
|
|
220
|
+
namespace: Namespace,
|
|
221
|
+
values: _t.Any,
|
|
222
|
+
option_string: str | None = None,
|
|
223
|
+
) -> None:
|
|
224
|
+
v = getattr(namespace, self.dest)
|
|
225
|
+
v.append(self.func(values))
|
|
226
|
+
setattr(namespace, self.dest, v)
|
|
227
|
+
|
|
228
|
+
|
|
197
229
|
class BetterArgumentParser(ArgumentParser):
|
|
198
230
|
"""Like `argparse.ArgumentParser`, but uses `BetterHelpFormatter` by default,
|
|
199
231
|
and implements `format_help` that recurses into subcommands and appends
|
|
@@ -225,6 +257,10 @@ class BetterArgumentParser(ArgumentParser):
|
|
|
225
257
|
**kwargs,
|
|
226
258
|
)
|
|
227
259
|
|
|
260
|
+
self.register("action", "store_map", _StoreMapAction)
|
|
261
|
+
self.register("action", "append_map", _AppendMapAction)
|
|
262
|
+
self.register("action", "help_omd", _OptionallyMarkdownHelpAction)
|
|
263
|
+
|
|
228
264
|
self.add_version = add_version
|
|
229
265
|
self.add_help = add_help
|
|
230
266
|
self.additional_sections = additional_sections
|
|
@@ -249,12 +285,10 @@ class BetterArgumentParser(ArgumentParser):
|
|
|
249
285
|
)
|
|
250
286
|
|
|
251
287
|
if add_help:
|
|
252
|
-
self.register("action", "help", OptionallyMarkdownHelpAction)
|
|
253
|
-
|
|
254
288
|
self.add_argument(
|
|
255
289
|
default_prefix + "h",
|
|
256
290
|
default_prefix * 2 + "help",
|
|
257
|
-
action="
|
|
291
|
+
action="help_omd",
|
|
258
292
|
default=SUPPRESS,
|
|
259
293
|
help=_gettext("show this help message and exit"),
|
|
260
294
|
)
|
|
@@ -36,41 +36,29 @@ import stat as _stat
|
|
|
36
36
|
import sys as _sys
|
|
37
37
|
import typing as _t
|
|
38
38
|
|
|
39
|
+
from os import fsencode, fsdecode
|
|
40
|
+
|
|
39
41
|
from .base import POSIX as _POSIX, identity as _identity
|
|
40
|
-
from .string_ext import escaper as _escaper
|
|
41
42
|
from .io.base import *
|
|
43
|
+
from .string_ext import escape_path as _escape_path
|
|
42
44
|
from .time import Timestamp as _Timestamp
|
|
43
45
|
|
|
44
46
|
sep = _op.sep
|
|
45
47
|
sepb = _os.fsencode(_op.sep)
|
|
46
48
|
|
|
47
49
|
|
|
48
|
-
def
|
|
49
|
-
"""Apply `os.fsdecode` if `bytes`."""
|
|
50
|
-
if isinstance(x, bytes):
|
|
51
|
-
return _os.fsdecode(x)
|
|
52
|
-
return x
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def fsencode_maybe(x: str | bytes) -> bytes:
|
|
56
|
-
"""Apply `os.fsencode` if `str`."""
|
|
57
|
-
if isinstance(x, str):
|
|
58
|
-
return _os.fsencode(x)
|
|
59
|
-
return x
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def dirname_dot(x: _t.AnyStr) -> tuple[_t.AnyStr, bool]:
|
|
50
|
+
def dirname_dot(path: _t.AnyStr) -> tuple[_t.AnyStr, bool]:
|
|
63
51
|
"""Apply `os.path.dirname` to the given argument, but if it's empty, return "." instead.
|
|
64
52
|
The second element of the tuple is `False` if the above replacement was performed, `True` otherwise.
|
|
65
53
|
"""
|
|
66
|
-
|
|
67
|
-
if isinstance(
|
|
68
|
-
if
|
|
54
|
+
path = _os.path.dirname(path)
|
|
55
|
+
if isinstance(path, bytes):
|
|
56
|
+
if path == b"":
|
|
69
57
|
return b".", False
|
|
70
|
-
return
|
|
71
|
-
if
|
|
58
|
+
return path, True
|
|
59
|
+
if path == "":
|
|
72
60
|
return ".", False
|
|
73
|
-
return
|
|
61
|
+
return path, True
|
|
74
62
|
|
|
75
63
|
|
|
76
64
|
def read_file_maybe(path: str | bytes) -> bytes | None:
|
|
@@ -100,8 +88,8 @@ def same_file_data(path1: str | bytes, path2: str | bytes) -> bool:
|
|
|
100
88
|
|
|
101
89
|
def same_symlink_data(path1: str | bytes, path2: str | bytes) -> bool:
|
|
102
90
|
"""Check if two given symlinks have exactly the same content."""
|
|
103
|
-
a = _os.readlink(
|
|
104
|
-
b = _os.readlink(
|
|
91
|
+
a = _os.readlink(fsencode(path1))
|
|
92
|
+
b = _os.readlink(fsencode(path2))
|
|
105
93
|
return a == b
|
|
106
94
|
|
|
107
95
|
|
|
@@ -206,7 +194,7 @@ def iter_subtree(
|
|
|
206
194
|
if handle_error is not None:
|
|
207
195
|
eno = exc.errno
|
|
208
196
|
handle_error(
|
|
209
|
-
"failed to stat
|
|
197
|
+
"failed to `stat`: [Errno %d, %s] %s: %s",
|
|
210
198
|
eno,
|
|
211
199
|
_errno.errorcode.get(eno, "?"),
|
|
212
200
|
_os.strerror(eno),
|
|
@@ -410,12 +398,12 @@ def describe_forest(
|
|
|
410
398
|
"""Produce a simple `find .`+`stat`-like description of walks of given `paths`.
|
|
411
399
|
See `describe-forest` script for more info.
|
|
412
400
|
"""
|
|
413
|
-
escape: _t.Callable[[str], str] = _identity if literal else
|
|
401
|
+
escape: _t.Callable[[str], str] = _identity if literal else _escape_path # type: ignore
|
|
414
402
|
seen: dict[tuple[int, int], tuple[_t.AnyStr, int, str]] = {}
|
|
415
403
|
for i, dirpath in enumerate(paths):
|
|
416
404
|
for fpath, _eps, _edir in iter_subtree(dirpath, follow_symlinks=follow_symlinks):
|
|
417
405
|
abs_path = _op.abspath(fpath)
|
|
418
|
-
rpath =
|
|
406
|
+
rpath = fsdecode(_op.relpath(fpath, dirpath))
|
|
419
407
|
epath: str
|
|
420
408
|
if numbers is False or numbers is None and len(paths) == 1:
|
|
421
409
|
epath = escape(rpath)
|
|
@@ -434,7 +422,7 @@ def describe_forest(
|
|
|
434
422
|
if relative_hardlinks and hi == i:
|
|
435
423
|
# within the same `dirpath`
|
|
436
424
|
dirname = _op.dirname(abs_path)
|
|
437
|
-
target = escape(
|
|
425
|
+
target = escape(fsdecode(_op.relpath(habs_path, dirname)))
|
|
438
426
|
yield [epath, "ref", "=>", target]
|
|
439
427
|
else:
|
|
440
428
|
yield [epath, "ref", "==>", hepath]
|
|
@@ -464,7 +452,7 @@ def describe_forest(
|
|
|
464
452
|
sha256 = sha256[:hash_length]
|
|
465
453
|
yield [epath, "reg"] + emode + emtime + esize + ["sha256", sha256] # fmt: skip
|
|
466
454
|
elif _stat.S_ISLNK(stat.st_mode):
|
|
467
|
-
esymlink = escape(
|
|
455
|
+
esymlink = escape(fsdecode(_os.readlink(abs_path)))
|
|
468
456
|
yield [epath, "sym"] + emode + emtime + ["->", esymlink]
|
|
469
457
|
else:
|
|
470
458
|
yield [epath, "???"] + emode + emtime + esize
|
|
@@ -588,6 +576,7 @@ class DeferredSync(_t.Generic[_t.AnyStr]):
|
|
|
588
576
|
Doing this can improve disk performance considerably.
|
|
589
577
|
"""
|
|
590
578
|
|
|
579
|
+
defer: bool
|
|
591
580
|
tmp_file: set[_t.AnyStr]
|
|
592
581
|
unlink_file: set[_t.AnyStr]
|
|
593
582
|
fsync_file: set[_t.AnyStr]
|
|
@@ -598,7 +587,8 @@ class DeferredSync(_t.Generic[_t.AnyStr]):
|
|
|
598
587
|
# if all of the above succeed, also do these
|
|
599
588
|
_after: _t.Optional["DeferredSync[_t.AnyStr]"]
|
|
600
589
|
|
|
601
|
-
def __init__(self) -> None:
|
|
590
|
+
def __init__(self, defer: bool) -> None:
|
|
591
|
+
self.defer = defer
|
|
602
592
|
self.reset()
|
|
603
593
|
|
|
604
594
|
def reset(self) -> None:
|
|
@@ -628,14 +618,14 @@ after="""
|
|
|
628
618
|
@property
|
|
629
619
|
def after(self) -> "DeferredSync[_t.AnyStr]":
|
|
630
620
|
if self._after is None:
|
|
631
|
-
self._after = DeferredSync()
|
|
621
|
+
self._after = DeferredSync(self.defer)
|
|
632
622
|
return self._after
|
|
633
623
|
|
|
634
624
|
def copy(self) -> "DeferredSync[_t.AnyStr]":
|
|
635
625
|
"""Return a shallow copy of this object (elements describing operations are not
|
|
636
626
|
copied, only the structure is).
|
|
637
627
|
"""
|
|
638
|
-
res: DeferredSync[_t.AnyStr] = DeferredSync()
|
|
628
|
+
res: DeferredSync[_t.AnyStr] = DeferredSync(self.defer)
|
|
639
629
|
res.tmp_file = set(self.tmp_file)
|
|
640
630
|
res.unlink_file = set(self.unlink_file)
|
|
641
631
|
res.fsync_file = set(self.fsync_file)
|
|
@@ -654,20 +644,18 @@ after="""
|
|
|
654
644
|
unlink_many(self.tmp_file)
|
|
655
645
|
self.reset()
|
|
656
646
|
|
|
657
|
-
def
|
|
658
|
-
self, strict: bool = True, simulate: list[list[str]] | None = None
|
|
659
|
-
) -> list[Exception]:
|
|
647
|
+
def sync(self, strict: bool = True, simulate: list[list[str]] | None = None) -> list[Exception]:
|
|
660
648
|
"""Perform all deferred operations and return a list of raised exceptions.
|
|
661
649
|
|
|
662
650
|
Operations that fail will be left sitting in the corresponding fields.
|
|
663
651
|
|
|
664
652
|
With `simulate` argument set, this function instead simulates the
|
|
665
|
-
`
|
|
653
|
+
`sync` and writes the log of things it would do into that argument.
|
|
666
654
|
"""
|
|
667
655
|
exceptions: list[Exception] = []
|
|
668
656
|
|
|
669
657
|
if simulate is not None:
|
|
670
|
-
simulate += [["unlink",
|
|
658
|
+
simulate += [["unlink", fsdecode(f)] for f in sorted(self.unlink_file)]
|
|
671
659
|
self.unlink_file = set()
|
|
672
660
|
elif self.unlink_file:
|
|
673
661
|
self.unlink_file = set(unlink_many(sorted(self.unlink_file), exceptions))
|
|
@@ -689,7 +677,7 @@ after="""
|
|
|
689
677
|
|
|
690
678
|
while self.fsync_file or self.fsync_dir or self.fsync_dir2 or self.rename_file:
|
|
691
679
|
if simulate is not None:
|
|
692
|
-
simulate += [["fsync",
|
|
680
|
+
simulate += [["fsync", fsdecode(f)] for f in sorted(self.fsync_file)]
|
|
693
681
|
self.fsync_file = set()
|
|
694
682
|
elif self.fsync_file:
|
|
695
683
|
fsync_file_left.update(fsync_many_files(sorted(self.fsync_file), 0, exceptions))
|
|
@@ -723,7 +711,7 @@ after="""
|
|
|
723
711
|
|
|
724
712
|
try:
|
|
725
713
|
if simulate is not None:
|
|
726
|
-
simulate.append(["replace" if allow_overwrites else "rename",
|
|
714
|
+
simulate.append(["replace" if allow_overwrites else "rename", fsdecode(src_path), fsdecode(dst_path)]) # fmt: skip
|
|
727
715
|
else:
|
|
728
716
|
rename(src_path, dst_path, allow_overwrites, makedirs=False, dst_dir=dst_dir) # fmt: skip
|
|
729
717
|
except Exception as exc:
|
|
@@ -751,14 +739,14 @@ after="""
|
|
|
751
739
|
return exceptions
|
|
752
740
|
|
|
753
741
|
if simulate is not None:
|
|
754
|
-
simulate += [["fsync_win",
|
|
742
|
+
simulate += [["fsync_win", fsdecode(f)] for f in sorted(self.fsync_file)]
|
|
755
743
|
self.fsync_file = set()
|
|
756
744
|
elif self.fsync_file:
|
|
757
745
|
fsync_file_left.update(fsync_many_files(sorted(self.fsync_file), 0, exceptions))
|
|
758
746
|
self.fsync_file = set()
|
|
759
747
|
|
|
760
748
|
if simulate is not None:
|
|
761
|
-
simulate += [["fsync_dir",
|
|
749
|
+
simulate += [["fsync_dir", fsdecode(f)] for f in sorted(self.fsync_dir)]
|
|
762
750
|
self.fsync_dir = set()
|
|
763
751
|
elif self.fsync_dir:
|
|
764
752
|
if _POSIX:
|
|
@@ -766,7 +754,7 @@ after="""
|
|
|
766
754
|
self.fsync_dir = set()
|
|
767
755
|
|
|
768
756
|
if simulate is not None:
|
|
769
|
-
simulate += [["fsync_dir",
|
|
757
|
+
simulate += [["fsync_dir", fsdecode(f)] for f in sorted(self.fsync_dir2)]
|
|
770
758
|
self.fsync_dir2 = set()
|
|
771
759
|
elif self.fsync_dir2:
|
|
772
760
|
if _POSIX:
|
|
@@ -785,7 +773,7 @@ after="""
|
|
|
785
773
|
return exceptions
|
|
786
774
|
|
|
787
775
|
if self._after is not None:
|
|
788
|
-
excs = self._after.
|
|
776
|
+
excs = self._after.sync(strict, simulate)
|
|
789
777
|
if len(excs) > 0:
|
|
790
778
|
exceptions += excs
|
|
791
779
|
else:
|
|
@@ -793,12 +781,12 @@ after="""
|
|
|
793
781
|
|
|
794
782
|
return exceptions
|
|
795
783
|
|
|
796
|
-
def
|
|
797
|
-
"""Like `
|
|
784
|
+
def flush(self, strict: bool = True) -> None:
|
|
785
|
+
"""Like `sync` but raises collected exceptions as an exception group and calls
|
|
798
786
|
`.clear` in that case.
|
|
799
787
|
"""
|
|
800
788
|
try:
|
|
801
|
-
excs = self.
|
|
789
|
+
excs = self.sync(strict)
|
|
802
790
|
if len(excs) > 0:
|
|
803
791
|
raise ExceptionGroup("failed to sync", excs)
|
|
804
792
|
finally:
|
|
@@ -811,7 +799,7 @@ def atomic_rename(
|
|
|
811
799
|
allow_overwrites: bool,
|
|
812
800
|
*,
|
|
813
801
|
makedirs: bool = True,
|
|
814
|
-
|
|
802
|
+
sync: DeferredSync[_t.AnyStr] | bool = True,
|
|
815
803
|
) -> None:
|
|
816
804
|
"""Atomically rename a file, performing all necesary `fsync`s."""
|
|
817
805
|
|
|
@@ -821,19 +809,24 @@ def atomic_rename(
|
|
|
821
809
|
if makedirs and nondot:
|
|
822
810
|
_os.makedirs(dst_dir, exist_ok=True)
|
|
823
811
|
|
|
824
|
-
if
|
|
825
|
-
|
|
812
|
+
if isinstance(sync, DeferredSync) and sync.defer:
|
|
813
|
+
sync.rename_file.append((src_path, dst_path, allow_overwrites, src_dir, dst_dir))
|
|
826
814
|
return
|
|
827
815
|
|
|
828
816
|
rename(src_path, dst_path, allow_overwrites, makedirs=False, dst_dir=dst_dir)
|
|
829
817
|
|
|
830
|
-
if
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
818
|
+
if isinstance(sync, DeferredSync):
|
|
819
|
+
sync.fsync_dir.add(dst_dir)
|
|
820
|
+
return
|
|
821
|
+
|
|
822
|
+
if sync:
|
|
823
|
+
if _POSIX:
|
|
824
|
+
fsync_file(dst_dir, _os.O_DIRECTORY)
|
|
825
|
+
if src_dir != dst_dir:
|
|
826
|
+
fsync_file(src_dir, _os.O_DIRECTORY)
|
|
827
|
+
else:
|
|
828
|
+
# on Windows, some docs claim, this helps
|
|
829
|
+
fsync_file(dst_path)
|
|
837
830
|
|
|
838
831
|
|
|
839
832
|
def make_file(
|
|
@@ -842,7 +835,7 @@ def make_file(
|
|
|
842
835
|
allow_overwrites: bool = False,
|
|
843
836
|
*,
|
|
844
837
|
makedirs: bool = True,
|
|
845
|
-
|
|
838
|
+
sync: DeferredSync[_t.AnyStr] | bool = True,
|
|
846
839
|
) -> None:
|
|
847
840
|
"""Create a file using a given `make_dst` function."""
|
|
848
841
|
|
|
@@ -854,24 +847,28 @@ def make_file(
|
|
|
854
847
|
|
|
855
848
|
if makedirs and nondot:
|
|
856
849
|
_os.makedirs(dst_dir, exist_ok=True)
|
|
857
|
-
make_dst(dst_path,
|
|
850
|
+
make_dst(dst_path, isinstance(sync, bool))
|
|
858
851
|
|
|
859
|
-
if
|
|
860
|
-
|
|
861
|
-
|
|
852
|
+
if isinstance(sync, DeferredSync):
|
|
853
|
+
sync.fsync_file.add(dst_path)
|
|
854
|
+
sync.fsync_dir.add(dst_dir)
|
|
862
855
|
return
|
|
863
856
|
|
|
864
|
-
if _POSIX:
|
|
857
|
+
if sync and _POSIX:
|
|
865
858
|
fsync_file(dst_dir, _os.O_DIRECTORY)
|
|
866
859
|
|
|
867
860
|
|
|
861
|
+
_dot_part = ".part"
|
|
862
|
+
_dot_partb = b".part"
|
|
863
|
+
|
|
864
|
+
|
|
868
865
|
def atomic_make_file(
|
|
869
866
|
make_dst: _t.Callable[[_t.AnyStr, bool], None],
|
|
870
867
|
dst_path: _t.AnyStr,
|
|
871
868
|
allow_overwrites: bool = False,
|
|
872
869
|
*,
|
|
873
870
|
makedirs: bool = True,
|
|
874
|
-
|
|
871
|
+
sync: DeferredSync[_t.AnyStr] | bool = True,
|
|
875
872
|
) -> None:
|
|
876
873
|
"""Atomically create a file using a given `make_dst` function. This
|
|
877
874
|
runs `make_dst` on a `.part` path first, `fsync`s it, then does
|
|
@@ -885,29 +882,35 @@ def atomic_make_file(
|
|
|
885
882
|
raise FileExistsError(_errno.EEXIST, _os.strerror(_errno.EEXIST), dst_path)
|
|
886
883
|
|
|
887
884
|
if isinstance(dst_path, str):
|
|
888
|
-
tmp_path = dst_path +
|
|
885
|
+
tmp_path = dst_path + _dot_part
|
|
889
886
|
else:
|
|
890
|
-
tmp_path = dst_path +
|
|
887
|
+
tmp_path = dst_path + _dot_partb
|
|
891
888
|
|
|
892
889
|
dst_dir, nondot = dirname_dot(dst_path)
|
|
893
890
|
|
|
894
891
|
if makedirs and nondot:
|
|
895
892
|
_os.makedirs(dst_dir, exist_ok=True)
|
|
896
|
-
make_dst(tmp_path,
|
|
893
|
+
make_dst(tmp_path, isinstance(sync, bool) and sync)
|
|
897
894
|
|
|
898
|
-
if
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
895
|
+
if isinstance(sync, DeferredSync) and sync.defer:
|
|
896
|
+
sync.tmp_file.add(tmp_path)
|
|
897
|
+
sync.fsync_file.add(tmp_path)
|
|
898
|
+
sync.rename_file.append((tmp_path, dst_path, allow_overwrites, dst_dir, dst_dir))
|
|
902
899
|
return
|
|
903
900
|
|
|
904
901
|
try:
|
|
902
|
+
if sync:
|
|
903
|
+
fsync_file(tmp_path)
|
|
905
904
|
rename(tmp_path, dst_path, allow_overwrites, dst_dir=dst_dir)
|
|
906
905
|
except Exception:
|
|
907
906
|
unlink_maybe(tmp_path)
|
|
908
907
|
raise
|
|
909
908
|
|
|
910
|
-
if
|
|
909
|
+
if isinstance(sync, DeferredSync):
|
|
910
|
+
sync.fsync_dir.add(dst_dir)
|
|
911
|
+
return
|
|
912
|
+
|
|
913
|
+
if sync and _POSIX:
|
|
911
914
|
fsync_file(dst_dir, _os.O_DIRECTORY)
|
|
912
915
|
# NB: src_dir == dst_dir
|
|
913
916
|
|
|
@@ -919,7 +922,7 @@ def atomic_copy2(
|
|
|
919
922
|
*,
|
|
920
923
|
follow_symlinks: bool = True,
|
|
921
924
|
makedirs: bool = True,
|
|
922
|
-
|
|
925
|
+
sync: DeferredSync[_t.AnyStr] | bool = True,
|
|
923
926
|
) -> None:
|
|
924
927
|
"""Atomically copy `src_path` to `dst_path`."""
|
|
925
928
|
|
|
@@ -942,7 +945,7 @@ def atomic_copy2(
|
|
|
942
945
|
|
|
943
946
|
# always use the atomic version here, like rsync does,
|
|
944
947
|
# since copying can be interrupted in the middle
|
|
945
|
-
atomic_make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs,
|
|
948
|
+
atomic_make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs, sync=sync)
|
|
946
949
|
|
|
947
950
|
|
|
948
951
|
def atomic_link(
|
|
@@ -952,7 +955,7 @@ def atomic_link(
|
|
|
952
955
|
*,
|
|
953
956
|
follow_symlinks: bool = True,
|
|
954
957
|
makedirs: bool = True,
|
|
955
|
-
|
|
958
|
+
sync: DeferredSync[_t.AnyStr] | bool = True,
|
|
956
959
|
) -> None:
|
|
957
960
|
"""Atomically hardlink `src_path` to `dst_path`."""
|
|
958
961
|
|
|
@@ -963,10 +966,10 @@ def atomic_link(
|
|
|
963
966
|
_os.link(src_path, dst_path, follow_symlinks=follow_symlinks)
|
|
964
967
|
|
|
965
968
|
# _os.link is atomic, so non-atomic make_file is ok
|
|
966
|
-
if allow_overwrites:
|
|
967
|
-
atomic_make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs,
|
|
969
|
+
if allow_overwrites or (isinstance(sync, DeferredSync) and sync.defer):
|
|
970
|
+
atomic_make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs, sync=sync)
|
|
968
971
|
else:
|
|
969
|
-
make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs,
|
|
972
|
+
make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs, sync=sync)
|
|
970
973
|
|
|
971
974
|
|
|
972
975
|
def atomic_symlink(
|
|
@@ -976,7 +979,7 @@ def atomic_symlink(
|
|
|
976
979
|
*,
|
|
977
980
|
follow_symlinks: bool = True,
|
|
978
981
|
makedirs: bool = True,
|
|
979
|
-
|
|
982
|
+
sync: DeferredSync[_t.AnyStr] | bool = True,
|
|
980
983
|
) -> None:
|
|
981
984
|
"""Atomically symlink `src_path` to `dst_path`."""
|
|
982
985
|
|
|
@@ -987,10 +990,10 @@ def atomic_symlink(
|
|
|
987
990
|
_os.symlink(src_path, dst_path)
|
|
988
991
|
|
|
989
992
|
# _os.symlink is atomic, so non-atomic make_file is ok
|
|
990
|
-
if allow_overwrites:
|
|
991
|
-
atomic_make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs,
|
|
993
|
+
if allow_overwrites or (isinstance(sync, DeferredSync) and sync.defer):
|
|
994
|
+
atomic_make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs, sync=sync)
|
|
992
995
|
else:
|
|
993
|
-
make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs,
|
|
996
|
+
make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs, sync=sync)
|
|
994
997
|
|
|
995
998
|
|
|
996
999
|
def atomic_link_or_copy2(
|
|
@@ -1000,16 +1003,16 @@ def atomic_link_or_copy2(
|
|
|
1000
1003
|
*,
|
|
1001
1004
|
follow_symlinks: bool = True,
|
|
1002
1005
|
makedirs: bool = True,
|
|
1003
|
-
|
|
1006
|
+
sync: DeferredSync[_t.AnyStr] | bool = True,
|
|
1004
1007
|
) -> None:
|
|
1005
1008
|
"""Atomically hardlink or copy `src_path` to `dst_path`."""
|
|
1006
1009
|
|
|
1007
1010
|
try:
|
|
1008
|
-
atomic_link(src_path, dst_path, allow_overwrites, follow_symlinks=follow_symlinks, makedirs=makedirs,
|
|
1011
|
+
atomic_link(src_path, dst_path, allow_overwrites, follow_symlinks=follow_symlinks, makedirs=makedirs, sync=sync) # fmt: skip
|
|
1009
1012
|
except OSError as exc:
|
|
1010
1013
|
if exc.errno != _errno.EXDEV:
|
|
1011
1014
|
raise
|
|
1012
|
-
atomic_copy2(src_path, dst_path, allow_overwrites, follow_symlinks=follow_symlinks, makedirs=makedirs,
|
|
1015
|
+
atomic_copy2(src_path, dst_path, allow_overwrites, follow_symlinks=follow_symlinks, makedirs=makedirs, sync=sync) # fmt: skip
|
|
1013
1016
|
|
|
1014
1017
|
|
|
1015
1018
|
def atomic_move(
|
|
@@ -1019,7 +1022,7 @@ def atomic_move(
|
|
|
1019
1022
|
*,
|
|
1020
1023
|
follow_symlinks: bool = False,
|
|
1021
1024
|
makedirs: bool = True,
|
|
1022
|
-
|
|
1025
|
+
sync: DeferredSync[_t.AnyStr] | bool = True,
|
|
1023
1026
|
) -> None:
|
|
1024
1027
|
"""Atomically move `src_path` to `dst_path`.
|
|
1025
1028
|
|
|
@@ -1029,17 +1032,17 @@ def atomic_move(
|
|
|
1029
1032
|
|
|
1030
1033
|
src_dir, _ = dirname_dot(src_path)
|
|
1031
1034
|
|
|
1032
|
-
atomic_link_or_copy2(src_path, dst_path, allow_overwrites, follow_symlinks=follow_symlinks, makedirs=makedirs,
|
|
1035
|
+
atomic_link_or_copy2(src_path, dst_path, allow_overwrites, follow_symlinks=follow_symlinks, makedirs=makedirs, sync=sync) # fmt: skip
|
|
1033
1036
|
|
|
1034
|
-
if
|
|
1035
|
-
after =
|
|
1037
|
+
if isinstance(sync, DeferredSync):
|
|
1038
|
+
after = sync.after
|
|
1036
1039
|
after.unlink_file.add(src_path)
|
|
1037
1040
|
after.fsync_dir.add(src_dir)
|
|
1038
1041
|
return
|
|
1039
1042
|
|
|
1040
1043
|
_os.unlink(src_path)
|
|
1041
1044
|
|
|
1042
|
-
if _POSIX:
|
|
1045
|
+
if sync and _POSIX:
|
|
1043
1046
|
fsync_file(src_dir, _os.O_DIRECTORY)
|
|
1044
1047
|
|
|
1045
1048
|
|
|
@@ -1049,7 +1052,7 @@ def atomic_write(
|
|
|
1049
1052
|
allow_overwrites: bool = False,
|
|
1050
1053
|
*,
|
|
1051
1054
|
makedirs: bool = True,
|
|
1052
|
-
|
|
1055
|
+
sync: DeferredSync[_t.AnyStr] | bool = True,
|
|
1053
1056
|
) -> None:
|
|
1054
1057
|
"""Atomically write given `data` to `dst_path`."""
|
|
1055
1058
|
|
|
@@ -1064,4 +1067,39 @@ def atomic_write(
|
|
|
1064
1067
|
unlink_maybe(tmp_path)
|
|
1065
1068
|
raise
|
|
1066
1069
|
|
|
1067
|
-
atomic_make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs,
|
|
1070
|
+
atomic_make_file(make_dst, dst_path, allow_overwrites, makedirs=makedirs, sync=sync)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def atomic_unlink(
|
|
1074
|
+
path: _t.AnyStr,
|
|
1075
|
+
*,
|
|
1076
|
+
sync: DeferredSync[_t.AnyStr] | bool = True,
|
|
1077
|
+
) -> None:
|
|
1078
|
+
"""Atomically unlink `path`."""
|
|
1079
|
+
|
|
1080
|
+
dirname, _ = dirname_dot(path)
|
|
1081
|
+
|
|
1082
|
+
if isinstance(sync, DeferredSync) and sync.defer:
|
|
1083
|
+
sync.unlink_file.add(path)
|
|
1084
|
+
sync.fsync_dir.add(dirname)
|
|
1085
|
+
return
|
|
1086
|
+
|
|
1087
|
+
_os.unlink(path)
|
|
1088
|
+
|
|
1089
|
+
if isinstance(sync, DeferredSync):
|
|
1090
|
+
sync.fsync_dir.add(dirname)
|
|
1091
|
+
return
|
|
1092
|
+
|
|
1093
|
+
if sync and _POSIX:
|
|
1094
|
+
fsync_file(dirname, _os.O_DIRECTORY)
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def setup_fs(prog: str | None = None, ext: str = ".part", add_pid: bool = True) -> None:
|
|
1098
|
+
"""Setup temporary files prefix."""
|
|
1099
|
+
if prog is None:
|
|
1100
|
+
prog = _op.basename(_sys.argv[0])
|
|
1101
|
+
|
|
1102
|
+
global _dot_part, _dot_partb
|
|
1103
|
+
pid = f"_{_os.getpid()}" if add_pid else ""
|
|
1104
|
+
_dot_part = f".{prog}{pid}{ext}"
|
|
1105
|
+
_dot_partb = _dot_part.encode("ascii")
|
|
@@ -78,7 +78,6 @@ def printf(
|
|
|
78
78
|
Colors get applied to `prefix`, rendered `pattern`, and `suffix`, but not to
|
|
79
79
|
`indent` and `end`, so that, e.g., `printf(..., indent=" " * 4`,
|
|
80
80
|
background=1)` produces an indented background-colored block.
|
|
81
|
-
|
|
82
81
|
"""
|
|
83
82
|
if file is None:
|
|
84
83
|
file = stdout
|
|
@@ -93,25 +92,29 @@ def printf(
|
|
|
93
92
|
end = file.eol
|
|
94
93
|
|
|
95
94
|
data = pattern % args
|
|
95
|
+
extra = len(prefix) + len(indent) + len(suffix)
|
|
96
96
|
|
|
97
97
|
lines = 0
|
|
98
|
+
implicit_eol = False
|
|
98
99
|
if start:
|
|
99
100
|
file.write(start)
|
|
100
101
|
lines += _numlines(start)
|
|
101
102
|
for line in data.splitlines(True):
|
|
102
|
-
w = len(
|
|
103
|
+
w = len(line) + extra
|
|
103
104
|
if indent:
|
|
104
105
|
file.write(indent)
|
|
105
106
|
lines += _numlines(indent)
|
|
106
107
|
if line[-1:] in ("\n", b"\n"):
|
|
107
108
|
file.write_ln(prefix + line[:-1] + suffix, **kwargs)
|
|
108
|
-
lines += 1
|
|
109
|
+
lines += 1
|
|
110
|
+
w -= 1
|
|
109
111
|
else:
|
|
110
112
|
file.write(prefix + line + suffix, **kwargs)
|
|
111
|
-
|
|
113
|
+
lines += w // width
|
|
114
|
+
implicit_eol = w > 0 and w % width == 0
|
|
112
115
|
if end:
|
|
113
116
|
file.write(end)
|
|
114
|
-
lines += _numlines(end)
|
|
117
|
+
lines += max(0, _numlines(end) - 1) if implicit_eol else _numlines(end)
|
|
115
118
|
if flush is True or flush is None and file.isatty():
|
|
116
119
|
file.flush()
|
|
117
120
|
return lines
|
|
@@ -153,16 +153,12 @@ class ANSILogHandler(_logging.StreamHandler[_TIOWrappedWriter]):
|
|
|
153
153
|
After a `reset` with default arguments:
|
|
154
154
|
|
|
155
155
|
- If next `emit`s repeat the previous message, these repeats will will
|
|
156
|
-
be counted separately
|
|
156
|
+
be counted separately.
|
|
157
157
|
|
|
158
158
|
- If the last message was both ephemeral and printed, it will simply
|
|
159
159
|
become pesistent.
|
|
160
160
|
|
|
161
161
|
- Next `update` timeout for new ephemeral messages will be reset.
|
|
162
|
-
|
|
163
|
-
Unless you want to commit the last ephemeral message to view (which can
|
|
164
|
-
be done with `update` followed by `reset`), you should use `flush`
|
|
165
|
-
instead.
|
|
166
162
|
"""
|
|
167
163
|
self.last = new
|
|
168
164
|
self.repeats = repeats
|
|
@@ -240,30 +236,27 @@ class ANSILogHandler(_logging.StreamHandler[_TIOWrappedWriter]):
|
|
|
240
236
|
stream.flush()
|
|
241
237
|
self.repeats_printed = True
|
|
242
238
|
|
|
243
|
-
def rollback(self) -> None:
|
|
244
|
-
"""Remove the last printed message from view, but not forget it, i.e. the next
|
|
245
|
-
`update` will re-print it again, if no `emit`s were done in the
|
|
246
|
-
meantime.
|
|
247
|
-
"""
|
|
248
|
-
self._rollback()
|
|
249
|
-
self.stream.flush()
|
|
250
|
-
self.reset(self.last, self.repeats)
|
|
251
|
-
|
|
252
239
|
def flush(self) -> None:
|
|
253
240
|
"""Remove any ephemeral state from view and/or commit non-ephemeral state to
|
|
254
241
|
view.
|
|
255
242
|
|
|
256
|
-
I.e., if the last message was ephemeral,
|
|
257
|
-
|
|
243
|
+
I.e., if the last message was not ephemeral, `update` and `reset`.
|
|
244
|
+
|
|
245
|
+
Otherwise, remove the last message from view, but don't forget about it,
|
|
246
|
+
allowing it to be re-printed with `update`.
|
|
247
|
+
|
|
248
|
+
If you want to commit the last ephemeral message to view, do `update`
|
|
249
|
+
and `reset` manually.
|
|
258
250
|
"""
|
|
259
251
|
last = self.last
|
|
260
252
|
if last is not None and last[0]:
|
|
261
253
|
# ephemeral
|
|
262
254
|
self._rollback()
|
|
263
255
|
self.stream.flush()
|
|
256
|
+
self.reset(self.last, self.repeats)
|
|
264
257
|
else:
|
|
265
258
|
self.update()
|
|
266
|
-
|
|
259
|
+
self.reset()
|
|
267
260
|
|
|
268
261
|
def emit(self, record: _logging.LogRecord) -> None:
|
|
269
262
|
"""`logging.Handler` implementation."""
|
|
@@ -65,12 +65,19 @@ class SignalInterrupt(BaseException):
|
|
|
65
65
|
# signal number
|
|
66
66
|
signum: int
|
|
67
67
|
# was this signal raised forcefully?
|
|
68
|
-
forced: bool
|
|
68
|
+
forced: bool = _dc.field(default=True)
|
|
69
69
|
|
|
70
70
|
def __post_init__(self) -> None:
|
|
71
71
|
BaseException.__init__(self)
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
class GentleSignalInterrupt(SignalInterrupt):
|
|
75
|
+
"""Unforced `SignalInterrupt`."""
|
|
76
|
+
|
|
77
|
+
def __init__(self, signum: int) -> None:
|
|
78
|
+
super().__init__(signum, False)
|
|
79
|
+
|
|
80
|
+
|
|
74
81
|
delay_signal_message: dict[int, str] = {}
|
|
75
82
|
delay_signal_message_default = "Interrupting, gently."
|
|
76
83
|
|
|
@@ -119,7 +126,7 @@ def delay_signal_handler(signum: int, _frame: _t.Any) -> None:
|
|
|
119
126
|
`yes_signals` block.
|
|
120
127
|
"""
|
|
121
128
|
|
|
122
|
-
|
|
129
|
+
num = _delayed_signals.get(signum, 0) + 1
|
|
123
130
|
|
|
124
131
|
enabled, verbose = _delay_signals_state
|
|
125
132
|
if isinstance(verbose, str):
|
|
@@ -134,7 +141,13 @@ def delay_signal_handler(signum: int, _frame: _t.Any) -> None:
|
|
|
134
141
|
start="\n",
|
|
135
142
|
color=1,
|
|
136
143
|
)
|
|
137
|
-
|
|
144
|
+
try:
|
|
145
|
+
del _delayed_signals[signum]
|
|
146
|
+
except KeyError:
|
|
147
|
+
pass
|
|
148
|
+
raise SignalInterrupt(signum)
|
|
149
|
+
|
|
150
|
+
_delayed_signals[signum] = num
|
|
138
151
|
|
|
139
152
|
if verbose:
|
|
140
153
|
_printf_err(
|
|
@@ -198,19 +211,20 @@ def pop_delayed_signal() -> int | None:
|
|
|
198
211
|
|
|
199
212
|
|
|
200
213
|
def raise_first_delayed_signal() -> None:
|
|
201
|
-
"""Raise `
|
|
214
|
+
"""Raise `GentleSignalInterrupt(signum)` with the first `signum` delayed by
|
|
202
215
|
`delay_signal_handler`, do nothing if no signals bound to
|
|
203
216
|
`delay_signal_handler` were recieved.
|
|
204
217
|
|
|
205
218
|
This function can be called repeatedly, in which case it will raise
|
|
206
|
-
`
|
|
219
|
+
`GentleSignalInterrupt`s for each delayed signal in order they were
|
|
220
|
+
received.
|
|
207
221
|
"""
|
|
208
222
|
try:
|
|
209
223
|
signum, _num = _delayed_signals.popitem(False)
|
|
210
224
|
except KeyError:
|
|
211
225
|
pass
|
|
212
226
|
else:
|
|
213
|
-
raise
|
|
227
|
+
raise GentleSignalInterrupt(signum)
|
|
214
228
|
|
|
215
229
|
|
|
216
230
|
def raise_delayed_signals() -> None:
|
|
@@ -220,7 +234,7 @@ def raise_delayed_signals() -> None:
|
|
|
220
234
|
excs = []
|
|
221
235
|
while len(_delayed_signals) > 0:
|
|
222
236
|
signum, _num = _delayed_signals.popitem(False)
|
|
223
|
-
excs.append(
|
|
237
|
+
excs.append(GentleSignalInterrupt(signum))
|
|
224
238
|
if len(excs) == 0:
|
|
225
239
|
return
|
|
226
240
|
if len(excs) == 1:
|
|
@@ -243,8 +257,8 @@ def no_signals(*, do_raise: bool = True, verbose: bool | str = True) -> _t.Itera
|
|
|
243
257
|
When `do_raise` is set (which is the default), if this is the topmost
|
|
244
258
|
`no_signals` block, or if the parent block is `yes_signals`, then
|
|
245
259
|
`raise_delayed_signals` will be called right after this block finishes.
|
|
246
|
-
I.e., any delayed `
|
|
247
|
-
this block.
|
|
260
|
+
I.e., any delayed `GentleSignalInterrupt`s will be raised immediately at
|
|
261
|
+
exit from this block.
|
|
248
262
|
|
|
249
263
|
You can also `raise` them at certain program points by calling
|
|
250
264
|
`raise_first_delayed_signal` or `raise_delayed_signals` manually from inside
|
|
@@ -267,7 +281,7 @@ def no_signals(*, do_raise: bool = True, verbose: bool | str = True) -> _t.Itera
|
|
|
267
281
|
try:
|
|
268
282
|
yield None
|
|
269
283
|
except Exception:
|
|
270
|
-
# pretend `
|
|
284
|
+
# pretend `GentleSignalInterrupt` was raised first
|
|
271
285
|
if do_raise and old[0]:
|
|
272
286
|
raise_delayed_signals()
|
|
273
287
|
raise
|
|
@@ -305,16 +319,18 @@ def soft_sleep(seconds: int | float, *, verbose: bool | str = False) -> None:
|
|
|
305
319
|
"""Start `yes_signals(verbose=verbose)` block, if doing that does not not
|
|
306
320
|
trigger `raise_delayed_signals`, run `time.sleep` with the given argument,
|
|
307
321
|
if the latter gets interrupted with a `SignalInterrupt`, raise
|
|
308
|
-
`
|
|
322
|
+
`GentleSignalInterrupt` with the same `signum` instead.
|
|
309
323
|
|
|
310
324
|
Note that you should probably use `try ... except*`, not simply `try ...
|
|
311
325
|
except` with this, unless you know what you are doing.
|
|
312
326
|
"""
|
|
327
|
+
# NB: there's no need to handle `BaseExceptionGroup` here since this these
|
|
328
|
+
# `except` clauses will only be executed right after `delay_signal_handler`
|
|
329
|
+
# `raise`s.
|
|
313
330
|
try:
|
|
314
331
|
with yes_signals(verbose=verbose):
|
|
315
332
|
sleep(seconds)
|
|
333
|
+
except GentleSignalInterrupt:
|
|
334
|
+
raise
|
|
316
335
|
except SignalInterrupt as exc:
|
|
317
|
-
|
|
318
|
-
# will only needs to be executed right after `delay_signal_handler`
|
|
319
|
-
# `raise`s.
|
|
320
|
-
raise SignalInterrupt(exc.signum, False) from exc
|
|
336
|
+
raise GentleSignalInterrupt(exc.signum) from exc
|
|
@@ -482,6 +482,13 @@ def unescape(
|
|
|
482
482
|
)
|
|
483
483
|
|
|
484
484
|
|
|
485
|
+
### Path escaping
|
|
486
|
+
|
|
487
|
+
path_escaper: StringLexer[str] = escaper("\\")
|
|
488
|
+
escape_path = path_escaper.escape
|
|
489
|
+
unescape_path = path_escaper.unescape
|
|
490
|
+
|
|
491
|
+
|
|
485
492
|
### URL-quoting
|
|
486
493
|
|
|
487
494
|
_safe_url_chars: dict[tuple[str, str], _t.Callable[[int | str], bool]] = {}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: kisstdlib
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: System programming with conceptually and algebraically simple primitives.
|
|
5
5
|
Author-email: Jan Malakhovski <oxij@oxij.org>
|
|
6
6
|
Project-URL: Homepage, https://oxij.org/software/kisstdlib/
|
|
@@ -9,7 +9,7 @@ namespaces = true
|
|
|
9
9
|
"kisstdlib" = ["py.typed"]
|
|
10
10
|
[project]
|
|
11
11
|
name = "kisstdlib"
|
|
12
|
-
version = "0.0.
|
|
12
|
+
version = "0.0.10"
|
|
13
13
|
authors = [{ name = "Jan Malakhovski", email = "oxij@oxij.org" }]
|
|
14
14
|
description = "System programming with conceptually and algebraically simple primitives."
|
|
15
15
|
readme = "README.md"
|
|
@@ -56,26 +56,26 @@ def test_iter_subtree(tmp_path: str) -> None:
|
|
|
56
56
|
]
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def atomic1(
|
|
59
|
+
def atomic1(defer: bool, tmp_path: str) -> None:
|
|
60
60
|
os.chdir(tmp_path)
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
if
|
|
64
|
-
|
|
62
|
+
sync: DeferredSync[str] | bool = True
|
|
63
|
+
if defer:
|
|
64
|
+
sync = DeferredSync(True)
|
|
65
65
|
|
|
66
66
|
def check(es: list[list[str]], ed: list[list[str]] | None = None) -> None:
|
|
67
|
-
if
|
|
67
|
+
if isinstance(sync, DeferredSync):
|
|
68
68
|
gs: list[list[str]] = []
|
|
69
|
-
|
|
69
|
+
sync.copy().sync(True, gs)
|
|
70
70
|
assert es == gs
|
|
71
71
|
|
|
72
72
|
if ed is not None:
|
|
73
73
|
gd = list(describe_forest(["."], hash_length=8))
|
|
74
74
|
assert ed == gd
|
|
75
75
|
|
|
76
|
-
atomic_write(b"test a", "test.a",
|
|
77
|
-
atomic_write(b"test b", "test.b",
|
|
78
|
-
atomic_write(b"test c", "test.c",
|
|
76
|
+
atomic_write(b"test a", "test.a", sync=sync)
|
|
77
|
+
atomic_write(b"test b", "test.b", sync=sync)
|
|
78
|
+
atomic_write(b"test c", "test.c", sync=sync)
|
|
79
79
|
check(
|
|
80
80
|
[
|
|
81
81
|
["fsync", "test.a.part"],
|
|
@@ -90,7 +90,7 @@ def atomic1(sync: bool, tmp_path: str) -> None:
|
|
|
90
90
|
["fsync_dir", "."],
|
|
91
91
|
]
|
|
92
92
|
)
|
|
93
|
-
atomic_rename("test.a", "a/test.a", False,
|
|
93
|
+
atomic_rename("test.a", "a/test.a", False, sync=sync)
|
|
94
94
|
check(
|
|
95
95
|
[
|
|
96
96
|
["fsync", "test.a.part"],
|
|
@@ -110,7 +110,7 @@ def atomic1(sync: bool, tmp_path: str) -> None:
|
|
|
110
110
|
["fsync_dir", "."],
|
|
111
111
|
]
|
|
112
112
|
)
|
|
113
|
-
atomic_rename("test.b", "a/test.b", False,
|
|
113
|
+
atomic_rename("test.b", "a/test.b", False, sync=sync)
|
|
114
114
|
check(
|
|
115
115
|
[
|
|
116
116
|
["fsync", "test.a.part"],
|
|
@@ -132,7 +132,7 @@ def atomic1(sync: bool, tmp_path: str) -> None:
|
|
|
132
132
|
["fsync_dir", "."],
|
|
133
133
|
]
|
|
134
134
|
)
|
|
135
|
-
atomic_rename("test.c", "b/test.c", False,
|
|
135
|
+
atomic_rename("test.c", "b/test.c", False, sync=sync)
|
|
136
136
|
check(
|
|
137
137
|
[
|
|
138
138
|
["fsync", "test.a.part"],
|
|
@@ -157,7 +157,7 @@ def atomic1(sync: bool, tmp_path: str) -> None:
|
|
|
157
157
|
["fsync_dir", "."],
|
|
158
158
|
]
|
|
159
159
|
)
|
|
160
|
-
atomic_rename("a/test.b", "test.b", False,
|
|
160
|
+
atomic_rename("a/test.b", "test.b", False, sync=sync)
|
|
161
161
|
check(
|
|
162
162
|
[
|
|
163
163
|
["fsync", "test.a.part"],
|
|
@@ -187,8 +187,8 @@ def atomic1(sync: bool, tmp_path: str) -> None:
|
|
|
187
187
|
["fsync_dir", "a"],
|
|
188
188
|
]
|
|
189
189
|
)
|
|
190
|
-
if
|
|
191
|
-
|
|
190
|
+
if isinstance(sync, DeferredSync):
|
|
191
|
+
sync.flush()
|
|
192
192
|
check(
|
|
193
193
|
[],
|
|
194
194
|
[
|
|
@@ -201,8 +201,8 @@ def atomic1(sync: bool, tmp_path: str) -> None:
|
|
|
201
201
|
],
|
|
202
202
|
)
|
|
203
203
|
|
|
204
|
-
atomic_link("a/test.a", "test.a", True,
|
|
205
|
-
atomic_symlink("b/test.c", "test.c", True,
|
|
204
|
+
atomic_link("a/test.a", "test.a", True, sync=sync)
|
|
205
|
+
atomic_symlink("b/test.c", "test.c", True, sync=sync)
|
|
206
206
|
check(
|
|
207
207
|
[
|
|
208
208
|
["fsync", "test.a.part"],
|
|
@@ -214,14 +214,14 @@ def atomic1(sync: bool, tmp_path: str) -> None:
|
|
|
214
214
|
["fsync_dir", "."],
|
|
215
215
|
]
|
|
216
216
|
)
|
|
217
|
-
if
|
|
218
|
-
|
|
217
|
+
if isinstance(sync, DeferredSync):
|
|
218
|
+
sync.flush()
|
|
219
219
|
check([])
|
|
220
220
|
|
|
221
|
-
atomic_copy2("test.a", "x/test.a", True,
|
|
222
|
-
atomic_copy2("test.c", "x/test.c", True,
|
|
223
|
-
atomic_link("test.c", "x/test.c.lnk", True, follow_symlinks=False,
|
|
224
|
-
atomic_copy2("test.c", "x/test.c.sym", True, follow_symlinks=False,
|
|
221
|
+
atomic_copy2("test.a", "x/test.a", True, sync=sync)
|
|
222
|
+
atomic_copy2("test.c", "x/test.c", True, sync=sync)
|
|
223
|
+
atomic_link("test.c", "x/test.c.lnk", True, follow_symlinks=False, sync=sync)
|
|
224
|
+
atomic_copy2("test.c", "x/test.c.sym", True, follow_symlinks=False, sync=sync)
|
|
225
225
|
check(
|
|
226
226
|
[
|
|
227
227
|
["fsync", "x/test.a.part"],
|
|
@@ -239,8 +239,8 @@ def atomic1(sync: bool, tmp_path: str) -> None:
|
|
|
239
239
|
["fsync_dir", "x"],
|
|
240
240
|
]
|
|
241
241
|
)
|
|
242
|
-
if
|
|
243
|
-
|
|
242
|
+
if isinstance(sync, DeferredSync):
|
|
243
|
+
sync.flush()
|
|
244
244
|
check(
|
|
245
245
|
[],
|
|
246
246
|
[
|
|
@@ -260,11 +260,11 @@ def atomic1(sync: bool, tmp_path: str) -> None:
|
|
|
260
260
|
],
|
|
261
261
|
)
|
|
262
262
|
|
|
263
|
-
atomic_copy2("test.a", "y/test.a", True,
|
|
264
|
-
atomic_copy2("test.c", "y/test.c", True,
|
|
265
|
-
atomic_link("test.c", "y/test.c.lnk", True, follow_symlinks=False,
|
|
266
|
-
atomic_copy2("test.c", "y/test.c.sym", True, follow_symlinks=False,
|
|
267
|
-
atomic_move("test.c", "z/test.c", True,
|
|
263
|
+
atomic_copy2("test.a", "y/test.a", True, sync=sync)
|
|
264
|
+
atomic_copy2("test.c", "y/test.c", True, sync=sync)
|
|
265
|
+
atomic_link("test.c", "y/test.c.lnk", True, follow_symlinks=False, sync=sync)
|
|
266
|
+
atomic_copy2("test.c", "y/test.c.sym", True, follow_symlinks=False, sync=sync)
|
|
267
|
+
atomic_move("test.c", "z/test.c", True, sync=sync)
|
|
268
268
|
check(
|
|
269
269
|
[
|
|
270
270
|
["fsync", "y/test.a.part"],
|
|
@@ -288,8 +288,8 @@ def atomic1(sync: bool, tmp_path: str) -> None:
|
|
|
288
288
|
["fsync_dir", "."],
|
|
289
289
|
]
|
|
290
290
|
)
|
|
291
|
-
if
|
|
292
|
-
|
|
291
|
+
if isinstance(sync, DeferredSync):
|
|
292
|
+
sync.flush()
|
|
293
293
|
check(
|
|
294
294
|
[],
|
|
295
295
|
[
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|