cgh-codegen 0.1.0__tar.gz → 0.1.1__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 (28) hide show
  1. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/PKG-INFO +1 -1
  2. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/flow.py +55 -3
  3. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen.egg-info/PKG-INFO +1 -1
  4. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/pyproject.toml +1 -1
  5. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_flow.py +106 -0
  6. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/README.md +0 -0
  7. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/__init__.py +0 -0
  8. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/backends.py +0 -0
  9. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/cli.py +0 -0
  10. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/gate.py +0 -0
  11. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/generate.py +0 -0
  12. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/logview.py +0 -0
  13. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/mcp_tools.py +0 -0
  14. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/picker.py +0 -0
  15. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/py.typed +0 -0
  16. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen.egg-info/SOURCES.txt +0 -0
  17. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen.egg-info/dependency_links.txt +0 -0
  18. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen.egg-info/entry_points.txt +0 -0
  19. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen.egg-info/top_level.txt +0 -0
  20. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/setup.cfg +0 -0
  21. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_backends.py +0 -0
  22. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_config_root.py +0 -0
  23. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_gate.py +0 -0
  24. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_generate.py +0 -0
  25. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_logview.py +0 -0
  26. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_picker.py +0 -0
  27. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_prompt_clean.py +0 -0
  28. {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_spec_source.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgh-codegen
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Pattern-matched code generation for cgh: a cheap model writes boilerplate that mirrors a reference file cgh picks from the graph
5
5
  Author-email: Joy Ndjama <joy.ndjama@altikva.com>
6
6
  License-Expression: MIT
@@ -191,6 +191,20 @@ def run_generation(
191
191
  "to regenerate it from scratch."
192
192
  )
193
193
 
194
+ # The checks above run before generation, which can take minutes. Every
195
+ # write below also checks, at the moment of writing, that the target still
196
+ # holds what this run expects: nothing for a new file, the content read
197
+ # above for force or extend, then whatever this run last wrote. A file
198
+ # created or edited by anyone else in between is left as is and the call
199
+ # fails, instead of being overwritten without a word.
200
+ expected: str | None = None
201
+ if tgt is not None and tgt.exists():
202
+ expected = _sha(
203
+ existing_text
204
+ if existing_text is not None
205
+ else tgt.read_text(encoding="utf-8", errors="replace")
206
+ )
207
+
194
208
  do_verify = bool(verify) and not to_stdout
195
209
  prior: tuple[str, str] | None = None
196
210
  verified: bool | None = None
@@ -224,12 +238,16 @@ def run_generation(
224
238
  body = _append_into(existing_text, code)
225
239
  else:
226
240
  body = code if code.endswith("\n") else code + "\n"
227
- tgt.parent.mkdir(parents=True, exist_ok=True)
228
- tgt.write_text(body, encoding="utf-8")
241
+ _write_expected(tgt, body, expected, target)
242
+ expected = _sha(body)
229
243
  written = True
230
244
  if not do_verify:
231
245
  break
232
246
  ok, output = _run_verify(root, verify) # type: ignore[arg-type]
247
+ # The check belongs to this run and may rewrite the file itself (a
248
+ # formatter with --fix), so what it leaves is what the next attempt
249
+ # expects to replace.
250
+ expected = _sha(tgt.read_text(encoding="utf-8", errors="replace"))
233
251
  verified = ok
234
252
  if ok:
235
253
  break
@@ -241,7 +259,7 @@ def run_generation(
241
259
  # unparsable. So put the original back and report the failure instead.
242
260
  rolled_back = False
243
261
  if extend and written and do_verify and not verified and existing_text is not None:
244
- tgt.write_text(existing_text, encoding="utf-8") # type: ignore[union-attr]
262
+ _write_expected(tgt, existing_text, expected, target) # type: ignore[arg-type]
245
263
  written = False
246
264
  rolled_back = True
247
265
 
@@ -408,6 +426,40 @@ def _sha(text: str) -> str:
408
426
  return hashlib.sha256(text.encode("utf-8")).hexdigest()
409
427
 
410
428
 
429
+ def _write_expected(tgt: Path, body: str, expected: str | None, target: str) -> None:
430
+ """Write ``body`` only if the target still holds what this run expects.
431
+
432
+ ``expected`` is the sha of the content that should be on disk, or None when
433
+ the file must not exist yet. A new file is created exclusively, so one that
434
+ appeared during generation fails the open instead of being truncated. For
435
+ an existing file the content is compared just before writing; the window
436
+ left between that read and the write is a few syscalls, against minutes of
437
+ generation before. On a mismatch nothing is written and the caller is told,
438
+ so another writer's work is never lost."""
439
+ tgt.parent.mkdir(parents=True, exist_ok=True)
440
+ if expected is None:
441
+ try:
442
+ with open(tgt, "x", encoding="utf-8") as fh:
443
+ fh.write(body)
444
+ except FileExistsError:
445
+ raise CodegenError(
446
+ f"target {target} appeared while codegen was generating it; it "
447
+ "was left as is and nothing was written. Read it before "
448
+ "generating again."
449
+ ) from None
450
+ return
451
+ try:
452
+ current: str | None = _sha(tgt.read_text(encoding="utf-8", errors="replace"))
453
+ except FileNotFoundError:
454
+ current = None
455
+ if current != expected:
456
+ raise CodegenError(
457
+ f"target {target} changed while codegen was generating; it was left "
458
+ "as is and nothing was written, so those changes are not lost."
459
+ )
460
+ tgt.write_text(body, encoding="utf-8")
461
+
462
+
411
463
  def _provenance_path(root: Path) -> Path:
412
464
  return root / ".codegraph" / "codegen" / "provenance.json"
413
465
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cgh-codegen
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Pattern-matched code generation for cgh: a cheap model writes boilerplate that mirrors a reference file cgh picks from the graph
5
5
  Author-email: Joy Ndjama <joy.ndjama@altikva.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cgh-codegen"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Pattern-matched code generation for cgh: a cheap model writes boilerplate that mirrors a reference file cgh picks from the graph"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -12,6 +12,8 @@
12
12
 
13
13
  from __future__ import annotations
14
14
 
15
+ import sys
16
+
15
17
  import pytest
16
18
 
17
19
  pytest.importorskip("cgh_codegen")
@@ -476,5 +478,109 @@ def test_generated_python_is_ruff_polished(tmp_path):
476
478
  assert all(line == line.rstrip() for line in content.splitlines())
477
479
 
478
480
 
481
+ class _WriterDuringGeneration:
482
+ """A backend that, while it "thinks", lets someone else write the target:
483
+ a hand edit, or another codegen call. Generation takes minutes in real use,
484
+ and the flow used to check the target only before it."""
485
+
486
+ is_local = True
487
+ name = "fake"
488
+
489
+ def __init__(self, target, text: str, reply: str) -> None:
490
+ self._target, self._text, self._reply = target, text, reply
491
+
492
+ def generate(self, system: str, user: str) -> tuple[str, float]:
493
+ self._target.write_text(self._text, encoding="utf-8")
494
+ return self._reply, 0.0
495
+
496
+
497
+ class TestWritesDuringGeneration:
498
+ def test_a_file_created_meanwhile_is_not_overwritten(self, tmp_path):
499
+ root = _repo(tmp_path)
500
+ target = root / "src" / "user_service.py"
501
+ backend = _WriterDuringGeneration(
502
+ target, "# written by hand\n", "```python\nx = 1\n```"
503
+ )
504
+ with pytest.raises(CodegenError, match="appeared while codegen"):
505
+ run_generation(
506
+ root,
507
+ "spec",
508
+ "src/user_service.py",
509
+ "src/order_service.py",
510
+ config={},
511
+ backend=backend,
512
+ )
513
+ assert target.read_text(encoding="utf-8") == "# written by hand\n"
514
+
515
+ def test_force_keeps_an_edit_made_meanwhile(self, tmp_path):
516
+ root = _repo(tmp_path)
517
+ target = root / "src" / "user_service.py"
518
+ run_generation(
519
+ root,
520
+ "spec",
521
+ "src/user_service.py",
522
+ "src/order_service.py",
523
+ config={},
524
+ backend=FakeBackend("```python\nx = 1\n```"),
525
+ )
526
+ backend = _WriterDuringGeneration(
527
+ target, "x = 1\n# edited meanwhile\n", "```python\nx = 2\n```"
528
+ )
529
+ with pytest.raises(CodegenError, match="changed while codegen"):
530
+ run_generation(
531
+ root,
532
+ "spec",
533
+ "src/user_service.py",
534
+ "src/order_service.py",
535
+ config={},
536
+ backend=backend,
537
+ force=True,
538
+ )
539
+ assert "# edited meanwhile" in target.read_text(encoding="utf-8")
540
+
541
+ def test_extend_keeps_an_edit_made_meanwhile(self, tmp_path):
542
+ root = _repo(tmp_path)
543
+ target = root / "src" / "existing.py"
544
+ target.write_text("# original\n", encoding="utf-8")
545
+ backend = _WriterDuringGeneration(
546
+ target, "# original\n# edited meanwhile\n", "```python\nx = 1\n```"
547
+ )
548
+ with pytest.raises(CodegenError, match="changed while codegen"):
549
+ run_generation(
550
+ root,
551
+ "spec",
552
+ "src/existing.py",
553
+ None,
554
+ config={},
555
+ backend=backend,
556
+ extend=True,
557
+ )
558
+ assert target.read_text(encoding="utf-8") == "# original\n# edited meanwhile\n"
559
+
560
+ def test_a_verify_that_rewrites_the_file_does_not_block_the_retry(self, tmp_path):
561
+ # The check is part of this run; a formatter that rewrites the file in
562
+ # place must not be mistaken for another writer on the next attempt.
563
+ root = _repo(tmp_path)
564
+ backend = SequenceBackend(
565
+ ["```python\nx = 1 # bad\n```", "```python\nx = 1 # GOOD\n```"]
566
+ )
567
+ out = run_generation(
568
+ root,
569
+ "spec",
570
+ "src/x.py",
571
+ "src/order_service.py",
572
+ config={},
573
+ backend=backend,
574
+ # argv, no shell: append to the file, then pass only once it says GOOD
575
+ verify=(
576
+ f"{sys.executable} -c \"import pathlib, sys; p = pathlib.Path('src/x.py'); "
577
+ "t = p.read_text(); p.write_text(t + '# touched by the check\\n'); "
578
+ "sys.exit(0 if 'GOOD' in t else 1)\""
579
+ ),
580
+ max_attempts=3,
581
+ )
582
+ assert out["verified"] is True and out["attempts"] == 2
583
+
584
+
479
585
  if __name__ == "__main__": # pragma: no cover
480
586
  pytest.main([__file__, "-q"])
File without changes
File without changes