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.
Files changed (41) hide show
  1. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/PKG-INFO +1 -1
  2. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/__init__.py +22 -6
  3. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/argparse_ext.py +40 -6
  4. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/base.py +1 -0
  5. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/fs.py +129 -91
  6. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/io/stdio.py +8 -5
  7. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/logging_ext.py +10 -17
  8. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/signal_ext.py +31 -15
  9. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/string_ext.py +7 -0
  10. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/PKG-INFO +1 -1
  11. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/pyproject.toml +1 -1
  12. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/test/test_fs.py +32 -32
  13. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/LICENSE.txt +0 -0
  14. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/README.md +0 -0
  15. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/compression.py +0 -0
  16. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/failure.py +0 -0
  17. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/getpass_ext.py +0 -0
  18. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/io/__init__.py +0 -0
  19. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/io/base.py +0 -0
  20. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/io/encoder.py +0 -0
  21. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/io/wrapper.py +0 -0
  22. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/itertools_ext.py +0 -0
  23. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/parsing.py +0 -0
  24. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/py.typed +0 -0
  25. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/pyrepr.py +0 -0
  26. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/sorted.py +0 -0
  27. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/sqlite3_ext.py +0 -0
  28. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/tariter.py +0 -0
  29. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib/time.py +0 -0
  30. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/SOURCES.txt +0 -0
  31. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/dependency_links.txt +0 -0
  32. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/entry_points.txt +0 -0
  33. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/requires.txt +0 -0
  34. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib.egg-info/top_level.txt +0 -0
  35. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/kisstdlib_bin/describe_forest.py +0 -0
  36. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/setup.cfg +0 -0
  37. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/setup.py +0 -0
  38. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/test/test_basics.py +0 -0
  39. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/test/test_io.py +0 -0
  40. {kisstdlib-0.0.9 → kisstdlib-0.0.10}/test/test_sorted.py +0 -0
  41. {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.9
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
- signals: list[str] | None = None,
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
- setup_delay_signals(signals)
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
- logger.flush()
106
- stdout.flush()
107
- stderr.flush()
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 OptionallyMarkdownHelpAction(Action):
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
- help: str | None = None, # pylint: disable=redefined-builtin
168
+ **kwargs: _t.Any,
169
169
  ) -> None:
170
170
  super().__init__(
171
- option_strings=option_strings, dest=dest, default=default, nargs=0, help=help
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="help",
291
+ action="help_omd",
258
292
  default=SUPPRESS,
259
293
  help=_gettext("show this help message and exit"),
260
294
  )
@@ -30,6 +30,7 @@ import traceback as _traceback
30
30
  import typing as _t
31
31
 
32
32
  from decimal import Decimal
33
+ from functools import partial, partialmethod # pylint: disable=unused-import
33
34
 
34
35
 
35
36
  ### Primitive types
@@ -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 fsdecode_maybe(x: str | bytes) -> str:
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
- x = _os.path.dirname(x)
67
- if isinstance(x, bytes):
68
- if x == b"":
54
+ path = _os.path.dirname(path)
55
+ if isinstance(path, bytes):
56
+ if path == b"":
69
57
  return b".", False
70
- return x, True
71
- if x == "":
58
+ return path, True
59
+ if path == "":
72
60
  return ".", False
