lean-runtime 2.0.1__tar.gz → 2.0.2__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 (111) hide show
  1. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/CHANGELOG.md +9 -0
  2. {lean_runtime-2.0.1/lean_runtime.egg-info → lean_runtime-2.0.2}/PKG-INFO +1 -1
  3. lean_runtime-2.0.2/lean_runtime/_paths.py +38 -0
  4. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/bundles.py +2 -1
  5. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/cli.py +14 -1
  6. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/environments.py +7 -4
  7. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/programs.py +4 -4
  8. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/store.py +3 -2
  9. {lean_runtime-2.0.1 → lean_runtime-2.0.2/lean_runtime.egg-info}/PKG-INFO +1 -1
  10. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime.egg-info/SOURCES.txt +2 -0
  11. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/pyproject.toml +1 -1
  12. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_cli.py +22 -1
  13. lean_runtime-2.0.2/tests/test_paths.py +25 -0
  14. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_store.py +4 -2
  15. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/LICENSE +0 -0
  16. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/MANIFEST.in +0 -0
  17. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/README.md +0 -0
  18. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/compatibility/README.md +0 -0
  19. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/compatibility/mathlib-4.32.2.json +0 -0
  20. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/compatibility/mathlib-4.32.2.toml +0 -0
  21. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/architecture.md +0 -0
  22. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/captures.md +0 -0
  23. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/case-study-v1.md +0 -0
  24. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/cli.md +0 -0
  25. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/development.md +0 -0
  26. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/environments.md +0 -0
  27. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/getting-started.md +0 -0
  28. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/index.md +0 -0
  29. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/local-projects.md +0 -0
  30. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/portable-copies.md +0 -0
  31. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/python-api.md +0 -0
  32. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/ready-programs.md +0 -0
  33. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/standalone-files.md +0 -0
  34. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/trust-and-limitations.md +0 -0
  35. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/docs/v1-precision.md +0 -0
  36. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/examples/mathlib.toml +0 -0
  37. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/__init__.py +0 -0
  38. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/__main__.py +0 -0
  39. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/_git.py +0 -0
  40. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/backends.py +0 -0
  41. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/comparison.py +0 -0
  42. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/decisions.py +0 -0
  43. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/diagnostics.py +0 -0
  44. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/errors.py +0 -0
  45. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/events.py +0 -0
  46. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/facade.py +0 -0
  47. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/frontmatter.py +0 -0
  48. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/health.py +0 -0
  49. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/lake.py +0 -0
  50. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/lockfiles.py +0 -0
  51. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/locking.py +0 -0
  52. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/matrix.py +0 -0
  53. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/models.py +0 -0
  54. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/oci.py +0 -0
  55. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/policies.py +0 -0
  56. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/profiling.py +0 -0
  57. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/projects.py +0 -0
  58. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/publisher_verification.py +0 -0
  59. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/py.typed +0 -0
  60. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/references.py +0 -0
  61. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/resolver.py +0 -0
  62. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/run_cli.py +0 -0
  63. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/runtime.py +0 -0
  64. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/schema_resources.py +0 -0
  65. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/serialization.py +0 -0
  66. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/specs.py +0 -0
  67. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/timings.py +0 -0
  68. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/toolchains.py +0 -0
  69. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/verification.py +0 -0
  70. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime/wire.py +0 -0
  71. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime.egg-info/dependency_links.txt +0 -0
  72. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime.egg-info/entry_points.txt +0 -0
  73. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime.egg-info/requires.txt +0 -0
  74. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/lean_runtime.egg-info/top_level.txt +0 -0
  75. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/mkdocs.yml +0 -0
  76. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/schemas/cleanup-v1.schema.json +0 -0
  77. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/schemas/comparison-v1.schema.json +0 -0
  78. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/schemas/execution-v1.schema.json +0 -0
  79. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/schemas/inspect-v1.schema.json +0 -0
  80. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/schemas/matrix-v1.schema.json +0 -0
  81. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/schemas/profile-v1.schema.json +0 -0
  82. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/schemas/verify-v1.schema.json +0 -0
  83. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/scripts/run_compatibility.py +0 -0
  84. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/scripts/run_v1_case_study.py +0 -0
  85. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/scripts/smoke_wheel.py +0 -0
  86. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/setup.cfg +0 -0
  87. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/conftest.py +0 -0
  88. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_backend.py +0 -0
  89. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_bundles.py +0 -0
  90. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_diagnostics.py +0 -0
  91. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_environment_integration.py +0 -0
  92. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_events_health.py +0 -0
  93. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_facade.py +0 -0
  94. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_frontmatter.py +0 -0
  95. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_git.py +0 -0
  96. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_interactive.py +0 -0
  97. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_lockfiles.py +0 -0
  98. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_multifile.py +0 -0
  99. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_prebuilt_policy.py +0 -0
  100. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_programs.py +0 -0
  101. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_projects.py +0 -0
  102. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_references.py +0 -0
  103. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_run_cli.py +0 -0
  104. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_runtime.py +0 -0
  105. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_schema_resources.py +0 -0
  106. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_schemas.py +0 -0
  107. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_signatures.py +0 -0
  108. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_specs.py +0 -0
  109. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_toolchains.py +0 -0
  110. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_v1_precision.py +0 -0
  111. {lean_runtime-2.0.1 → lean_runtime-2.0.2}/tests/test_verification_inventory.py +0 -0
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.2
4
+
5
+ - Remove staged environments safely when Git-for-Windows leaves source pack
6
+ files read-only or briefly locked.
7
+ - Preserve the primary environment-build diagnostic when staging cleanup also
8
+ fails.
9
+ - Show the failed phase, command, exit code, and captured tool output when a
10
+ verbose CLI materialization fails.
11
+
3
12
  ## 2.0.1
