code-standards 7.0.0__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.
- code_standards-7.0.0.dist-info/METADATA +53 -0
- code_standards-7.0.0.dist-info/RECORD +99 -0
- code_standards-7.0.0.dist-info/WHEEL +4 -0
- code_standards-7.0.0.dist-info/entry_points.txt +3 -0
- code_standards-7.0.0.dist-info/licenses/LICENSE +21 -0
- sarj_standards/__init__.py +30 -0
- sarj_standards/__main__.py +5 -0
- sarj_standards/_meta.py +22 -0
- sarj_standards/api.py +890 -0
- sarj_standards/cli/__init__.py +0 -0
- sarj_standards/cli/main.py +2466 -0
- sarj_standards/configs/cli-reference.v1.json +1 -0
- sarj_standards/configs/doctor.config.json +22 -0
- sarj_standards/configs/eslint.application.mjs +1366 -0
- sarj_standards/configs/eslint.peers.json +44 -0
- sarj_standards/configs/eslint.strict.mjs +1060 -0
- sarj_standards/configs/markdownlint.strict.yaml +12 -0
- sarj_standards/configs/pyright.strict.json +96 -0
- sarj_standards/configs/ruff.application.toml +363 -0
- sarj_standards/configs/ruff.strict.toml +338 -0
- sarj_standards/configs/rule-inventory.v1.json +1 -0
- sarj_standards/configs/rule-ledger.json +846 -0
- sarj_standards/configs/rule-warning-levels.v1.json +1 -0
- sarj_standards/configs/taplo.strict.toml +14 -0
- sarj_standards/configs/yamllint.strict.yaml +25 -0
- sarj_standards/libs/__init__.py +0 -0
- sarj_standards/libs/adoption/__init__.py +0 -0
- sarj_standards/libs/adoption/configs.py +36 -0
- sarj_standards/libs/adoption/doctor.py +1346 -0
- sarj_standards/libs/adoption/exclusions.py +66 -0
- sarj_standards/libs/adoption/hooks.py +423 -0
- sarj_standards/libs/adoption/launcher.py +240 -0
- sarj_standards/libs/adoption/lifecycle.py +493 -0
- sarj_standards/libs/adoption/manifest.py +550 -0
- sarj_standards/libs/adoption/packagemanager.py +285 -0
- sarj_standards/libs/adoption/retired_suppressions.py +371 -0
- sarj_standards/libs/adoption/scaffold.py +1660 -0
- sarj_standards/libs/adoption/service.py +441 -0
- sarj_standards/libs/adoption/transaction.py +274 -0
- sarj_standards/libs/adoption/upgrade.py +516 -0
- sarj_standards/libs/adoption/uvtool.py +62 -0
- sarj_standards/libs/catalogs/__init__.py +9 -0
- sarj_standards/libs/catalogs/slack_automations.py +627 -0
- sarj_standards/libs/corpus/__init__.py +25 -0
- sarj_standards/libs/corpus/manifest.py +211 -0
- sarj_standards/libs/corpus/snapshot.py +222 -0
- sarj_standards/libs/diagnostics/__init__.py +65 -0
- sarj_standards/libs/diagnostics/analysis.schema.json +161 -0
- sarj_standards/libs/diagnostics/baseline.py +131 -0
- sarj_standards/libs/diagnostics/models.py +574 -0
- sarj_standards/libs/diagnostics/serialize.py +290 -0
- sarj_standards/libs/diagnostics/source.py +172 -0
- sarj_standards/libs/filesystem.py +11 -0
- sarj_standards/libs/linting/__init__.py +0 -0
- sarj_standards/libs/linting/analysis.py +422 -0
- sarj_standards/libs/linting/external.py +1454 -0
- sarj_standards/libs/linting/library_policy.py +688 -0
- sarj_standards/libs/linting/policy.py +152 -0
- sarj_standards/libs/linting/runner.py +442 -0
- sarj_standards/libs/linting/textlint.py +1605 -0
- sarj_standards/libs/release/__init__.py +98 -0
- sarj_standards/libs/release/_values.py +24 -0
- sarj_standards/libs/release/artifacts.py +191 -0
- sarj_standards/libs/release/causality.py +80 -0
- sarj_standards/libs/release/changes.py +48 -0
- sarj_standards/libs/release/process.py +128 -0
- sarj_standards/libs/release/publish.py +85 -0
- sarj_standards/libs/release/registry.py +271 -0
- sarj_standards/libs/release/release_age.py +218 -0
- sarj_standards/libs/release/rollout.py +1163 -0
- sarj_standards/libs/release/tags.py +373 -0
- sarj_standards/libs/release/typescript.py +191 -0
- sarj_standards/libs/repository/__init__.py +0 -0
- sarj_standards/libs/repository/cli_reference_artifact.py +324 -0
- sarj_standards/libs/repository/comment_corpus.py +536 -0
- sarj_standards/libs/repository/config_generation.py +146 -0
- sarj_standards/libs/repository/docs.py +347 -0
- sarj_standards/libs/repository/hooks.py +118 -0
- sarj_standards/libs/repository/ledger.py +99 -0
- sarj_standards/libs/repository/repository.py +744 -0
- sarj_standards/libs/repository/rule_authoring.py +246 -0
- sarj_standards/libs/repository/rule_catalog_artifact.py +479 -0
- sarj_standards/libs/repository/rule_changes.py +318 -0
- sarj_standards/libs/repository/rule_inventory_artifact.py +142 -0
- sarj_standards/libs/repository/rule_lifecycle.py +167 -0
- sarj_standards/libs/repository/rule_maintenance.py +225 -0
- sarj_standards/libs/rules/__init__.py +74 -0
- sarj_standards/libs/rules/catalog.py +145 -0
- sarj_standards/libs/rules/contracts.py +382 -0
- sarj_standards/libs/rules/corpus_runner.py +365 -0
- sarj_standards/libs/rules/evaluation.py +177 -0
- sarj_standards/libs/setup/__init__.py +4 -0
- sarj_standards/libs/setup/repository.py +40 -0
- sarj_standards/py.typed +0 -0
- sarj_standards/schemas/__init__.py +4 -0
- sarj_standards/schemas/_paths.py +7 -0
- sarj_standards/schemas/rule-catalog.v1.json +1 -0
- sarj_standards/schemas/rule-catalog.v1.schema.json +112 -0
- sarj_standards/schemas/slack-automations.v1.schema.json +1751 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from sarj_standards._meta import CONFIGS_DIR
|
|
10
|
+
from sarj_standards.libs.filesystem import is_link_like
|
|
11
|
+
|
|
12
|
+
from . import lifecycle, manifest, scaffold, transaction
|
|
13
|
+
from .configs import APPLICATION_CONFIG_NAMES, CONFIG_NAMES, PYTHON_CONFIGS, TYPESCRIPT_COMPANION_CONFIGS
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from collections.abc import Sequence
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_INSTALL_MUTATED_NAMES = frozenset(
|
|
21
|
+
{
|
|
22
|
+
"bun.lock",
|
|
23
|
+
"bun.lockb",
|
|
24
|
+
"package-lock.json",
|
|
25
|
+
"package.json",
|
|
26
|
+
"pnpm-lock.yaml",
|
|
27
|
+
"pyproject.toml",
|
|
28
|
+
"uv.lock",
|
|
29
|
+
"yarn.lock",
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SyncOutcome(StrEnum):
|
|
35
|
+
OK = "ok"
|
|
36
|
+
WRITTEN = "written"
|
|
37
|
+
SKIPPED = "skipped"
|
|
38
|
+
DRIFT = "drift"
|
|
39
|
+
INVALID = "invalid"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _DestinationKind(StrEnum):
|
|
43
|
+
DEFAULT = "default"
|
|
44
|
+
PYTHON = "python"
|
|
45
|
+
TYPESCRIPT = "typescript"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class SyncTarget:
|
|
50
|
+
name: str
|
|
51
|
+
source: Path
|
|
52
|
+
destination: Path
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, slots=True)
|
|
56
|
+
class SyncPlan:
|
|
57
|
+
root: Path
|
|
58
|
+
profile: manifest.Profile
|
|
59
|
+
targets: tuple[SyncTarget, ...]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class SyncRecord:
|
|
64
|
+
target: SyncTarget
|
|
65
|
+
outcome: SyncOutcome
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True, slots=True)
|
|
69
|
+
class SyncResult:
|
|
70
|
+
records: tuple[SyncRecord, ...]
|
|
71
|
+
check: bool
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def status(self) -> int:
|
|
75
|
+
if self.count(SyncOutcome.INVALID):
|
|
76
|
+
return 2
|
|
77
|
+
if self.check and self.count(SyncOutcome.DRIFT):
|
|
78
|
+
return 1
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
def count(self, outcome: SyncOutcome) -> int:
|
|
82
|
+
return sum(record.outcome is outcome for record in self.records)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class InitFailure(StrEnum):
|
|
86
|
+
SYNC = "sync"
|
|
87
|
+
INSTALL = "install"
|
|
88
|
+
INTERRUPTED = "interrupted"
|
|
89
|
+
APPLY = "apply"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class InitPlan:
|
|
94
|
+
scaffold: scaffold.Plan
|
|
95
|
+
sync: SyncPlan | None
|
|
96
|
+
install_commands: tuple[lifecycle.Command, ...]
|
|
97
|
+
preconditions: dict[Path, bytes | None] = field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True, slots=True)
|
|
101
|
+
class InitResult:
|
|
102
|
+
status: int
|
|
103
|
+
sync: SyncResult | None = None
|
|
104
|
+
failure: InitFailure | None = None
|
|
105
|
+
error: str | None = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def plan_sync(
|
|
109
|
+
root: Path,
|
|
110
|
+
*,
|
|
111
|
+
configs: Sequence[str] | None = None,
|
|
112
|
+
python_dest: str | None = None,
|
|
113
|
+
typescript_dest: str | None = None,
|
|
114
|
+
profile: manifest.Profile | None = None,
|
|
115
|
+
) -> SyncPlan:
|
|
116
|
+
resolved = root.resolve()
|
|
117
|
+
if not resolved.is_dir():
|
|
118
|
+
msg = f"destination {resolved} is not a directory"
|
|
119
|
+
raise ValueError(msg)
|
|
120
|
+
adopted = _load_optional_manifest(resolved)
|
|
121
|
+
selected = _selected_configs(configs, adopted)
|
|
122
|
+
selected_profile = profile or (adopted.profile if adopted is not None else "standard")
|
|
123
|
+
destinations: dict[_DestinationKind, Path] = {}
|
|
124
|
+
|
|
125
|
+
def destination(kind: _DestinationKind, override: str | None) -> Path:
|
|
126
|
+
if kind not in destinations:
|
|
127
|
+
recorded = None
|
|
128
|
+
if adopted is not None:
|
|
129
|
+
match kind:
|
|
130
|
+
case _DestinationKind.TYPESCRIPT:
|
|
131
|
+
recorded = adopted.typescript_dest
|
|
132
|
+
case _DestinationKind.PYTHON:
|
|
133
|
+
recorded = adopted.python_dest
|
|
134
|
+
case _DestinationKind.DEFAULT:
|
|
135
|
+
pass
|
|
136
|
+
requested = override or (str(resolved) if recorded is None else str(resolved / recorded))
|
|
137
|
+
destinations[kind] = _contained_destination(resolved, requested, label=kind.value)
|
|
138
|
+
return destinations[kind]
|
|
139
|
+
|
|
140
|
+
targets: list[SyncTarget] = []
|
|
141
|
+
for name in selected:
|
|
142
|
+
standard_source, target_name = CONFIG_NAMES[name]
|
|
143
|
+
source_name = (
|
|
144
|
+
APPLICATION_CONFIG_NAMES.get(name, standard_source)
|
|
145
|
+
if selected_profile == "application"
|
|
146
|
+
else standard_source
|
|
147
|
+
)
|
|
148
|
+
if name == "eslint":
|
|
149
|
+
base = destination(_DestinationKind.TYPESCRIPT, typescript_dest)
|
|
150
|
+
elif name in PYTHON_CONFIGS:
|
|
151
|
+
base = destination(_DestinationKind.PYTHON, python_dest)
|
|
152
|
+
else:
|
|
153
|
+
base = destination(_DestinationKind.DEFAULT, None)
|
|
154
|
+
targets.append(SyncTarget(name, CONFIGS_DIR / source_name, base / target_name))
|
|
155
|
+
if name == "eslint":
|
|
156
|
+
for companion, (companion_source, companion_target) in TYPESCRIPT_COMPANION_CONFIGS.items():
|
|
157
|
+
targets.append(
|
|
158
|
+
SyncTarget(
|
|
159
|
+
companion,
|
|
160
|
+
CONFIGS_DIR / companion_source,
|
|
161
|
+
base / companion_target,
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
return SyncPlan(resolved, selected_profile, tuple(targets))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def apply_sync(plan: SyncPlan, *, force: bool = False, check: bool = False) -> SyncResult:
|
|
168
|
+
records = tuple(
|
|
169
|
+
SyncRecord(target, _sync_one(target, root=plan.root, force=force, check=check)) for target in plan.targets
|
|
170
|
+
)
|
|
171
|
+
return SyncResult(records, check)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def plan_init( # ruff: ignore[too-many-locals] -- one adoption boundary resolves existing and requested policy.
|
|
175
|
+
root: Path,
|
|
176
|
+
*,
|
|
177
|
+
force: bool = False,
|
|
178
|
+
configs: Sequence[str] | None = None,
|
|
179
|
+
python_dest: str | None = None,
|
|
180
|
+
typescript_dest: str | None = None,
|
|
181
|
+
profile: manifest.Profile | None = None,
|
|
182
|
+
hook_manager: manifest.HookManager | None = None,
|
|
183
|
+
) -> InitPlan:
|
|
184
|
+
if profile is not None and profile not in manifest.PROFILES:
|
|
185
|
+
msg = f"profile must be one of: {', '.join(manifest.PROFILES)}"
|
|
186
|
+
raise ValueError(msg)
|
|
187
|
+
resolved = root.resolve()
|
|
188
|
+
adopted = manifest.load_for_setup(resolved)
|
|
189
|
+
already_adopted = adopted is not None
|
|
190
|
+
selected_configs = configs if configs is not None else (adopted.configs if adopted is not None else None)
|
|
191
|
+
selected_profile = profile or (adopted.profile if adopted is not None else "standard")
|
|
192
|
+
selected_python_dest = python_dest or (
|
|
193
|
+
adopted.python_dest
|
|
194
|
+
if adopted is not None and any(name in adopted.configs for name in manifest.PYTHON_CONFIGS)
|
|
195
|
+
else None
|
|
196
|
+
)
|
|
197
|
+
selected_typescript_dest = typescript_dest or (
|
|
198
|
+
adopted.typescript_dest
|
|
199
|
+
if adopted is not None and any(name in adopted.configs for name in manifest.TYPESCRIPT_CONFIGS)
|
|
200
|
+
else None
|
|
201
|
+
)
|
|
202
|
+
selected_hook_manager = hook_manager or (adopted.hook_manager if adopted is not None else None)
|
|
203
|
+
scaffold_plan = scaffold.build_plan(
|
|
204
|
+
resolved,
|
|
205
|
+
force=force,
|
|
206
|
+
update_manifest=any(
|
|
207
|
+
option is not None for option in (configs, python_dest, typescript_dest, profile, hook_manager)
|
|
208
|
+
),
|
|
209
|
+
configs=selected_configs,
|
|
210
|
+
python_dest=selected_python_dest,
|
|
211
|
+
typescript_dest=selected_typescript_dest,
|
|
212
|
+
profile=selected_profile,
|
|
213
|
+
hook_manager=selected_hook_manager,
|
|
214
|
+
)
|
|
215
|
+
if scaffold_plan.errors or (not scaffold_plan.ecosystems.any and configs is None):
|
|
216
|
+
return InitPlan(scaffold_plan, None, ())
|
|
217
|
+
python_target = scaffold.dest_of(resolved, scaffold_plan.ecosystems.python_root)
|
|
218
|
+
typescript_target = scaffold.dest_of(resolved, scaffold_plan.ecosystems.typescript_root)
|
|
219
|
+
sync_plan = plan_sync(
|
|
220
|
+
resolved,
|
|
221
|
+
configs=scaffold_plan.configs,
|
|
222
|
+
python_dest=python_target,
|
|
223
|
+
typescript_dest=typescript_target,
|
|
224
|
+
profile=scaffold_plan.profile,
|
|
225
|
+
)
|
|
226
|
+
if not force and not already_adopted:
|
|
227
|
+
conflicts = tuple(
|
|
228
|
+
target.destination
|
|
229
|
+
for target in sync_plan.targets
|
|
230
|
+
if target.destination.is_file() and target.destination.read_bytes() != target.source.read_bytes()
|
|
231
|
+
)
|
|
232
|
+
if conflicts:
|
|
233
|
+
names = ", ".join(str(path.relative_to(resolved)) for path in conflicts)
|
|
234
|
+
scaffold_plan.errors.append(
|
|
235
|
+
"refusing to overwrite pre-existing lint configuration in an unadopted repository: "
|
|
236
|
+
f"{names}; review the files and rerun with --force"
|
|
237
|
+
)
|
|
238
|
+
return InitPlan(scaffold_plan, None, ())
|
|
239
|
+
mutations = (
|
|
240
|
+
tuple(path for path, _contents in (*scaffold_plan.writes, *scaffold_plan.edits))
|
|
241
|
+
+ tuple(scaffold_plan.deletes)
|
|
242
|
+
+ tuple(target.destination for target in sync_plan.targets)
|
|
243
|
+
)
|
|
244
|
+
transaction.validate_targets(resolved, mutations)
|
|
245
|
+
commands = tuple(
|
|
246
|
+
lifecycle.install_commands(
|
|
247
|
+
resolved,
|
|
248
|
+
scaffold_plan.ecosystems,
|
|
249
|
+
hook_manager=scaffold_plan.hook_manager,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
preconditions = {path: path.read_bytes() if path.is_file() else None for path in mutations}
|
|
253
|
+
return InitPlan(scaffold_plan, sync_plan, commands, preconditions)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def apply_init(plan: InitPlan, *, install: bool = True) -> InitResult:
|
|
257
|
+
if plan.sync is None:
|
|
258
|
+
return InitResult(2, failure=InitFailure.APPLY, error="setup plan is not applicable")
|
|
259
|
+
try:
|
|
260
|
+
transaction.validate_targets(plan.sync.root, tuple(plan.preconditions))
|
|
261
|
+
stale = any(
|
|
262
|
+
(path.read_bytes() if path.is_file() else None) != expected for path, expected in plan.preconditions.items()
|
|
263
|
+
)
|
|
264
|
+
except OSError as exc:
|
|
265
|
+
return InitResult(2, failure=InitFailure.APPLY, error=str(exc))
|
|
266
|
+
if stale:
|
|
267
|
+
return InitResult(2, failure=InitFailure.APPLY, error="setup plan is stale; rerun setup")
|
|
268
|
+
scaffold_targets = tuple(path for path, _contents in (*plan.scaffold.writes, *plan.scaffold.edits)) + tuple(
|
|
269
|
+
plan.scaffold.deletes
|
|
270
|
+
)
|
|
271
|
+
python_environment = (
|
|
272
|
+
None if plan.scaffold.ecosystems.python_root is None else plan.scaffold.ecosystems.python_root / ".venv"
|
|
273
|
+
)
|
|
274
|
+
typescript_root = plan.scaffold.ecosystems.typescript_install_root or plan.scaffold.ecosystems.typescript_root
|
|
275
|
+
node_modules = None if typescript_root is None else typescript_root / "node_modules"
|
|
276
|
+
generated_trees = tuple((path, path.exists()) for path in (python_environment, node_modules) if path is not None)
|
|
277
|
+
file_transaction: transaction.FileTransaction | None = None
|
|
278
|
+
try:
|
|
279
|
+
file_transaction = transaction.FileTransaction.capture(plan.sync.root, scaffold_targets)
|
|
280
|
+
result = _apply_init_transaction(plan, file_transaction, install=install)
|
|
281
|
+
except KeyboardInterrupt:
|
|
282
|
+
rollback_error = _rollback_error(file_transaction)
|
|
283
|
+
cleanup_error = _cleanup_new_trees(generated_trees, failed=True)
|
|
284
|
+
return InitResult(
|
|
285
|
+
130,
|
|
286
|
+
failure=InitFailure.INTERRUPTED,
|
|
287
|
+
error=_join_errors(rollback_error, cleanup_error),
|
|
288
|
+
)
|
|
289
|
+
except OSError as exc:
|
|
290
|
+
rollback_error = _rollback_error(file_transaction)
|
|
291
|
+
cleanup_error = _cleanup_new_trees(generated_trees, failed=True)
|
|
292
|
+
return InitResult(
|
|
293
|
+
2,
|
|
294
|
+
failure=InitFailure.APPLY,
|
|
295
|
+
error=_join_errors(str(exc), rollback_error, cleanup_error),
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
if result.status:
|
|
299
|
+
rollback_error = _rollback_error(file_transaction)
|
|
300
|
+
cleanup_error = _cleanup_new_trees(generated_trees, failed=True)
|
|
301
|
+
error = _join_errors(result.error, rollback_error, cleanup_error)
|
|
302
|
+
status = 2 if error or result.failure is InitFailure.INSTALL else result.status
|
|
303
|
+
return InitResult(status, result.sync, result.failure, error)
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _rollback_error(file_transaction: transaction.FileTransaction | None) -> str | None:
|
|
308
|
+
if file_transaction is None:
|
|
309
|
+
return None
|
|
310
|
+
return file_transaction.rollback().render()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _cleanup_new_trees(trees: tuple[tuple[Path, bool], ...], *, failed: bool) -> str | None:
|
|
314
|
+
if not failed:
|
|
315
|
+
return None
|
|
316
|
+
failures: list[str] = []
|
|
317
|
+
for path, existed in trees:
|
|
318
|
+
if existed or not path.is_dir():
|
|
319
|
+
continue
|
|
320
|
+
try:
|
|
321
|
+
shutil.rmtree(path)
|
|
322
|
+
except OSError as exc:
|
|
323
|
+
failures.append(f"could not remove newly created environment {path}: {exc}")
|
|
324
|
+
return "; ".join(failures) or None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _join_errors(*errors: str | None) -> str | None:
|
|
328
|
+
return "; ".join(error for error in errors if error) or None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _apply_init_transaction(
|
|
332
|
+
plan: InitPlan,
|
|
333
|
+
file_transaction: transaction.FileTransaction,
|
|
334
|
+
*,
|
|
335
|
+
install: bool,
|
|
336
|
+
) -> InitResult:
|
|
337
|
+
if plan.sync is None:
|
|
338
|
+
return InitResult(2, failure=InitFailure.APPLY, error="setup plan is not applicable")
|
|
339
|
+
file_transaction.track(*(target.destination for target in plan.sync.targets))
|
|
340
|
+
sync_result = _apply_planned_sync(plan.sync, plan.preconditions)
|
|
341
|
+
if sync_result.status:
|
|
342
|
+
return InitResult(sync_result.status, sync_result, InitFailure.SYNC)
|
|
343
|
+
scaffold.apply(plan.scaffold, preconditions=plan.preconditions)
|
|
344
|
+
direct_targets = (
|
|
345
|
+
*(target.destination for target in plan.sync.targets),
|
|
346
|
+
*(path for path, _contents in (*plan.scaffold.writes, *plan.scaffold.edits)),
|
|
347
|
+
*plan.scaffold.deletes,
|
|
348
|
+
)
|
|
349
|
+
file_transaction.mark_written(*(path for path in direct_targets if path.name not in _INSTALL_MUTATED_NAMES))
|
|
350
|
+
if install:
|
|
351
|
+
install_status = lifecycle.execute(plan.install_commands)
|
|
352
|
+
if install_status:
|
|
353
|
+
return InitResult(
|
|
354
|
+
2,
|
|
355
|
+
sync_result,
|
|
356
|
+
InitFailure.INSTALL,
|
|
357
|
+
f"dependency or hook installer exited with status {install_status}",
|
|
358
|
+
)
|
|
359
|
+
return InitResult(0, sync_result)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _apply_planned_sync(plan: SyncPlan, preconditions: dict[Path, bytes | None]) -> SyncResult:
|
|
363
|
+
records: list[SyncRecord] = []
|
|
364
|
+
for target in plan.targets:
|
|
365
|
+
transaction.assert_expected(plan.root, target.destination, preconditions[target.destination])
|
|
366
|
+
records.append(SyncRecord(target, _sync_one(target, root=plan.root, force=True, check=False)))
|
|
367
|
+
return SyncResult(records=tuple(records), check=False)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def init_destination(root: Path, name: str, *, python_dest: str, typescript_dest: str) -> Path:
|
|
371
|
+
if name == "eslint":
|
|
372
|
+
base = root / typescript_dest
|
|
373
|
+
elif name in PYTHON_CONFIGS:
|
|
374
|
+
base = root / python_dest
|
|
375
|
+
else:
|
|
376
|
+
base = root
|
|
377
|
+
return base / CONFIG_NAMES[name][1]
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _selected_configs(configs: Sequence[str] | None, adopted: manifest.Manifest | None) -> tuple[str, ...]:
|
|
381
|
+
if configs is not None:
|
|
382
|
+
if isinstance(configs, str):
|
|
383
|
+
msg = "configs must be a sequence of config names, not a string"
|
|
384
|
+
raise TypeError(msg)
|
|
385
|
+
selected = tuple(dict.fromkeys(configs))
|
|
386
|
+
if not selected:
|
|
387
|
+
msg = "configs must contain at least one config name"
|
|
388
|
+
raise ValueError(msg)
|
|
389
|
+
unknown = sorted(set(selected) - set(CONFIG_NAMES))
|
|
390
|
+
if unknown:
|
|
391
|
+
msg = f"unknown configs: {', '.join(unknown)}"
|
|
392
|
+
raise ValueError(msg)
|
|
393
|
+
return selected
|
|
394
|
+
if adopted is not None:
|
|
395
|
+
known = tuple(name for name in adopted.configs if name in CONFIG_NAMES)
|
|
396
|
+
if known:
|
|
397
|
+
return known
|
|
398
|
+
return tuple(CONFIG_NAMES)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _load_optional_manifest(root: Path) -> manifest.Manifest | None:
|
|
402
|
+
return manifest.load_for_setup(root)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _contained_destination(root: Path, requested: str, *, label: str) -> Path:
|
|
406
|
+
candidate = Path(requested)
|
|
407
|
+
destination = (candidate if candidate.is_absolute() else root / candidate).resolve()
|
|
408
|
+
try:
|
|
409
|
+
destination.relative_to(root)
|
|
410
|
+
except ValueError as exc:
|
|
411
|
+
msg = f"{label} destination {destination} escapes repository root {root}"
|
|
412
|
+
raise ValueError(msg) from exc
|
|
413
|
+
if not destination.is_dir():
|
|
414
|
+
msg = f"{label} destination {destination} is not a directory"
|
|
415
|
+
raise ValueError(msg)
|
|
416
|
+
return destination
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _sync_one(target: SyncTarget, *, root: Path, force: bool, check: bool) -> SyncOutcome:
|
|
420
|
+
destination = target.destination
|
|
421
|
+
if is_link_like(destination):
|
|
422
|
+
if not check:
|
|
423
|
+
return SyncOutcome.INVALID
|
|
424
|
+
try:
|
|
425
|
+
resolved = destination.resolve(strict=True)
|
|
426
|
+
resolved.relative_to(root)
|
|
427
|
+
except OSError, ValueError:
|
|
428
|
+
return SyncOutcome.INVALID
|
|
429
|
+
if not resolved.is_file() or resolved.read_bytes() != target.source.read_bytes():
|
|
430
|
+
return SyncOutcome.DRIFT
|
|
431
|
+
return SyncOutcome.OK
|
|
432
|
+
if destination.exists() and not destination.is_file():
|
|
433
|
+
return SyncOutcome.INVALID
|
|
434
|
+
if destination.is_file() and destination.read_bytes() == target.source.read_bytes():
|
|
435
|
+
return SyncOutcome.OK
|
|
436
|
+
if check:
|
|
437
|
+
return SyncOutcome.DRIFT
|
|
438
|
+
if destination.exists() and not force:
|
|
439
|
+
return SyncOutcome.SKIPPED
|
|
440
|
+
transaction.atomic_write_bytes(root, destination, target.source.read_bytes())
|
|
441
|
+
return SyncOutcome.WRITTEN
|