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.
Files changed (80) hide show
  1. gitgrip-1.5.0.dist-info/METADATA +13 -0
  2. gitgrip-1.5.0.dist-info/RECORD +80 -0
  3. gitgrip-1.5.0.dist-info/WHEEL +5 -0
  4. gitgrip-1.5.0.dist-info/entry_points.txt +2 -0
  5. gitgrip-1.5.0.dist-info/top_level.txt +2 -0
  6. gr2/__init__.py +0 -0
  7. gr2/overlay/__init__.py +6 -0
  8. gr2/overlay/activate.py +196 -0
  9. gr2/overlay/agent_manifest.py +138 -0
  10. gr2/overlay/cli.py +181 -0
  11. gr2/overlay/cross_repo.py +124 -0
  12. gr2/overlay/drivers.py +113 -0
  13. gr2/overlay/introspection.py +155 -0
  14. gr2/overlay/language_drivers.py +115 -0
  15. gr2/overlay/objects.py +412 -0
  16. gr2/overlay/perf.py +251 -0
  17. gr2/overlay/refs.py +36 -0
  18. gr2/overlay/trust.py +150 -0
  19. gr2/overlay/types.py +69 -0
  20. gr2/overlay/units.py +313 -0
  21. gr2/overlay/workspace_spec.py +59 -0
  22. gr2/prototypes/__init__.py +0 -0
  23. gr2/prototypes/cache_materialization_probe.py +190 -0
  24. gr2/prototypes/concurrent_event_stress.py +199 -0
  25. gr2/prototypes/concurrent_lease_stress.py +240 -0
  26. gr2/prototypes/concurrent_workspace_cap_stress.py +231 -0
  27. gr2/prototypes/contribution_protocol.py +665 -0
  28. gr2/prototypes/cross_mode_lane_stress.py +986 -0
  29. gr2/prototypes/jsonl_store.py +158 -0
  30. gr2/prototypes/lane_workspace_prototype.py +2088 -0
  31. gr2/prototypes/layout_model_probe.py +139 -0
  32. gr2/prototypes/propagation_daemon.py +546 -0
  33. gr2/prototypes/propagation_state_machine.py +1478 -0
  34. gr2/prototypes/python_exec_playground.py +194 -0
  35. gr2/prototypes/python_hook_runtime_playground.py +240 -0
  36. gr2/prototypes/python_migration_playground.py +144 -0
  37. gr2/prototypes/python_review_checkout_playground.py +242 -0
  38. gr2/prototypes/python_spec_apply_playground.py +282 -0
  39. gr2/prototypes/real_git_lane_materialization.py +248 -0
  40. gr2/prototypes/real_git_playground.py +334 -0
  41. gr2/prototypes/recall_lane_history.py +274 -0
  42. gr2/prototypes/repo_maintenance_prototype.py +659 -0
  43. gr2/prototypes/repo_transport_probe.py +147 -0
  44. gr2/python_cli/__init__.py +2 -0
  45. gr2/python_cli/__main__.py +6 -0
  46. gr2/python_cli/add.py +51 -0
  47. gr2/python_cli/app.py +2516 -0
  48. gr2/python_cli/branch.py +67 -0
  49. gr2/python_cli/channel_bridge.py +131 -0
  50. gr2/python_cli/clone_exec.py +1019 -0
  51. gr2/python_cli/commit.py +199 -0
  52. gr2/python_cli/config.py +291 -0
  53. gr2/python_cli/env_exec.py +419 -0
  54. gr2/python_cli/events.py +529 -0
  55. gr2/python_cli/execops.py +372 -0
  56. gr2/python_cli/failures.py +98 -0
  57. gr2/python_cli/file_exec.py +256 -0
  58. gr2/python_cli/gitops.py +226 -0
  59. gr2/python_cli/grip.py +1337 -0
  60. gr2/python_cli/grip_cli.py +493 -0
  61. gr2/python_cli/hooks.py +450 -0
  62. gr2/python_cli/launch_exec.py +786 -0
  63. gr2/python_cli/merge_verification.py +274 -0
  64. gr2/python_cli/migration.py +985 -0
  65. gr2/python_cli/open_gr_review.py +699 -0
  66. gr2/python_cli/platform.py +441 -0
  67. gr2/python_cli/pr.py +487 -0
  68. gr2/python_cli/project_review.py +314 -0
  69. gr2/python_cli/prune.py +365 -0
  70. gr2/python_cli/push.py +172 -0
  71. gr2/python_cli/review.py +462 -0
  72. gr2/python_cli/review_ephemeral.py +143 -0
  73. gr2/python_cli/review_run.py +621 -0
  74. gr2/python_cli/spec_apply.py +1285 -0
  75. gr2/python_cli/staging_cleanup.py +205 -0
  76. gr2/python_cli/syncops.py +920 -0
  77. gr2/python_cli/target.py +100 -0
  78. gr2/python_cli/workspace_snapshot.py +105 -0
  79. gr2/schemas/gr2-materialization-plan-v1.schema.json +191 -0
  80. 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
+ }