4
13
 
5
14
  - Enable Git's long-path support for every Runtime-managed source operation so
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1,38 @@
1
+ """Internal cross-platform filesystem operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import shutil
7
+ import stat
8
+ import time
9
+ from collections.abc import Callable
10
+ from pathlib import Path
11
+ from types import TracebackType
12
+ from typing import Any
13
+
14
+
15
+ def _retry_writable(
16
+ function: Callable[..., Any],
17
+ path: str,
18
+ _error: tuple[type[BaseException], BaseException, TracebackType | None],
19
+ ) -> None:
20
+ """Clear Git-for-Windows read-only attributes and retry one removal."""
21
+
22
+ os.chmod(path, stat.S_IWRITE)
23
+ function(path)
24
+
25
+
26
+ def remove_tree(path: Path) -> None:
27
+ """Remove a tree containing Git packs, tolerating transient Windows locks."""
28
+
29
+ for attempt in range(4):
30
+ try:
31
+ shutil.rmtree(path, onerror=_retry_writable)
32
+ return
33
+ except PermissionError:
34
+ if not path.exists():
35
+ return
36
+ if attempt == 3:
37
+ raise
38
+ time.sleep(0.05 * (2**attempt))
@@ -20,6 +20,7 @@ from pathlib import Path, PurePosixPath
20
20
  from typing import Any
21
21
 
22
22
  from ._git import git_command
23
+ from ._paths import remove_tree
23
24
  from .backends import Backend
24
25
  from .errors import EnvironmentError
25
26
  from .events import EventEmitter
@@ -667,7 +668,7 @@ class EnvironmentBundles:
667
668
  stage.replace(destination)
668
669
  finally:
669
670
  if stage.exists():
670
- shutil.rmtree(stage)
671
+ remove_tree(stage)
671
672
  if name:
672
673
  self.store.set_alias(name, environment_id)
673
674
  self.events.emit(
@@ -50,6 +50,19 @@ def _progress(event: RuntimeEvent) -> None:
50
50
  print(f"lean-runtime: {event.kind}{package}: {event.message}", file=sys.stderr)
51
51
 
52
52
 
53
+ def _print_operation_failure(exc: ResolutionError | MaterializationError, *, verbose: bool) -> None:
54
+ print(f"lean-runtime: {exc}", file=sys.stderr)
55
+ if not verbose:
56
+ return
57
+ print(f"phase: {exc.phase}", file=sys.stderr)
58
+ if exc.command:
59
+ print("command: " + " ".join(exc.command), file=sys.stderr)
60
+ if exc.exit_code is not None:
61
+ print(f"exit code: {exc.exit_code}", file=sys.stderr)
62
+ if exc.output:
63
+ print(exc.output.rstrip(), file=sys.stderr)
64
+
65
+
53
66
  def _cli_source_name(path: Path) -> str:
54
67
  if path.is_absolute():
55
68
  return path.name
@@ -643,7 +656,7 @@ def main(argv: list[str] | None = None) -> int:
643
656
  )
644
657
  )
645
658
  else:
646
- print(f"lean-runtime: {exc}", file=sys.stderr)
659
+ _print_operation_failure(exc, verbose=args.verbose)
647
660
  return 2
648
661
  except (LeanRuntimeError, OSError, UnicodeError, ValueError, json.JSONDecodeError) as exc:
649
662
  if getattr(args, "json", False):
@@ -6,7 +6,6 @@ import asyncio
6
6
  import json
7
7
  import os
8
8
  import re
9
- import shutil
10
9
  import subprocess
11
10
  import tempfile
12
11
  import threading
@@ -21,6 +20,7 @@ from pathlib import Path, PurePosixPath
21
20
  from typing import Any, Generic, TextIO, TypeVar, cast
22
21
 
23
22
  from ._git import git_command
23
+ from ._paths import remove_tree
24
24
  from .backends import Backend, BackendResult, InteractiveProcess, InteractiveTextReader
25
25
  from .diagnostics import error_diagnostic, parse_diagnostics
26
26
  from .errors import EnvironmentError, MaterializationError, PolicyError
@@ -559,7 +559,7 @@ class Environment:
559
559
 
560
560
  def cleanup() -> None:
561
561
  if instance.exists():
562
- shutil.rmtree(instance)
562
+ remove_tree(instance)
563
563
  with suppress(OSError):
564
564
  job_parent.rmdir()
565
565
 
@@ -847,7 +847,7 @@ class Environment:
847
847
  return result
848
848
  finally:
849
849
  if instance.exists():
850
- shutil.rmtree(instance)
850
+ remove_tree(instance)
851
851
  with suppress(OSError):
852
852
  job_parent.rmdir()
853
853
 
@@ -1107,7 +1107,10 @@ class EnvironmentManager:
1107
1107
  )
1108
1108
  finally:
1109
1109
  if stage.exists():
1110
- shutil.rmtree(stage)
1110
+ # Preserve a build/materialization diagnostic if cleanup of
1111
+ # Git's read-only pack files also fails on Windows.
1112
+ with suppress(OSError):
1113
+ remove_tree(stage)
1111
1114
 
1112
1115
  def _ensure_sources(self, lock: EnvironmentLock) -> None:
1113
1116
  """Acquire exact locked sources without invoking Lake resolution."""
@@ -10,7 +10,6 @@ from __future__ import annotations
10
10
  import hashlib
11
11
  import os
12
12
  import re
13
- import shutil
14
13
  import tempfile
15
14
  import uuid
16
15
  from collections.abc import Mapping, Sequence
@@ -20,6 +19,7 @@ from datetime import datetime, timezone
20
19
  from pathlib import Path, PurePosixPath
21
20
  from typing import Any, cast
22
21
 
22
+ from ._paths import remove_tree
23
23
  from .backends import Backend, BackendResult, InteractiveProcess
24
24
  from .bundles import (
25
25
  INDEX_MEDIA_TYPE,
@@ -238,7 +238,7 @@ class ReadyProgram:
238
238
 
239
239
  def cleanup() -> None:
240
240
  if instance.exists():
241
- shutil.rmtree(instance)
241
+ remove_tree(instance)
242
242
  with suppress(OSError):
243
243
  job_parent.rmdir()
244
244
 
@@ -347,7 +347,7 @@ class ProgramManager:
347
347
  stage.replace(destination)
348
348
  finally:
349
349
  if stage.exists():
350
- shutil.rmtree(stage)
350
+ remove_tree(stage)
351
351
  self.events.emit(
352
352
  "program.created", "Created executable program", program_id=manifest.program_id
353
353
  )
@@ -534,7 +534,7 @@ class ProgramManager:
534
534
  stage.replace(destination)
535
535
  finally:
536
536
  if stage.exists():
537
- shutil.rmtree(stage)
537
+ remove_tree(stage)
538
538
  return self.open(manifest.program_id)
539
539
 
540
540
 
@@ -18,6 +18,7 @@ from pathlib import Path
18
18
  from typing import Any
19
19
 
20
20
  from ._git import git_command
21
+ from ._paths import remove_tree
21
22
  from .errors import EnvironmentError
22
23
  from .lockfiles import EnvironmentLock
23
24
  from .locking import FileLock
@@ -348,7 +349,7 @@ class EnvironmentStore:
348
349
  # Failed Git checkouts can leave locked pack files on
349
350
  # Windows. Cleanup must not mask the actionable Git error.
350
351
  with suppress(OSError):
351
- shutil.rmtree(stage)
352
+ remove_tree(stage)
352
353
  return destination
353
354
 
354
355
  def environment_path(self, environment_id: str) -> Path:
@@ -556,7 +557,7 @@ class EnvironmentStore:
556
557
  ):
557
558
  retained.append(path.name)
558
559
  continue
559
- shutil.rmtree(path)
560
+ remove_tree(path)
560
561
  usage.unlink(missing_ok=True)
561
562
  removed.append(path.name)
562
563
  return CleanupReport(tuple(candidates), tuple(removed), tuple(retained), dry_run)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -26,6 +26,7 @@ examples/mathlib.toml
26
26
  lean_runtime/__init__.py
27
27
  lean_runtime/__main__.py
28
28
  lean_runtime/_git.py
29
+ lean_runtime/_paths.py
29
30
  lean_runtime/backends.py
30
31
  lean_runtime/bundles.py
31
32
  lean_runtime/cli.py
@@ -91,6 +92,7 @@ tests/test_git.py
91
92
  tests/test_interactive.py
92
93
  tests/test_lockfiles.py
93
94
  tests/test_multifile.py
95
+ tests/test_paths.py
94
96
  tests/test_prebuilt_policy.py
95
97
  tests/test_programs.py
96
98
  tests/test_projects.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lean-runtime"
7
- version = "2.0.1"
7
+ version = "2.0.2"
8
8
  description = "Run Lean 4 proofs from Python or standalone files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -4,7 +4,8 @@ import json
4
4
  from pathlib import Path
5
5
 
6
6
  from lean_runtime.bundles import PortableCopyInfo
7
- from lean_runtime.cli import main, parser
7
+ from lean_runtime.cli import _print_operation_failure, main, parser
8
+ from lean_runtime.errors import MaterializationError
8
9
  from lean_runtime.models import ExecutionResult
9
10
  from lean_runtime.verification import VerificationCheck, VerificationReport
10
11
 
@@ -34,6 +35,26 @@ def test_removed_v1_commands_are_absent() -> None:
34
35
  assert commands.isdisjoint(removed)
35
36
 
36
37
 
38
+ def test_verbose_materialization_failure_includes_tool_output(capsys) -> None:
39
+ failure = MaterializationError(
40
+ "environment build failed",
41
+ phase="build",
42
+ command=("lake", "build"),
43
+ exit_code=1,
44
+ output="specific compiler failure\n",
45
+ )
46
+
47
+ _print_operation_failure(failure, verbose=True)
48
+
49
+ assert capsys.readouterr().err == (
50
+ "lean-runtime: environment build failed\n"
51
+ "phase: build\n"
52
+ "command: lake build\n"
53
+ "exit code: 1\n"
54
+ "specific compiler failure\n"
55
+ )
56
+
57
+
37
58
  def test_check_file_cli_json_result(monkeypatch, tmp_path: Path, capsys) -> None:
38
59
  source = tmp_path / "Main.lean"
39
60
  source.write_text("example : True := by trivial")
@@ -0,0 +1,25 @@
1
+ from pathlib import Path
2
+
3
+ from lean_runtime._paths import remove_tree
4
+
5
+
6
+ def test_remove_tree_retries_transient_permission_errors(tmp_path: Path, monkeypatch) -> None:
7
+ target = tmp_path / "tree"
8
+ target.mkdir()
9
+ calls = 0
10
+
11
+ def flaky_rmtree(path: Path, *, onerror) -> None:
12
+ nonlocal calls
13
+ calls += 1
14
+ if calls == 1:
15
+ raise PermissionError("Git pack is briefly locked")
16
+ assert onerror is not None
17
+ path.rmdir()
18
+
19
+ monkeypatch.setattr("lean_runtime._paths.shutil.rmtree", flaky_rmtree)
20
+ monkeypatch.setattr("lean_runtime._paths.time.sleep", lambda _seconds: None)
21
+
22
+ remove_tree(target)
23
+
24
+ assert calls == 2
25
+ assert not target.exists()
@@ -217,11 +217,13 @@ def test_source_snapshot_preserves_git_error_when_cleanup_also_fails(
217
217
  Path(command[-1]).mkdir()
218
218
  return subprocess.CompletedProcess(command, 1, "", "Filename too long")
219
219
 
220
- def failed_cleanup(_path: Path) -> None:
220
+ def failed_cleanup(_path: Path, *, onerror) -> None:
221
+ assert onerror is not None
221
222
  raise PermissionError("pack file is locked")
222
223
 
223
224
  monkeypatch.setattr("lean_runtime.store.subprocess.run", failed_clone)
224
- monkeypatch.setattr("lean_runtime.store.shutil.rmtree", failed_cleanup)
225
+ monkeypatch.setattr("lean_runtime._paths.shutil.rmtree", failed_cleanup)
226
+ monkeypatch.setattr("lean_runtime._paths.time.sleep", lambda _seconds: None)
225
227
 
226
228
  with pytest.raises(EnvironmentError, match="Filename too long"):
227
229
  store.publish_source(checkout, source_id, metadata)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes