gitgrip 1.5.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.
- gitgrip-1.5.0.dist-info/METADATA +13 -0
- gitgrip-1.5.0.dist-info/RECORD +80 -0
- gitgrip-1.5.0.dist-info/WHEEL +5 -0
- gitgrip-1.5.0.dist-info/entry_points.txt +2 -0
- gitgrip-1.5.0.dist-info/top_level.txt +2 -0
- gr2/__init__.py +0 -0
- gr2/overlay/__init__.py +6 -0
- gr2/overlay/activate.py +196 -0
- gr2/overlay/agent_manifest.py +138 -0
- gr2/overlay/cli.py +181 -0
- gr2/overlay/cross_repo.py +124 -0
- gr2/overlay/drivers.py +113 -0
- gr2/overlay/introspection.py +155 -0
- gr2/overlay/language_drivers.py +115 -0
- gr2/overlay/objects.py +412 -0
- gr2/overlay/perf.py +251 -0
- gr2/overlay/refs.py +36 -0
- gr2/overlay/trust.py +150 -0
- gr2/overlay/types.py +69 -0
- gr2/overlay/units.py +313 -0
- gr2/overlay/workspace_spec.py +59 -0
- gr2/prototypes/__init__.py +0 -0
- gr2/prototypes/cache_materialization_probe.py +190 -0
- gr2/prototypes/concurrent_event_stress.py +199 -0
- gr2/prototypes/concurrent_lease_stress.py +240 -0
- gr2/prototypes/concurrent_workspace_cap_stress.py +231 -0
- gr2/prototypes/contribution_protocol.py +665 -0
- gr2/prototypes/cross_mode_lane_stress.py +986 -0
- gr2/prototypes/jsonl_store.py +158 -0
- gr2/prototypes/lane_workspace_prototype.py +2088 -0
- gr2/prototypes/layout_model_probe.py +139 -0
- gr2/prototypes/propagation_daemon.py +546 -0
- gr2/prototypes/propagation_state_machine.py +1478 -0
- gr2/prototypes/python_exec_playground.py +194 -0
- gr2/prototypes/python_hook_runtime_playground.py +240 -0
- gr2/prototypes/python_migration_playground.py +144 -0
- gr2/prototypes/python_review_checkout_playground.py +242 -0
- gr2/prototypes/python_spec_apply_playground.py +282 -0
- gr2/prototypes/real_git_lane_materialization.py +248 -0
- gr2/prototypes/real_git_playground.py +334 -0
- gr2/prototypes/recall_lane_history.py +274 -0
- gr2/prototypes/repo_maintenance_prototype.py +659 -0
- gr2/prototypes/repo_transport_probe.py +147 -0
- gr2/python_cli/__init__.py +2 -0
- gr2/python_cli/__main__.py +6 -0
- gr2/python_cli/add.py +51 -0
- gr2/python_cli/app.py +2516 -0
- gr2/python_cli/branch.py +67 -0
- gr2/python_cli/channel_bridge.py +131 -0
- gr2/python_cli/clone_exec.py +1019 -0
- gr2/python_cli/commit.py +199 -0
- gr2/python_cli/config.py +291 -0
- gr2/python_cli/env_exec.py +419 -0
- gr2/python_cli/events.py +529 -0
- gr2/python_cli/execops.py +372 -0
- gr2/python_cli/failures.py +98 -0
- gr2/python_cli/file_exec.py +256 -0
- gr2/python_cli/gitops.py +226 -0
- gr2/python_cli/grip.py +1337 -0
- gr2/python_cli/grip_cli.py +493 -0
- gr2/python_cli/hooks.py +450 -0
- gr2/python_cli/launch_exec.py +786 -0
- gr2/python_cli/merge_verification.py +274 -0
- gr2/python_cli/migration.py +985 -0
- gr2/python_cli/open_gr_review.py +699 -0
- gr2/python_cli/platform.py +441 -0
- gr2/python_cli/pr.py +487 -0
- gr2/python_cli/project_review.py +314 -0
- gr2/python_cli/prune.py +365 -0
- gr2/python_cli/push.py +172 -0
- gr2/python_cli/review.py +462 -0
- gr2/python_cli/review_ephemeral.py +143 -0
- gr2/python_cli/review_run.py +621 -0
- gr2/python_cli/spec_apply.py +1285 -0
- gr2/python_cli/staging_cleanup.py +205 -0
- gr2/python_cli/syncops.py +920 -0
- gr2/python_cli/target.py +100 -0
- gr2/python_cli/workspace_snapshot.py +105 -0
- gr2/schemas/gr2-materialization-plan-v1.schema.json +191 -0
- gr2_overlay/__init__.py +37 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Executors for the `venv` and `editable_install` operations (S4-D).
|
|
2
|
+
|
|
3
|
+
The last two materialization handlers. With these, the A -> B -> C -> D path is
|
|
4
|
+
complete: contract, clones, projections, environments.
|
|
5
|
+
|
|
6
|
+
Contract: MaterializationPlan v1 environment invariant 6 and
|
|
7
|
+
acceptance fruit 10/11/12.
|
|
8
|
+
|
|
9
|
+
Working, not exhaustively hardened (Layne's 2026-07-27 prototype doctrine).
|
|
10
|
+
|
|
11
|
+
INVARIANT 6 IS THE INTERESTING ONE, and it is written as a warning about
|
|
12
|
+
proxies: "Treat an existing venv as valid only when pyvenv.cfg is a regular file
|
|
13
|
+
AND the platform interpreter is present, executable, and succeeds under an
|
|
14
|
+
isolated bounded probe. The probe must report a resolved sys.prefix equal to the
|
|
15
|
+
declared venv path and a distinct sys.base_prefix. FILE PRESENCE ALONE IS NOT
|
|
16
|
+
VENV EVIDENCE."
|
|
17
|
+
|
|
18
|
+
That last sentence is the spec naming the mask itself. `pyvenv.cfg exists` is
|
|
19
|
+
the cheap check that LOOKS like venv validation, and it stays true for a venv
|
|
20
|
+
whose interpreter was deleted, whose base Python was upgraded out from under it,
|
|
21
|
+
or which was copied to a new path (the config records the OLD prefix, so every
|
|
22
|
+
relative path inside it is wrong). Only running the interpreter can tell.
|
|
23
|
+
|
|
24
|
+
The spec's TWO PREFIX ASSERTIONS ARE KEPT BUT CANNOT FIRE on CPython 3.11+, and
|
|
25
|
+
that was established by running it rather than by reading the docs:
|
|
26
|
+
|
|
27
|
+
- pyvenv.cfg next to bin/python is LITERALLY what makes a venv, so base_prefix
|
|
28
|
+
always differs from prefix -- even when the file contains garbage. A "bare
|
|
29
|
+
interpreter plus a pyvenv.cfg" is a real venv by Python's own rules.
|
|
30
|
+
- sys.prefix is derived from the directory you INVOKED THROUGH, not from
|
|
31
|
+
anything recorded inside, so it equals the declared path even when
|
|
32
|
+
bin/python symlinks to a different venv's interpreter -- and a COPIED venv
|
|
33
|
+
is self-consistent at its new location.
|
|
34
|
+
|
|
35
|
+
They stay because the spec mandates them and a future Python may behave
|
|
36
|
+
differently; they are defence in depth, not live guards, and saying so beats
|
|
37
|
+
leaving them looking like protection. test_the_prefix_assertions_cannot_fire_on_
|
|
38
|
+
current_cpython pins the empirical finding and FAILS if CPython ever changes,
|
|
39
|
+
which is the signal that they have become reachable and deserve real probes.
|
|
40
|
+
|
|
41
|
+
What the probe genuinely catches today is the reachable set: an interpreter that
|
|
42
|
+
is missing, not executable, failing, or hanging, and a missing pyvenv.cfg. Those
|
|
43
|
+
are the real content of "file presence alone is not venv evidence".
|
|
44
|
+
|
|
45
|
+
FRUIT 11's `python -I` IS LOAD-BEARING, not decoration. Without isolation, an
|
|
46
|
+
import can be satisfied by the current working directory or an inherited
|
|
47
|
+
PYTHONPATH, so a passing import proves nothing about what the venv installed.
|
|
48
|
+
`-I` ignores both, which is what makes "resolves inside that unit clone" a real
|
|
49
|
+
claim.
|
|
50
|
+
"""
|
|
51
|
+
from __future__ import annotations
|
|
52
|
+
|
|
53
|
+
import dataclasses
|
|
54
|
+
import hashlib
|
|
55
|
+
import json
|
|
56
|
+
import os
|
|
57
|
+
import subprocess
|
|
58
|
+
from pathlib import Path
|
|
59
|
+
|
|
60
|
+
from .spec_apply import (
|
|
61
|
+
MaterializationPlanError,
|
|
62
|
+
ValidatedPlan,
|
|
63
|
+
_read_canonical_workspace_spec_bytes,
|
|
64
|
+
canonicalize_workspace_path,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# A venv probe that hangs would hang the timed path, and section 13 gives the
|
|
68
|
+
# whole materialization 180s. Bounded, per invariant 6's "bounded probe".
|
|
69
|
+
_PROBE_TIMEOUT_SECONDS = 30
|
|
70
|
+
_INSTALL_TIMEOUT_SECONDS = 300
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class EnvExecutionError(MaterializationPlanError):
|
|
74
|
+
"""A validated plan's venv or editable_install operation could not run."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclasses.dataclass(frozen=True)
|
|
78
|
+
class _VenvBinding:
|
|
79
|
+
dest_path: str
|
|
80
|
+
engine: str
|
|
81
|
+
python: str
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclasses.dataclass(frozen=True)
|
|
85
|
+
class _EditableBinding:
|
|
86
|
+
venv_path: str
|
|
87
|
+
source_path: str
|
|
88
|
+
extras: tuple[str, ...]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def venv_interpreter(venv_root: Path) -> Path:
|
|
92
|
+
"""Platform interpreter inside a venv. Windows puts it in Scripts/."""
|
|
93
|
+
if os.name == "nt":
|
|
94
|
+
return venv_root / "Scripts" / "python.exe"
|
|
95
|
+
return venv_root / "bin" / "python"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _require_workspace_binding(validated: ValidatedPlan, workspace_root: Path) -> None:
|
|
99
|
+
"""Re-prove at USE what validation proved earlier -- see S4-B/S4-C. Local
|
|
100
|
+
rather than imported so the error names THIS layer; the shared executor
|
|
101
|
+
scaffolding wants extracting into one module, deferred."""
|
|
102
|
+
try:
|
|
103
|
+
spec_bytes = _read_canonical_workspace_spec_bytes(workspace_root)
|
|
104
|
+
except MaterializationPlanError as exc:
|
|
105
|
+
raise EnvExecutionError(
|
|
106
|
+
f"cannot execute against {workspace_root}: its canonical WorkspaceSpec is "
|
|
107
|
+
f"unreadable ({exc})"
|
|
108
|
+
) from exc
|
|
109
|
+
if hashlib.sha256(spec_bytes).hexdigest() != validated.workspace_spec_sha256:
|
|
110
|
+
raise EnvExecutionError(
|
|
111
|
+
f"plan is bound to WorkspaceSpec {validated.workspace_spec_sha256} but "
|
|
112
|
+
f"{workspace_root} has a different one -- executing a plan against a "
|
|
113
|
+
"workspace it was not validated for would resolve every path elsewhere"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _operation_at(
|
|
118
|
+
validated: ValidatedPlan, index: int, expected_kind: str
|
|
119
|
+
) -> dict[str, object]:
|
|
120
|
+
operations = validated.plan["operations"]
|
|
121
|
+
if not 0 <= index < len(operations):
|
|
122
|
+
raise EnvExecutionError(
|
|
123
|
+
f"operation index {index} is out of range for a plan with "
|
|
124
|
+
f"{len(operations)} operation(s)"
|
|
125
|
+
)
|
|
126
|
+
op = operations[index]
|
|
127
|
+
kind = op.get("kind")
|
|
128
|
+
if kind != expected_kind:
|
|
129
|
+
raise EnvExecutionError(
|
|
130
|
+
f"operations[{index}] is kind {kind!r}, not {expected_kind!r} -- evidence "
|
|
131
|
+
"must correspond to its operation, in order"
|
|
132
|
+
)
|
|
133
|
+
return op
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def probe_venv(venv_root: Path) -> dict[str, str]:
|
|
137
|
+
"""Run the venv's own interpreter and ask it where it thinks it lives.
|
|
138
|
+
|
|
139
|
+
Invariant 6's bounded probe. `-I` isolates: no site-packages from the user
|
|
140
|
+
base, no inherited PYTHONPATH, no cwd on sys.path -- so the answer describes
|
|
141
|
+
the venv rather than the environment this process happens to be running in.
|
|
142
|
+
|
|
143
|
+
Raises rather than returning a sentinel: every caller treats a probe failure
|
|
144
|
+
as 'this is not a usable venv', and a sentinel invites a caller that forgets
|
|
145
|
+
to check it."""
|
|
146
|
+
interpreter = venv_interpreter(venv_root)
|
|
147
|
+
if not interpreter.exists():
|
|
148
|
+
raise EnvExecutionError(
|
|
149
|
+
f"venv at {venv_root} has no interpreter at {interpreter} -- pyvenv.cfg "
|
|
150
|
+
"presence alone is not venv evidence (invariant 6)"
|
|
151
|
+
)
|
|
152
|
+
if not os.access(interpreter, os.X_OK):
|
|
153
|
+
raise EnvExecutionError(
|
|
154
|
+
f"venv interpreter {interpreter} is not executable"
|
|
155
|
+
)
|
|
156
|
+
script = (
|
|
157
|
+
"import json,sys;"
|
|
158
|
+
"print(json.dumps({'prefix':sys.prefix,'base_prefix':sys.base_prefix,"
|
|
159
|
+
"'version':'%d.%d'%sys.version_info[:2]}))"
|
|
160
|
+
)
|
|
161
|
+
try:
|
|
162
|
+
proc = subprocess.run(
|
|
163
|
+
[str(interpreter), "-I", "-c", script],
|
|
164
|
+
capture_output=True,
|
|
165
|
+
text=True,
|
|
166
|
+
timeout=_PROBE_TIMEOUT_SECONDS,
|
|
167
|
+
check=False,
|
|
168
|
+
)
|
|
169
|
+
except subprocess.TimeoutExpired as exc:
|
|
170
|
+
raise EnvExecutionError(
|
|
171
|
+
f"venv interpreter {interpreter} did not answer within "
|
|
172
|
+
f"{_PROBE_TIMEOUT_SECONDS}s -- a hung probe would hang the timed path"
|
|
173
|
+
) from exc
|
|
174
|
+
if proc.returncode != 0:
|
|
175
|
+
raise EnvExecutionError(
|
|
176
|
+
f"venv interpreter {interpreter} failed to run: "
|
|
177
|
+
f"{proc.stderr.strip() or proc.stdout.strip()}"
|
|
178
|
+
)
|
|
179
|
+
try:
|
|
180
|
+
return json.loads(proc.stdout)
|
|
181
|
+
except json.JSONDecodeError as exc:
|
|
182
|
+
raise EnvExecutionError(
|
|
183
|
+
f"venv probe returned unparseable output: {proc.stdout!r}"
|
|
184
|
+
) from exc
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def verify_venv(venv_root: Path) -> dict[str, str]:
|
|
188
|
+
"""Invariant 6 in full. Returns the probe result for receipt evidence."""
|
|
189
|
+
cfg = venv_root / "pyvenv.cfg"
|
|
190
|
+
if not cfg.is_file():
|
|
191
|
+
raise EnvExecutionError(
|
|
192
|
+
f"venv at {venv_root} has no pyvenv.cfg regular file"
|
|
193
|
+
)
|
|
194
|
+
probe = probe_venv(venv_root)
|
|
195
|
+
|
|
196
|
+
# Orthogonal to the prefix check below: a bare system interpreter satisfies
|
|
197
|
+
# 'prefix == declared path' only by accident, but ALWAYS has
|
|
198
|
+
# base_prefix == prefix. This is what distinguishes a venv from a Python.
|
|
199
|
+
if probe["base_prefix"] == probe["prefix"]:
|
|
200
|
+
raise EnvExecutionError(
|
|
201
|
+
f"{venv_root} is not a virtual environment: its interpreter reports "
|
|
202
|
+
f"base_prefix == prefix ({probe['prefix']})"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# And this is what distinguishes THIS venv from one that was copied or moved
|
|
206
|
+
# here -- pyvenv.cfg would still parse, the interpreter would still run, and
|
|
207
|
+
# every relative path inside it would point at the old location.
|
|
208
|
+
if Path(probe["prefix"]).resolve() != venv_root.resolve():
|
|
209
|
+
raise EnvExecutionError(
|
|
210
|
+
f"venv at {venv_root} reports sys.prefix {probe['prefix']} -- it was "
|
|
211
|
+
"created for a different path and its internal paths are stale"
|
|
212
|
+
)
|
|
213
|
+
return probe
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def execute_venv_operation(
|
|
217
|
+
validated: ValidatedPlan, index: int, *, workspace_root: Path
|
|
218
|
+
) -> dict[str, object]:
|
|
219
|
+
"""Create (or accept) the unit venv at `dest_path` using `uv`."""
|
|
220
|
+
workspace_root = Path(os.fspath(workspace_root))
|
|
221
|
+
validated.verify(require_provenance=True)
|
|
222
|
+
_require_workspace_binding(validated, workspace_root)
|
|
223
|
+
|
|
224
|
+
op = _operation_at(validated, index, "venv")
|
|
225
|
+
binding = _VenvBinding(
|
|
226
|
+
dest_path=str(op["dest_path"]),
|
|
227
|
+
engine=str(op["engine"]),
|
|
228
|
+
python=str(op["python"]),
|
|
229
|
+
)
|
|
230
|
+
dest = canonicalize_workspace_path(
|
|
231
|
+
workspace_root, binding.dest_path, field_name=f"operations[{index}].dest_path"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
reused = False
|
|
235
|
+
if dest.exists():
|
|
236
|
+
# Validate every time, not only on creation: invariant 5's rule for
|
|
237
|
+
# clones applies here too -- path absence must not be the condition that
|
|
238
|
+
# makes validation reachable.
|
|
239
|
+
probe = verify_venv(dest)
|
|
240
|
+
reused = True
|
|
241
|
+
else:
|
|
242
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
243
|
+
proc = subprocess.run(
|
|
244
|
+
["uv", "venv", "--python", binding.python, str(dest)],
|
|
245
|
+
capture_output=True,
|
|
246
|
+
text=True,
|
|
247
|
+
timeout=_INSTALL_TIMEOUT_SECONDS,
|
|
248
|
+
check=False,
|
|
249
|
+
)
|
|
250
|
+
if proc.returncode != 0:
|
|
251
|
+
raise EnvExecutionError(
|
|
252
|
+
f"uv venv failed for {dest} with python {binding.python!r}:\n"
|
|
253
|
+
f"{proc.stderr.strip() or proc.stdout.strip()}"
|
|
254
|
+
)
|
|
255
|
+
probe = verify_venv(dest)
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
"kind": "venv",
|
|
259
|
+
"dest_path": binding.dest_path,
|
|
260
|
+
"engine": binding.engine,
|
|
261
|
+
"python": binding.python,
|
|
262
|
+
"interpreter_version": probe["version"],
|
|
263
|
+
"reused": reused,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _distribution_name(source: Path) -> str | None:
|
|
268
|
+
"""Best-effort project name from pyproject.toml, for locating PEP 610
|
|
269
|
+
metadata. Prototype-scoped: a project whose name is set dynamically is not
|
|
270
|
+
handled, and the caller falls back to scanning."""
|
|
271
|
+
pyproject = source / "pyproject.toml"
|
|
272
|
+
if not pyproject.is_file():
|
|
273
|
+
return None
|
|
274
|
+
try:
|
|
275
|
+
import tomllib
|
|
276
|
+
except ModuleNotFoundError: # pragma: no cover - 3.10 and older
|
|
277
|
+
return None
|
|
278
|
+
try:
|
|
279
|
+
data = tomllib.loads(pyproject.read_text())
|
|
280
|
+
except Exception:
|
|
281
|
+
return None
|
|
282
|
+
name = data.get("project", {}).get("name")
|
|
283
|
+
return str(name) if name else None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def read_direct_url(venv_root: Path, source: Path) -> dict[str, object]:
|
|
287
|
+
"""PEP 610 metadata for the distribution installed from `source`.
|
|
288
|
+
|
|
289
|
+
Fruit 12 wants editable mode AND the correct source recorded. Located by
|
|
290
|
+
scanning `.dist-info/direct_url.json` and matching the recorded url back to
|
|
291
|
+
the source, rather than trusting the first one found -- a unit installs
|
|
292
|
+
several editables and 'some direct_url.json exists' proves nothing about
|
|
293
|
+
the one being asked about."""
|
|
294
|
+
probe = probe_venv(venv_root)
|
|
295
|
+
version = probe["version"]
|
|
296
|
+
site_packages = venv_root / "lib" / f"python{version}" / "site-packages"
|
|
297
|
+
if os.name == "nt":
|
|
298
|
+
site_packages = venv_root / "Lib" / "site-packages"
|
|
299
|
+
if not site_packages.is_dir():
|
|
300
|
+
raise EnvExecutionError(f"venv at {venv_root} has no site-packages directory")
|
|
301
|
+
|
|
302
|
+
wanted = source.resolve()
|
|
303
|
+
for dist_info in sorted(site_packages.glob("*.dist-info")):
|
|
304
|
+
direct_url = dist_info / "direct_url.json"
|
|
305
|
+
if not direct_url.is_file():
|
|
306
|
+
continue
|
|
307
|
+
try:
|
|
308
|
+
data = json.loads(direct_url.read_text())
|
|
309
|
+
except json.JSONDecodeError:
|
|
310
|
+
continue
|
|
311
|
+
url = str(data.get("url", ""))
|
|
312
|
+
if not url.startswith("file://"):
|
|
313
|
+
continue
|
|
314
|
+
# Percent-decoded: a source path containing a space arrives as %20, so a
|
|
315
|
+
# raw string comparison silently fails to match the very install it is
|
|
316
|
+
# looking for.
|
|
317
|
+
from urllib.parse import unquote, urlparse
|
|
318
|
+
|
|
319
|
+
recorded = Path(unquote(urlparse(url).path)).resolve()
|
|
320
|
+
if recorded == wanted:
|
|
321
|
+
return data
|
|
322
|
+
raise EnvExecutionError(
|
|
323
|
+
f"no PEP 610 direct_url.json in {venv_root} records an install from {source} "
|
|
324
|
+
"-- the editable install did not record its source (fruit 12)"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def import_origin(venv_root: Path, module: str) -> str:
|
|
329
|
+
"""Where the venv's OWN interpreter resolves `module` from, in isolation.
|
|
330
|
+
|
|
331
|
+
`-I` is the whole point (fruit 11): without it an inherited PYTHONPATH or
|
|
332
|
+
the working directory can satisfy the import, and a passing import would say
|
|
333
|
+
nothing about what this venv installed."""
|
|
334
|
+
interpreter = venv_interpreter(venv_root)
|
|
335
|
+
proc = subprocess.run(
|
|
336
|
+
[
|
|
337
|
+
str(interpreter), "-I", "-c",
|
|
338
|
+
f"import {module},os;print(os.path.realpath({module}.__file__))",
|
|
339
|
+
],
|
|
340
|
+
capture_output=True,
|
|
341
|
+
text=True,
|
|
342
|
+
timeout=_PROBE_TIMEOUT_SECONDS,
|
|
343
|
+
check=False,
|
|
344
|
+
)
|
|
345
|
+
if proc.returncode != 0:
|
|
346
|
+
raise EnvExecutionError(
|
|
347
|
+
f"module {module!r} does not import under `python -I` in {venv_root}: "
|
|
348
|
+
f"{proc.stderr.strip()}"
|
|
349
|
+
)
|
|
350
|
+
return proc.stdout.strip()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def execute_editable_install_operation(
|
|
354
|
+
validated: ValidatedPlan, index: int, *, workspace_root: Path
|
|
355
|
+
) -> dict[str, object]:
|
|
356
|
+
"""Install `source_path` (with extras) as editable into `venv_path`."""
|
|
357
|
+
workspace_root = Path(os.fspath(workspace_root))
|
|
358
|
+
validated.verify(require_provenance=True)
|
|
359
|
+
_require_workspace_binding(validated, workspace_root)
|
|
360
|
+
|
|
361
|
+
op = _operation_at(validated, index, "editable_install")
|
|
362
|
+
extras = tuple(str(e) for e in op.get("extras", ()))
|
|
363
|
+
binding = _EditableBinding(
|
|
364
|
+
venv_path=str(op["venv_path"]),
|
|
365
|
+
source_path=str(op["source_path"]),
|
|
366
|
+
extras=extras,
|
|
367
|
+
)
|
|
368
|
+
venv_root = canonicalize_workspace_path(
|
|
369
|
+
workspace_root, binding.venv_path, field_name=f"operations[{index}].venv_path"
|
|
370
|
+
)
|
|
371
|
+
source = canonicalize_workspace_path(
|
|
372
|
+
workspace_root, binding.source_path, field_name=f"operations[{index}].source_path"
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# The venv must be a real one BEFORE we install into it. Installing into a
|
|
376
|
+
# directory that merely looks like a venv is how the operator's own
|
|
377
|
+
# interpreter gets mutated -- section 9.2's "never mutate the operator's
|
|
378
|
+
# Python environment".
|
|
379
|
+
verify_venv(venv_root)
|
|
380
|
+
if not (source / "pyproject.toml").is_file() and not (source / "setup.py").is_file():
|
|
381
|
+
raise EnvExecutionError(
|
|
382
|
+
f"editable source {source} has no pyproject.toml or setup.py -- there is "
|
|
383
|
+
"nothing to install"
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
target = str(source) + (f"[{','.join(binding.extras)}]" if binding.extras else "")
|
|
387
|
+
proc = subprocess.run(
|
|
388
|
+
[
|
|
389
|
+
"uv", "pip", "install",
|
|
390
|
+
"--python", str(venv_interpreter(venv_root)),
|
|
391
|
+
"--editable", target,
|
|
392
|
+
],
|
|
393
|
+
capture_output=True,
|
|
394
|
+
text=True,
|
|
395
|
+
timeout=_INSTALL_TIMEOUT_SECONDS,
|
|
396
|
+
check=False,
|
|
397
|
+
)
|
|
398
|
+
if proc.returncode != 0:
|
|
399
|
+
raise EnvExecutionError(
|
|
400
|
+
f"uv pip install --editable {target} failed:\n"
|
|
401
|
+
f"{proc.stderr.strip() or proc.stdout.strip()}"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
direct_url = read_direct_url(venv_root, source)
|
|
405
|
+
if not direct_url.get("dir_info", {}).get("editable"):
|
|
406
|
+
raise EnvExecutionError(
|
|
407
|
+
f"install from {source} is recorded in PEP 610 metadata but not as "
|
|
408
|
+
"editable (fruit 12)"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
"kind": "editable_install",
|
|
413
|
+
"venv_path": binding.venv_path,
|
|
414
|
+
"source_path": binding.source_path,
|
|
415
|
+
"extras": list(binding.extras),
|
|
416
|
+
"distribution": _distribution_name(source),
|
|
417
|
+
"editable": True,
|
|
418
|
+
"direct_url": str(direct_url.get("url", "")),
|
|
419
|
+
}
|