skit-cli 0.0.1__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.
- skit/__init__.py +3 -0
- skit/analyzer.py +410 -0
- skit/argspec.py +528 -0
- skit/argstate.py +166 -0
- skit/atomic.py +101 -0
- skit/cli.py +1621 -0
- skit/config.py +255 -0
- skit/editor.py +77 -0
- skit/flows.py +596 -0
- skit/i18n.py +324 -0
- skit/inlineform.py +61 -0
- skit/launcher.py +326 -0
- skit/locales/zh_CN/LC_MESSAGES/skit.mo +0 -0
- skit/locales/zh_TW/LC_MESSAGES/skit.mo +0 -0
- skit/metawriter.py +267 -0
- skit/models.py +154 -0
- skit/paths.py +44 -0
- skit/pep723.py +292 -0
- skit/promptform.py +67 -0
- skit/reconcile.py +321 -0
- skit/shim.py +386 -0
- skit/store.py +566 -0
- skit/theme.py +123 -0
- skit/tokens.py +112 -0
- skit/tui.py +774 -0
- skit/tui_add.py +361 -0
- skit/tui_footer.py +38 -0
- skit/tui_form.py +586 -0
- skit/tui_health.py +150 -0
- skit/tui_prefs.py +175 -0
- skit/tui_settings.py +374 -0
- skit/uvman.py +288 -0
- skit_cli-0.0.1.dist-info/METADATA +199 -0
- skit_cli-0.0.1.dist-info/RECORD +37 -0
- skit_cli-0.0.1.dist-info/WHEEL +4 -0
- skit_cli-0.0.1.dist-info/entry_points.txt +3 -0
- skit_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
skit/atomic.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Atomic writes: every state/metadata file goes through tmp + os.replace (C7)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import tomllib
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import tomli_w
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _fsync_dir(dir_path: Path) -> None:
|
|
19
|
+
"""Fsync a directory fd so a prior os.replace()'s rename entry is durable on stable storage.
|
|
20
|
+
|
|
21
|
+
POSIX only: directories can never be opened for writing, but a read-only fd is enough --
|
|
22
|
+
fsync() flushes the underlying inode's dirty metadata regardless of which fd requested it.
|
|
23
|
+
Callers must guard with `sys.platform != "win32"` (os.open can't open a directory there) and
|
|
24
|
+
wrap this in contextlib.suppress(OSError): not every filesystem supports fsync on a
|
|
25
|
+
directory, and that failure must never undo or fail a write os.replace() already committed.
|
|
26
|
+
"""
|
|
27
|
+
fd = os.open(dir_path, os.O_RDONLY)
|
|
28
|
+
try:
|
|
29
|
+
os.fsync(fd)
|
|
30
|
+
finally:
|
|
31
|
+
os.close(fd)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def atomic_write_bytes(path: Path, data: bytes) -> None:
|
|
35
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.", suffix=".tmp")
|
|
37
|
+
try:
|
|
38
|
+
with os.fdopen(fd, "wb") as f:
|
|
39
|
+
f.write(data)
|
|
40
|
+
f.flush()
|
|
41
|
+
os.fsync(f.fileno()) # durable on disk BEFORE the rename, not just before this returns
|
|
42
|
+
os.replace(tmp, path)
|
|
43
|
+
if sys.platform != "win32": # os.open can't open a directory on Windows
|
|
44
|
+
with contextlib.suppress(OSError):
|
|
45
|
+
_fsync_dir(path.parent) # best-effort: persist the rename's directory entry too
|
|
46
|
+
except BaseException:
|
|
47
|
+
with contextlib.suppress(OSError):
|
|
48
|
+
os.unlink(tmp)
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def atomic_write_text(path: Path, text: str) -> None:
|
|
53
|
+
atomic_write_bytes(path, text.encode("utf-8")) # pragma: no mutate — utf-8/UTF-8 alias
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def atomic_write_toml(path: Path, doc: dict[str, Any]) -> None:
|
|
57
|
+
atomic_write_bytes(path, tomli_w.dumps(doc).encode("utf-8")) # pragma: no mutate — alias
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class TomlRecovery:
|
|
62
|
+
"""Result of load_toml_recoverable().
|
|
63
|
+
|
|
64
|
+
`doc` is the parsed table when the file is absent or parses fine (and `corrupt` is False,
|
|
65
|
+
`backup_path` is None). When the file exists but fails to parse, `doc` is always {} and
|
|
66
|
+
`corrupt` is True; `backup_path` then names where the original got copied to before the
|
|
67
|
+
caller treats it as empty and overwrites it — or is None when even the backup attempt failed
|
|
68
|
+
(e.g. a read-only config dir), so the caller can still warn the user which case it hit.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
doc: dict[str, Any]
|
|
72
|
+
corrupt: bool
|
|
73
|
+
backup_path: Path | None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_toml_recoverable(path: Path) -> TomlRecovery:
|
|
77
|
+
"""Read a TOML file for a read-modify-write save, without silently discarding a corrupt one.
|
|
78
|
+
|
|
79
|
+
A plain best-effort load (catch-and-return-{}) is fine for read-only callers, but a
|
|
80
|
+
read-modify-write saver that started from that {} would then overwrite the file with just the
|
|
81
|
+
one key it just set — silently destroying every other saved setting with no trace. So here: if
|
|
82
|
+
the file exists but fails to parse, it is first copied to `<path>.bak` (best-effort) before
|
|
83
|
+
being treated as empty, so the caller can proceed *and* tell the user where to recover from.
|
|
84
|
+
|
|
85
|
+
Headless: this module has no gettext dependency, so it never prints anything itself — callers
|
|
86
|
+
own the warning (worded for their own context) based on `corrupt` / `backup_path`. This is what
|
|
87
|
+
lets both config.py and i18n.py (which config.py imports, so the reverse would be a cycle)
|
|
88
|
+
share the exact same backup mechanics.
|
|
89
|
+
"""
|
|
90
|
+
if not path.is_file():
|
|
91
|
+
return TomlRecovery(doc={}, corrupt=False, backup_path=None)
|
|
92
|
+
try:
|
|
93
|
+
with open(path, "rb") as f:
|
|
94
|
+
return TomlRecovery(doc=tomllib.load(f), corrupt=False, backup_path=None)
|
|
95
|
+
except (OSError, tomllib.TOMLDecodeError):
|
|
96
|
+
backup = path.with_name(path.name + ".bak")
|
|
97
|
+
try:
|
|
98
|
+
shutil.copy2(path, backup)
|
|
99
|
+
except OSError:
|
|
100
|
+
return TomlRecovery(doc={}, corrupt=True, backup_path=None)
|
|
101
|
+
return TomlRecovery(doc={}, corrupt=True, backup_path=backup)
|