boost-skill-cli 1.0.52__tar.gz → 1.0.53__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.
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/PKG-INFO +1 -1
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/_version.py +2 -2
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/config.py +3 -2
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/journal.py +3 -1
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/lockfile.py +27 -2
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/util.py +32 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_skill_cli.egg-info/PKG-INFO +1 -1
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_lockfile.py +25 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_util.py +44 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/.gitignore +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/CLAUDE.md +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/CONTRIBUTING.md +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/LICENSE +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/MANIFEST.in +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/Makefile +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/README.md +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/SECURITY.md +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/__init__.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/__main__.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/cli.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/cliparse.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/__init__.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/bmad.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/configuration.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/discovery.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/hooks.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/info.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/intelligence.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/pkg.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/quality.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/taps.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/commands/team.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/__init__.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/agents.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/ai.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/catalog.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/claude_settings.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/dense.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/embed.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/frontmatter.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/gitutil.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/logs.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/mcp.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/output.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/paths.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/policy.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/rag.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/registry.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/core/store.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/data/registries.json +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/errors.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_cli/spin.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_skill_cli.egg-info/SOURCES.txt +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_skill_cli.egg-info/dependency_links.txt +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_skill_cli.egg-info/entry_points.txt +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_skill_cli.egg-info/requires.txt +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_skill_cli.egg-info/top_level.txt +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/docs/.claude/settings.local.json +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/docs/DEBUGGING.md +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/docs/demo.gif +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/docs/demo.tape +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/docs/design-roadmap.html +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/docs/index.html +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/docs/rag-architecture.md +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/docs/roadmap.html +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/index.html +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/pyproject.toml +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/scripts/build_registries.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/scripts/mutation_gate.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/setup.cfg +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/style/README.md +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/style/boost.css +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/style/boost.js +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/style/demo.html +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/conftest.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_bmad.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_configuration.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_discovery.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_hooks.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_info.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_intelligence.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_pkg.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_quality.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_taps.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_team.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_everyday_loop.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/make_fixture.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/smoke.sh +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_agents.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_ai.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_catalog.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_claude_settings.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_cliparse.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_config.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_dense.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_embed.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_errors_and_cli_table.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_frontmatter.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_gitutil.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_journal.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_logs.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_mcp.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_mutation_hardening.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_output.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_paths.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_policy.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_rag.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_registry.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_spin.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_store.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_token_parity.py +0 -0
- {boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/unit/test_version.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 0,
|
|
21
|
+
__version__ = version = '1.0.53'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 53)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
from copy import deepcopy
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
|
-
from . import paths
|
|
8
|
+
from . import paths, util
|
|
9
9
|
|
|
10
10
|
DEFAULTS = {
|
|
11
11
|
"agents": {
|
|
@@ -90,7 +90,8 @@ def load() -> dict:
|
|
|
90
90
|
|
|
91
91
|
def save(cfg: dict) -> None:
|
|
92
92
|
paths.ensure_dirs()
|
|
93
|
-
|
|
93
|
+
util.atomic_write_text(
|
|
94
|
+
paths.config_path(), json.dumps(cfg, indent=2, sort_keys=False) + "\n")
|
|
94
95
|
|
|
95
96
|
|
|
96
97
|
def get(dotted: str, default=None):
|
|
@@ -67,4 +67,6 @@ def _maybe_rotate() -> None:
|
|
|
67
67
|
except OSError:
|
|
68
68
|
return
|
|
69
69
|
if len(lines) > ROTATE_AT:
|
|
70
|
-
|
|
70
|
+
# Atomic swap: a bare write_text here races concurrent log() appends and
|
|
71
|
+
# can truncate the feed mid-line if interrupted.
|
|
72
|
+
util.atomic_write_text(p, "\n".join(lines[-ROTATE_KEEP:]) + "\n")
|
|
@@ -26,16 +26,41 @@ def _skeleton() -> dict:
|
|
|
26
26
|
def read() -> dict:
|
|
27
27
|
p = paths.lockfile_path()
|
|
28
28
|
if not p.exists():
|
|
29
|
-
return _skeleton()
|
|
29
|
+
return _skeleton() # empty: never installed anything yet
|
|
30
30
|
try:
|
|
31
31
|
lock = json.loads(p.read_text())
|
|
32
32
|
except (json.JSONDecodeError, OSError):
|
|
33
|
+
# Corrupt (present but unparseable) is NOT the same as empty: returning
|
|
34
|
+
# a bare skeleton here would let the next write() overwrite the only
|
|
35
|
+
# record of every prior install. Preserve the bytes for recovery and
|
|
36
|
+
# surface it loudly before falling back to the skeleton.
|
|
37
|
+
_preserve_corrupt(p)
|
|
33
38
|
return _skeleton()
|
|
34
39
|
lock.setdefault("version", SCHEMA_VERSION)
|
|
35
40
|
lock.setdefault("skills", {})
|
|
36
41
|
return lock
|
|
37
42
|
|
|
38
43
|
|
|
44
|
+
def _preserve_corrupt(p) -> None:
|
|
45
|
+
"""Copy an unparseable lock file aside as ``<lock>.corrupt`` and warn.
|
|
46
|
+
|
|
47
|
+
Best effort: a read must still succeed (returning the skeleton) even if the
|
|
48
|
+
sidecar cannot be written, so failures here are swallowed after logging.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
backup = p.with_name(p.name + ".corrupt")
|
|
52
|
+
shutil.copy(p, backup)
|
|
53
|
+
except OSError:
|
|
54
|
+
backup = None
|
|
55
|
+
try:
|
|
56
|
+
from . import logs
|
|
57
|
+
logs.get_logger().warning(
|
|
58
|
+
"lock file %s is corrupt; preserved %s and continuing with an "
|
|
59
|
+
"empty lock", p, backup or "(backup failed)")
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
39
64
|
def write(lock: dict) -> None:
|
|
40
65
|
paths.ensure_dirs()
|
|
41
66
|
p = paths.lockfile_path()
|
|
@@ -52,7 +77,7 @@ def write(lock: dict) -> None:
|
|
|
52
77
|
_prune_history()
|
|
53
78
|
lock["version"] = SCHEMA_VERSION
|
|
54
79
|
lock["updated"] = util.now_iso()
|
|
55
|
-
|
|
80
|
+
util.atomic_write_text(p, json.dumps(lock, indent=2, sort_keys=True) + "\n")
|
|
56
81
|
|
|
57
82
|
|
|
58
83
|
def _history_files() -> List:
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import hashlib
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
7
|
+
import tempfile
|
|
6
8
|
from datetime import datetime, timezone
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
from typing import List, Tuple
|
|
@@ -10,6 +12,36 @@ from typing import List, Tuple
|
|
|
10
12
|
IGNORED = {".git", "__pycache__", ".DS_Store"}
|
|
11
13
|
|
|
12
14
|
|
|
15
|
+
def atomic_write_text(path: Path, text: str, encoding: str = "utf-8") -> None:
|
|
16
|
+
"""Write ``text`` to ``path`` so a reader never sees a partial file.
|
|
17
|
+
|
|
18
|
+
The bytes go to a temp file in the *same* directory (so the rename stays on
|
|
19
|
+
one filesystem and is atomic), get flushed and fsynced to disk, then swap
|
|
20
|
+
into place with ``os.replace``. An interrupted or concurrent write can no
|
|
21
|
+
longer truncate the target: the previous file survives intact until the
|
|
22
|
+
complete new one atomically replaces it. This is the corruption-safe
|
|
23
|
+
substitute for a bare ``Path.write_text`` on any file boost must not lose —
|
|
24
|
+
the lock file, the config, the pulse journal.
|
|
25
|
+
"""
|
|
26
|
+
path = Path(path)
|
|
27
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
fd, tmp = tempfile.mkstemp(dir=str(path.parent),
|
|
29
|
+
prefix="." + path.name + ".", suffix=".tmp")
|
|
30
|
+
try:
|
|
31
|
+
with os.fdopen(fd, "w", encoding=encoding) as f:
|
|
32
|
+
f.write(text)
|
|
33
|
+
f.flush()
|
|
34
|
+
os.fsync(f.fileno())
|
|
35
|
+
os.replace(tmp, str(path))
|
|
36
|
+
except BaseException:
|
|
37
|
+
# Never leave a temp turd behind, and never mask the original error.
|
|
38
|
+
try:
|
|
39
|
+
os.unlink(tmp)
|
|
40
|
+
except OSError:
|
|
41
|
+
pass
|
|
42
|
+
raise
|
|
43
|
+
|
|
44
|
+
|
|
13
45
|
def now_iso() -> str:
|
|
14
46
|
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
15
47
|
|
|
@@ -35,6 +35,31 @@ class TestRead:
|
|
|
35
35
|
assert lock["version"] == 3
|
|
36
36
|
assert lock["skills"] == {}
|
|
37
37
|
|
|
38
|
+
def test_corrupt_file_preserved_as_sidecar(self, sandbox):
|
|
39
|
+
paths.ensure_dirs()
|
|
40
|
+
p = paths.lockfile_path()
|
|
41
|
+
p.write_text("{definitely not json")
|
|
42
|
+
lockfile.read()
|
|
43
|
+
backup = p.with_name(p.name + ".corrupt")
|
|
44
|
+
assert backup.exists()
|
|
45
|
+
assert backup.read_text() == "{definitely not json"
|
|
46
|
+
|
|
47
|
+
def test_missing_file_leaves_no_sidecar(self, sandbox):
|
|
48
|
+
lockfile.read()
|
|
49
|
+
p = paths.lockfile_path()
|
|
50
|
+
assert not p.with_name(p.name + ".corrupt").exists()
|
|
51
|
+
|
|
52
|
+
def test_corrupt_read_does_not_lose_prior_records(self, sandbox):
|
|
53
|
+
# A corrupt lock followed by a new install must not silently erase the
|
|
54
|
+
# only surviving copy of the earlier records — they live on in .corrupt.
|
|
55
|
+
paths.ensure_dirs()
|
|
56
|
+
p = paths.lockfile_path()
|
|
57
|
+
p.write_text('{"skills": {"old": {"version": "1.0"}}, TRAILING GARBAGE')
|
|
58
|
+
lockfile.set_skill("new", {"version": "2.0"})
|
|
59
|
+
assert set(lockfile.installed()) == {"new"} # skeleton took over
|
|
60
|
+
backup = p.with_name(p.name + ".corrupt")
|
|
61
|
+
assert '"old"' in backup.read_text() # but old bytes survive
|
|
62
|
+
|
|
38
63
|
def test_missing_keys_defaulted(self, sandbox):
|
|
39
64
|
paths.ensure_dirs()
|
|
40
65
|
paths.lockfile_path().write_text('{"skills": {"a": {}}}')
|
|
@@ -364,3 +364,47 @@ class TestScoreSkill:
|
|
|
364
364
|
assert len(text) == 48_000
|
|
365
365
|
d = make_skill(tmp_path, text)
|
|
366
366
|
assert util.score_skill(d) == (95, [])
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class TestAtomicWriteText:
|
|
370
|
+
def test_writes_content(self, tmp_path):
|
|
371
|
+
p = tmp_path / "f.txt"
|
|
372
|
+
util.atomic_write_text(p, "hello")
|
|
373
|
+
assert p.read_text() == "hello"
|
|
374
|
+
|
|
375
|
+
def test_overwrites_existing(self, tmp_path):
|
|
376
|
+
p = tmp_path / "f.txt"
|
|
377
|
+
p.write_text("old")
|
|
378
|
+
util.atomic_write_text(p, "new")
|
|
379
|
+
assert p.read_text() == "new"
|
|
380
|
+
|
|
381
|
+
def test_creates_missing_parents(self, tmp_path):
|
|
382
|
+
p = tmp_path / "a" / "b" / "f.txt"
|
|
383
|
+
util.atomic_write_text(p, "deep")
|
|
384
|
+
assert p.read_text() == "deep"
|
|
385
|
+
|
|
386
|
+
def test_leaves_no_temp_files(self, tmp_path):
|
|
387
|
+
util.atomic_write_text(tmp_path / "f.txt", "x")
|
|
388
|
+
leftovers = [q.name for q in tmp_path.iterdir() if q.name != "f.txt"]
|
|
389
|
+
assert leftovers == []
|
|
390
|
+
|
|
391
|
+
def test_honours_encoding(self, tmp_path):
|
|
392
|
+
p = tmp_path / "f.txt"
|
|
393
|
+
util.atomic_write_text(p, "café", encoding="utf-8")
|
|
394
|
+
assert p.read_bytes() == "café".encode("utf-8")
|
|
395
|
+
|
|
396
|
+
def test_replace_failure_cleans_temp_and_raises(self, tmp_path, monkeypatch):
|
|
397
|
+
import os as _os
|
|
398
|
+
p = tmp_path / "f.txt"
|
|
399
|
+
p.write_text("original")
|
|
400
|
+
|
|
401
|
+
def boom(_src, _dst):
|
|
402
|
+
raise OSError("replace failed")
|
|
403
|
+
|
|
404
|
+
monkeypatch.setattr(_os, "replace", boom)
|
|
405
|
+
with pytest.raises(OSError, match="replace failed"):
|
|
406
|
+
util.atomic_write_text(p, "new")
|
|
407
|
+
# original untouched, and no temp turd left behind
|
|
408
|
+
assert p.read_text() == "original"
|
|
409
|
+
leftovers = [q.name for q in tmp_path.iterdir() if q.name != "f.txt"]
|
|
410
|
+
assert leftovers == []
|
|
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
|
|
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
|
{boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/boost_skill_cli.egg-info/dependency_links.txt
RENAMED
|
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
|
{boost_skill_cli-1.0.52 → boost_skill_cli-1.0.53}/tests/functional/test_cli_configuration.py
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|