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.
Files changed (99) hide show
  1. code_standards-7.0.0.dist-info/METADATA +53 -0
  2. code_standards-7.0.0.dist-info/RECORD +99 -0
  3. code_standards-7.0.0.dist-info/WHEEL +4 -0
  4. code_standards-7.0.0.dist-info/entry_points.txt +3 -0
  5. code_standards-7.0.0.dist-info/licenses/LICENSE +21 -0
  6. sarj_standards/__init__.py +30 -0
  7. sarj_standards/__main__.py +5 -0
  8. sarj_standards/_meta.py +22 -0
  9. sarj_standards/api.py +890 -0
  10. sarj_standards/cli/__init__.py +0 -0
  11. sarj_standards/cli/main.py +2466 -0
  12. sarj_standards/configs/cli-reference.v1.json +1 -0
  13. sarj_standards/configs/doctor.config.json +22 -0
  14. sarj_standards/configs/eslint.application.mjs +1366 -0
  15. sarj_standards/configs/eslint.peers.json +44 -0
  16. sarj_standards/configs/eslint.strict.mjs +1060 -0
  17. sarj_standards/configs/markdownlint.strict.yaml +12 -0
  18. sarj_standards/configs/pyright.strict.json +96 -0
  19. sarj_standards/configs/ruff.application.toml +363 -0
  20. sarj_standards/configs/ruff.strict.toml +338 -0
  21. sarj_standards/configs/rule-inventory.v1.json +1 -0
  22. sarj_standards/configs/rule-ledger.json +846 -0
  23. sarj_standards/configs/rule-warning-levels.v1.json +1 -0
  24. sarj_standards/configs/taplo.strict.toml +14 -0
  25. sarj_standards/configs/yamllint.strict.yaml +25 -0
  26. sarj_standards/libs/__init__.py +0 -0
  27. sarj_standards/libs/adoption/__init__.py +0 -0
  28. sarj_standards/libs/adoption/configs.py +36 -0
  29. sarj_standards/libs/adoption/doctor.py +1346 -0
  30. sarj_standards/libs/adoption/exclusions.py +66 -0
  31. sarj_standards/libs/adoption/hooks.py +423 -0
  32. sarj_standards/libs/adoption/launcher.py +240 -0
  33. sarj_standards/libs/adoption/lifecycle.py +493 -0
  34. sarj_standards/libs/adoption/manifest.py +550 -0
  35. sarj_standards/libs/adoption/packagemanager.py +285 -0
  36. sarj_standards/libs/adoption/retired_suppressions.py +371 -0
  37. sarj_standards/libs/adoption/scaffold.py +1660 -0
  38. sarj_standards/libs/adoption/service.py +441 -0
  39. sarj_standards/libs/adoption/transaction.py +274 -0
  40. sarj_standards/libs/adoption/upgrade.py +516 -0
  41. sarj_standards/libs/adoption/uvtool.py +62 -0
  42. sarj_standards/libs/catalogs/__init__.py +9 -0
  43. sarj_standards/libs/catalogs/slack_automations.py +627 -0
  44. sarj_standards/libs/corpus/__init__.py +25 -0
  45. sarj_standards/libs/corpus/manifest.py +211 -0
  46. sarj_standards/libs/corpus/snapshot.py +222 -0
  47. sarj_standards/libs/diagnostics/__init__.py +65 -0
  48. sarj_standards/libs/diagnostics/analysis.schema.json +161 -0
  49. sarj_standards/libs/diagnostics/baseline.py +131 -0
  50. sarj_standards/libs/diagnostics/models.py +574 -0
  51. sarj_standards/libs/diagnostics/serialize.py +290 -0
  52. sarj_standards/libs/diagnostics/source.py +172 -0
  53. sarj_standards/libs/filesystem.py +11 -0
  54. sarj_standards/libs/linting/__init__.py +0 -0
  55. sarj_standards/libs/linting/analysis.py +422 -0
  56. sarj_standards/libs/linting/external.py +1454 -0
  57. sarj_standards/libs/linting/library_policy.py +688 -0
  58. sarj_standards/libs/linting/policy.py +152 -0
  59. sarj_standards/libs/linting/runner.py +442 -0
  60. sarj_standards/libs/linting/textlint.py +1605 -0
  61. sarj_standards/libs/release/__init__.py +98 -0
  62. sarj_standards/libs/release/_values.py +24 -0
  63. sarj_standards/libs/release/artifacts.py +191 -0
  64. sarj_standards/libs/release/causality.py +80 -0
  65. sarj_standards/libs/release/changes.py +48 -0
  66. sarj_standards/libs/release/process.py +128 -0
  67. sarj_standards/libs/release/publish.py +85 -0
  68. sarj_standards/libs/release/registry.py +271 -0
  69. sarj_standards/libs/release/release_age.py +218 -0
  70. sarj_standards/libs/release/rollout.py +1163 -0
  71. sarj_standards/libs/release/tags.py +373 -0
  72. sarj_standards/libs/release/typescript.py +191 -0
  73. sarj_standards/libs/repository/__init__.py +0 -0
  74. sarj_standards/libs/repository/cli_reference_artifact.py +324 -0
  75. sarj_standards/libs/repository/comment_corpus.py +536 -0
  76. sarj_standards/libs/repository/config_generation.py +146 -0
  77. sarj_standards/libs/repository/docs.py +347 -0
  78. sarj_standards/libs/repository/hooks.py +118 -0
  79. sarj_standards/libs/repository/ledger.py +99 -0
  80. sarj_standards/libs/repository/repository.py +744 -0
  81. sarj_standards/libs/repository/rule_authoring.py +246 -0
  82. sarj_standards/libs/repository/rule_catalog_artifact.py +479 -0
  83. sarj_standards/libs/repository/rule_changes.py +318 -0
  84. sarj_standards/libs/repository/rule_inventory_artifact.py +142 -0
  85. sarj_standards/libs/repository/rule_lifecycle.py +167 -0
  86. sarj_standards/libs/repository/rule_maintenance.py +225 -0
  87. sarj_standards/libs/rules/__init__.py +74 -0
  88. sarj_standards/libs/rules/catalog.py +145 -0
  89. sarj_standards/libs/rules/contracts.py +382 -0
  90. sarj_standards/libs/rules/corpus_runner.py +365 -0
  91. sarj_standards/libs/rules/evaluation.py +177 -0
  92. sarj_standards/libs/setup/__init__.py +4 -0
  93. sarj_standards/libs/setup/repository.py +40 -0
  94. sarj_standards/py.typed +0 -0
  95. sarj_standards/schemas/__init__.py +4 -0
  96. sarj_standards/schemas/_paths.py +7 -0
  97. sarj_standards/schemas/rule-catalog.v1.json +1 -0
  98. sarj_standards/schemas/rule-catalog.v1.schema.json +112 -0
  99. 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