mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 00:27:01 +01:00
_challenge.py 467 -> 236. The two flows and run_coding_challenge go to _challenge_flows.py; the three static prompt strings to _challenge_prompts.py, which covered the last eleven lines after the first cut landed at 261. The 13 patch strings in test_challenge_part3.py were repointed per test, by which function is under test - the _run_user_impl tests keep _challenge. Getting that wrong does not fail here, it HANGS: an unintercepted _collect_and_rate_tests ran the real interactive flow and spun the suite for two minutes. Runs are now under an explicit timeout, and the patch targets are asserted to resolve before pytest starts. Pass count is 216 before and after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
238 lines
7.5 KiB
Python
238 lines
7.5 KiB
Python
"""Coding challenge: reproduce a function from scratch, validated by tests.
|
|
|
|
After a PASS verdict the user is offered a challenge: rewrite the function
|
|
without seeing its body, validated either by existing tests or by tests they
|
|
write themselves.
|
|
|
|
If existing tests exist for the function -> show them, ask user to implement.
|
|
If no tests exist -> ask user to write tests first, rate them, then ask user
|
|
to implement.
|
|
|
|
In both cases the original implementation is hidden during the challenge.
|
|
|
|
The lower-level discovery / pytest / verdict helpers live in
|
|
:mod:`python_pkg.code_tutor._challenge_support`, and the two flows plus the
|
|
public ``run_coding_challenge`` entry point in
|
|
:mod:`python_pkg.code_tutor._challenge_flows`. This module keeps the three
|
|
interactive stages those flows are built from.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import ast
|
|
from pathlib import Path
|
|
import tempfile
|
|
from typing import TYPE_CHECKING
|
|
|
|
from rich.panel import Panel
|
|
from rich.syntax import Syntax
|
|
|
|
from python_pkg.code_tutor._challenge_prompts import (
|
|
_MAX_TEST_ATTEMPTS,
|
|
_TEST_JUDGE_SYSTEM,
|
|
)
|
|
from python_pkg.code_tutor._challenge_support import (
|
|
_collect_lines,
|
|
_extract_signature_block,
|
|
_project_root,
|
|
)
|
|
from python_pkg.code_tutor._pytest_runner import _pytest_clean
|
|
from python_pkg.code_tutor._verdict import _parse_verdict, _stream_verdict
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
from rich.console import Console
|
|
|
|
from python_pkg.code_tutor._analyzer import CodeItem
|
|
from python_pkg.code_tutor._llm import Backend
|
|
|
|
|
|
def _validate_tests_against_real(
|
|
test_code: str,
|
|
import_header: str,
|
|
project_root: Path,
|
|
console: Console,
|
|
) -> bool:
|
|
"""Write *test_code* to a temp file and run it against the real implementation.
|
|
|
|
If the tests fail on the real (correct) implementation, they are wrong.
|
|
|
|
Args:
|
|
test_code: The user's pytest test code.
|
|
import_header: Auto-generated import line prepended to the test file.
|
|
project_root: Project root used for PYTHONPATH and cwd.
|
|
console: Rich console for output.
|
|
|
|
Returns:
|
|
``True`` when all tests pass on the real implementation.
|
|
"""
|
|
full_code = import_header + "\n\n" + test_code
|
|
try:
|
|
ast.parse(full_code)
|
|
except SyntaxError as exc:
|
|
console.print(f"[red]Syntax error in your tests: {exc}[/red]")
|
|
return False
|
|
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w",
|
|
suffix="_ct_test.py",
|
|
dir=None,
|
|
delete=False,
|
|
encoding="utf-8",
|
|
) as fh:
|
|
fh.write(full_code)
|
|
temp_path = Path(fh.name)
|
|
|
|
try:
|
|
return _pytest_clean(
|
|
[str(temp_path)],
|
|
project_root,
|
|
console,
|
|
extra_env={"PYTHONPATH": str(project_root)},
|
|
)
|
|
finally:
|
|
temp_path.unlink(missing_ok=True)
|
|
|
|
|
|
def _collect_and_rate_tests(
|
|
sig_block: str,
|
|
user_explanation: str,
|
|
backend: Backend,
|
|
console: Console,
|
|
input_fn: Callable[[str], str],
|
|
) -> str | None:
|
|
"""Prompt the user to write tests and rate them up to *_MAX_TEST_ATTEMPTS* times.
|
|
|
|
Args:
|
|
sig_block: Function signature + docstring shown as context.
|
|
user_explanation: Student's earlier explanation (shown as reference).
|
|
backend: LLM backend for test quality rating.
|
|
console: Rich console for output.
|
|
input_fn: Callable for reading user input.
|
|
|
|
Returns:
|
|
The approved test code string, or ``None`` if the user skipped.
|
|
"""
|
|
for attempt in range(1, _MAX_TEST_ATTEMPTS + 1):
|
|
collected = _collect_lines(
|
|
f"\n[bold]Write your tests (attempt {attempt}/{_MAX_TEST_ATTEMPTS}).[/bold]"
|
|
" Finish with [dim]END[/dim] on a blank line,"
|
|
" or [dim]skip[/dim] to exit.",
|
|
console,
|
|
input_fn,
|
|
)
|
|
if collected is None:
|
|
console.print("[yellow]Challenge skipped.[/yellow]")
|
|
return None
|
|
|
|
sig_and_exp = (
|
|
f"Function contract:\n{sig_block}"
|
|
f"\n\nStudent explanation:\n{user_explanation}"
|
|
)
|
|
raw = _stream_verdict(
|
|
_TEST_JUDGE_SYSTEM,
|
|
f"{sig_and_exp}\n\nStudent's tests:\n{collected}",
|
|
backend,
|
|
console,
|
|
label="Rating tests",
|
|
)
|
|
verdict, gap = _parse_verdict(raw)
|
|
|
|
if verdict == "PASS":
|
|
return collected
|
|
|
|
console.print(f"[red]Tests need improvement[/red] -- {gap}")
|
|
if attempt == _MAX_TEST_ATTEMPTS:
|
|
console.print(
|
|
f"[yellow]Skipping challenge after"
|
|
f" {_MAX_TEST_ATTEMPTS} attempts.[/yellow]"
|
|
)
|
|
return None
|
|
console.print("Try again with better coverage:\n")
|
|
return None # pragma: no cover - loop always returns on the final attempt
|
|
|
|
|
|
def _run_user_impl(
|
|
item: CodeItem,
|
|
codebase_path: str,
|
|
test_code: str,
|
|
import_hint: str,
|
|
console: Console,
|
|
input_fn: Callable[[str], str],
|
|
) -> str:
|
|
"""Prompt user to write an implementation and run it against *test_code*.
|
|
|
|
Patches the source file, runs a temp test file, then always restores.
|
|
|
|
Args:
|
|
item: Code item to implement.
|
|
codebase_path: Absolute codebase root path.
|
|
test_code: Approved test code (without import header).
|
|
import_hint: Auto-generated import line prepended to the test file.
|
|
console: Rich console for output.
|
|
input_fn: Callable for reading user input.
|
|
|
|
Returns:
|
|
``"passed"``, ``"failed"``, or ``"skipped"``.
|
|
"""
|
|
project_root = _project_root(Path(codebase_path))
|
|
sig_block = _extract_signature_block(item, codebase_path)
|
|
console.print(
|
|
Panel(
|
|
Syntax(sig_block, "python", theme="monokai"),
|
|
title="[blue]Function signature (implement this)[/blue]",
|
|
border_style="blue",
|
|
)
|
|
)
|
|
user_impl = _collect_lines(
|
|
f"\n[bold]Now write the implementation of [cyan]{item.name}[/cyan].[/bold]"
|
|
" Finish with [dim]END[/dim] on a blank line, or [dim]skip[/dim] to exit.",
|
|
console,
|
|
input_fn,
|
|
)
|
|
if user_impl is None:
|
|
console.print("[yellow]Challenge skipped.[/yellow]")
|
|
return "skipped"
|
|
|
|
try:
|
|
ast.parse(user_impl)
|
|
except SyntaxError as exc:
|
|
console.print(f"[red]Syntax error in your implementation: {exc}[/red]")
|
|
return "failed"
|
|
|
|
full_test_code = import_hint + "\n\n" + test_code
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix="_ct_test.py", delete=False, encoding="utf-8"
|
|
) as fh:
|
|
fh.write(full_test_code)
|
|
temp_path = Path(fh.name)
|
|
|
|
source_file = Path(codebase_path) / item.file
|
|
original = source_file.read_text(encoding="utf-8")
|
|
orig_lines = original.splitlines()
|
|
before = orig_lines[: item.start_line - 1]
|
|
after = orig_lines[item.end_line :]
|
|
new_source = "\n".join(before + user_impl.splitlines() + after) + "\n"
|
|
|
|
try:
|
|
source_file.write_text(new_source, encoding="utf-8")
|
|
passed = _pytest_clean(
|
|
[str(temp_path)],
|
|
project_root,
|
|
console,
|
|
extra_env={"PYTHONPATH": str(project_root)},
|
|
)
|
|
finally:
|
|
source_file.write_text(original, encoding="utf-8")
|
|
temp_path.unlink(missing_ok=True)
|
|
|
|
if passed:
|
|
console.print(
|
|
"[green bold]✓ All your tests passed"
|
|
" -- you can write it and test it![/green bold]"
|
|
)
|
|
return "passed"
|
|
console.print("[red]✗ Your implementation didn't pass your own tests.[/red]")
|
|
return "failed"
|