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.
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/PKG-INFO +1 -1
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/flow.py +55 -3
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen.egg-info/PKG-INFO +1 -1
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/pyproject.toml +1 -1
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_flow.py +106 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/README.md +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/__init__.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/backends.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/cli.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/gate.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/generate.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/logview.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/mcp_tools.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/picker.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen/py.typed +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen.egg-info/SOURCES.txt +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen.egg-info/dependency_links.txt +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen.egg-info/entry_points.txt +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/cgh_codegen.egg-info/top_level.txt +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/setup.cfg +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_backends.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_config_root.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_gate.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_generate.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_logview.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_picker.py +0 -0
- {cgh_codegen-0.1.0 → cgh_codegen-0.1.1}/tests/test_prompt_clean.py +0 -0
- {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.
|
|
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
|
|
228
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|