73
- return x, True
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(fsencode_maybe(path1))
104
- b = _os.readlink(fsencode_maybe(path2))
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 `%s`: [Errno %d, %s] %s: %s",
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 _escaper("\\").escape # type: ignore
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 = fsdecode_maybe(_op.relpath(fpath, dirpath))
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(fsdecode_maybe(_op.relpath(habs_path, dirname)))
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(fsdecode_maybe(_os.readlink(abs_path)))
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 commit(
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
- `commit` and writes the log of things it would do into that argument.
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", fsdecode_maybe(f)] for f in sorted(self.unlink_file)]
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", fsdecode_maybe(f)] for f in sorted(self.fsync_file)]
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", fsdecode_maybe(src_path), fsdecode_maybe(dst_path)]) # fmt: skip
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", fsdecode_maybe(f)] for f in sorted(self.fsync_file)]
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", fsdecode_maybe(f)] for f in sorted(self.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", fsdecode_maybe(f)] for f in sorted(self.fsync_dir2)]
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.commit(strict, simulate)
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 finish(self, strict: bool = True) -> None:
797
- """Like `commit` but raises collected exceptions as an exception group and calls
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.commit(strict)
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
- dsync: DeferredSync[_t.AnyStr] | None = None,
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 dsync is not None:
825
- dsync.rename_file.append((src_path, dst_path, allow_overwrites, src_dir, dst_dir))
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 _POSIX:
831
- fsync_file(dst_dir, _os.O_DIRECTORY)
832
- if src_dir != dst_dir:
833
- fsync_file(src_dir, _os.O_DIRECTORY)
834
- else:
835
- # on Windows, some docs claim, this helps
836
- fsync_file(dst_path)
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
- dsync: DeferredSync[_t.AnyStr] | None = None,
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, dsync is None)
850
+ make_dst(dst_path, isinstance(sync, bool))
858
851
 
859
- if dsync is not None:
860
- dsync.fsync_file.add(dst_path)
861
- dsync.fsync_dir.add(dst_dir)
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
- dsync: DeferredSync[_t.AnyStr] | None = None,
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 + ".part"
885
+ tmp_path = dst_path + _dot_part
889
886
  else:
890
- tmp_path = dst_path + b".part"
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, dsync is None)
893
+ make_dst(tmp_path, isinstance(sync, bool) and sync)
897
894
 
898
- if dsync is not None:
899
- dsync.tmp_file.add(tmp_path)
900
- dsync.fsync_file.add(tmp_path)
901
- dsync.rename_file.append((tmp_path, dst_path, allow_overwrites, dst_dir, dst_dir))
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 _POSIX:
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
- dsync: DeferredSync[_t.AnyStr] | None = None,
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, dsync=dsync)
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
- dsync: DeferredSync[_t.AnyStr] | None = None,
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, dsync=dsync)
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, dsync=dsync)
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
- dsync: DeferredSync[_t.AnyStr] | None = None,
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, dsync=dsync)
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, dsync=dsync)
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
- dsync: DeferredSync[_t.AnyStr] | None = None,
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, dsync=dsync) # fmt: skip
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, dsync=dsync) # fmt: skip
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
- dsync: DeferredSync[_t.AnyStr] | None = None,
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, dsync=dsync) # fmt: skip
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 dsync is not None:
1035
- after = dsync.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
- dsync: DeferredSync[_t.AnyStr] | None = None,
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, dsync=dsync)
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(indent) + len(prefix) + len(line) + len(suffix)
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 + (w - 1) // width
109
+ lines += 1
110
+ w -= 1
109
111
  else:
110
112
  file.write(prefix + line + suffix, **kwargs)
111
- lines += w // width
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 from the previous message.
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, remove it from view and `reset`.
257
- Otherwise, `update` and `reset`.
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
- self.reset()
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
- _delayed_signals[signum] = num = _delayed_signals.get(signum, 0) + 1
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
- raise SignalInterrupt(signum, True)
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 `SignalInterrupt(signum, False)` with the first `signum` delayed by
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
- `SignalInterrupt`s for each delayed signal in order they were received.
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 SignalInterrupt(signum, False)
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(SignalInterrupt(signum, False))
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 `SignalInterrupt`s will be raised immediately at exit from
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 `SignalInterrupt` was raised first
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
- `SignalInterrupt` with the same `signum` but with `forced=False`.
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
- # NB: there's no need to handle `BaseExceptionGroup` here since this
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.9
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.9"
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(sync: bool, tmp_path: str) -> None:
59
+ def atomic1(defer: bool, tmp_path: str) -> None:
60
60
  os.chdir(tmp_path)
61
61
 
62
- dsync = None
63
- if sync:
64
- dsync = DeferredSync()
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 dsync is not None:
67
+ if isinstance(sync, DeferredSync):
68
68
  gs: list[list[str]] = []
69
- dsync.copy().commit(True, gs)
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", dsync=dsync)
77
- atomic_write(b"test b", "test.b", dsync=dsync)
78
- atomic_write(b"test c", "test.c", dsync=dsync)
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, dsync=dsync)
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, dsync=dsync)
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, dsync=dsync)
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, dsync=dsync)
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 dsync is not None:
191
- dsync.finish()
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, dsync=dsync)
205
- atomic_symlink("b/test.c", "test.c", True, dsync=dsync)
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 dsync is not None:
218
- dsync.finish()
217
+ if isinstance(sync, DeferredSync):
218
+ sync.flush()
219
219
  check([])
220
220
 
221
- atomic_copy2("test.a", "x/test.a", True, dsync=dsync)
222
- atomic_copy2("test.c", "x/test.c", True, dsync=dsync)
223
- atomic_link("test.c", "x/test.c.lnk", True, follow_symlinks=False, dsync=dsync)
224
- atomic_copy2("test.c", "x/test.c.sym", True, follow_symlinks=False, dsync=dsync)
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 dsync is not None:
243
- dsync.finish()
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, dsync=dsync)
264
- atomic_copy2("test.c", "y/test.c", True, dsync=dsync)
265
- atomic_link("test.c", "y/test.c.lnk", True, follow_symlinks=False, dsync=dsync)
266
- atomic_copy2("test.c", "y/test.c.sym", True, follow_symlinks=False, dsync=dsync)
267
- atomic_move("test.c", "z/test.c", True, dsync=dsync)
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 dsync is not None:
292
- dsync.finish()
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