Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions plain2code_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class RenderContextSnapshot:
unit_tests_running_context: Optional[UnitTestsRunningContext]
script_execution_history: Optional[ScriptExecutionHistory]
module_name: str
# Where the functionality stands among those of its module, as "3/12". None before one is picked.
functionality_position: Optional[str] = None


@dataclass
Expand Down
12 changes: 12 additions & 0 deletions plain2code_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ def format_duration_hms(total_seconds: float) -> str:
}


def functionality_position(frids: list[str], frid: Optional[str]) -> Optional[str]:
"""Where a functionality stands among the functionalities of its module, as "3/12".

The count is over every functionality of the module, not over a render range, so the number
says where the render is in the specification whatever part of it was asked for.
"""
if frid is None or frid not in frids:
return None

return f"{frids.index(frid) + 1}/{len(frids)}"


def qualified_frid(module_name: Optional[str], frid: Optional[str]) -> Optional[str]:
"""A functionality named as `<module>:<func-id>`.

Expand Down
24 changes: 16 additions & 8 deletions render_machine/actions/render_functional_requirement.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ class RenderFunctionalRequirement(BaseAction):
FUNCTIONAL_REQUIREMENT_TOO_COMPLEX_OUTCOME = "functional_requirement_too_complex"
ITERATION_LIMIT_EXCEEDED_OUTCOME = "frid_iteration_limit_exceeded"

@staticmethod
def functionality_heading(render_context) -> str:
"""The block logged when a functionality starts: its module, its ID, and where it stands."""
msg = "-------------------------------------\n"
msg += f"Module: {render_context.module_name}\n"
msg += f"Rendering functionality {render_context.frid_context.frid}"
position = render_context.current_functionality_position()
if position:
msg += f" ({position})"
if render_context.frid_context.functional_requirement_render_attempts > 1:
msg += f", attempt number {render_context.frid_context.functional_requirement_render_attempts}/{MAX_CODE_GENERATION_RETRIES}"
msg += f":\n{render_context.frid_context.functional_requirement_text}\n"
msg += "-------------------------------------"
return msg

def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
if render_context.frid_context.functional_requirement_render_attempts >= MAX_CODE_GENERATION_RETRIES:
# A render attempt is only restarted once its unit tests used up every fix round.
Expand Down Expand Up @@ -49,14 +64,7 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any |
)
_, memory_files_content = MemoryManager.fetch_memory_files(render_context.memory_manager.memory_folder)

msg = "-------------------------------------\n"
msg += f"Module: {render_context.module_name}\n"
msg += f"Rendering functionality {render_context.frid_context.frid}"
if render_context.frid_context.functional_requirement_render_attempts > 1:
msg += f", attempt number {render_context.frid_context.functional_requirement_render_attempts}/{MAX_CODE_GENERATION_RETRIES}"
msg += f":\n{render_context.frid_context.functional_requirement_text}\n"
msg += "-------------------------------------"
console.info(msg)
console.info(self.functionality_heading(render_context))

try:
render_utils.print_inputs(render_context, existing_files_content, "Files sent as input to code generation:")
Expand Down
10 changes: 9 additions & 1 deletion render_machine/render_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from plain2code_console import RETRY_COLOR, console
from plain2code_events import RenderContextSnapshot
from plain2code_state import RunState
from plain2code_utils import qualified_frid
from plain2code_utils import functionality_position, qualified_frid
from plain_modules import PlainModule
from render_machine import triggers
from render_machine.conformance_tests import CONFORMANCE_TESTS_DEFINITION_FILE_NAME, ConformanceTests
Expand Down Expand Up @@ -122,6 +122,13 @@ def dispatch_error(self, error_message: str):
self.last_error_message = error_message
self.machine.dispatch(triggers.HANDLE_ERROR)

def current_functionality_position(self) -> Optional[str]:
"""Where the functionality being implemented stands among those of its module, as "3/12"."""
return functionality_position(
list(plain_spec.get_frids(self.plain_source_tree)),
self.frid_context.frid if self.frid_context else None,
)

def create_snapshot(self) -> RenderContextSnapshot:
return RenderContextSnapshot(
frid_context=deepcopy(self.frid_context) if self.frid_context else None,
Expand All @@ -133,6 +140,7 @@ def create_snapshot(self) -> RenderContextSnapshot:
),
script_execution_history=deepcopy(self.script_execution_history),
module_name=self.module_name,
functionality_position=self.current_functionality_position(),
)

def get_required_modules_functionalities(self):
Expand Down
90 changes: 90 additions & 0 deletions tests/test_functionality_position.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""The TUI names the functionality being implemented and where it stands in its module.

The func-id says which functionality it is. It does not say how far the module has come, which the
number before the renderer took func-ids used to say.
"""

from types import SimpleNamespace
from unittest.mock import MagicMock, patch

import pytest

import file_utils # noqa: F401 # imported before plain_modules, which it imports back from
from plain2code_events import RenderContextSnapshot
from plain2code_utils import functionality_position
from render_machine.actions.render_functional_requirement import RenderFunctionalRequirement
from tui.state_handlers import FridReadyHandler

FRIDS = ["add-login-button", "add-logout-button", "remember-the-session"]


class TestFunctionalityPosition:
def test_the_first_functionality(self):
assert functionality_position(FRIDS, "add-login-button") == "1/3"

def test_the_last_functionality(self):
assert functionality_position(FRIDS, "remember-the-session") == "3/3"

@pytest.mark.parametrize("frid", [None, "no-such-functionality"], ids=["none", "unknown"])
def test_nothing_to_place(self, frid):
assert functionality_position(FRIDS, frid) is None

def test_a_module_with_one_functionality(self):
assert functionality_position(["only-one"], "only-one") == "1/1"


def _snapshot(position):
return RenderContextSnapshot(
frid_context=SimpleNamespace(frid="add-login-button", functional_requirement_text="Add a login button."),
conformance_tests_running_context=None,
unit_tests_running_context=None,
script_execution_history=None,
module_name="",
functionality_position=position,
)


def _functionality_line(position):
handler = FridReadyHandler(MagicMock(), unittests_script=None, conformance_tests_script=None)
with (
patch("tui.state_handlers.get_frid_progress") as get_progress,
patch("tui.state_handlers.update_progress_item_status"),
):
handler.handle([], _snapshot(position), [])
return get_progress.return_value.update_functionality_text.call_args.args[0]


def test_the_tui_line_carries_the_position():
assert _functionality_line("3/12") == "Functionality add-login-button (3/12): Add a login button."


def test_the_tui_line_without_a_position_reads_as_before():
assert _functionality_line(None) == "Functionality add-login-button: Add a login button."


def _log_heading(frids, frid, attempts=1):
"""The heading the renderer logs when it starts a functionality."""
render_context = SimpleNamespace(
module_name="shop",
frid_context=SimpleNamespace(
frid=frid,
functional_requirement_text="Add a login button.",
functional_requirement_render_attempts=attempts,
),
current_functionality_position=lambda: functionality_position(frids, frid),
)
return RenderFunctionalRequirement.functionality_heading(render_context)


def test_the_log_heading_carries_the_position():
assert "Rendering functionality add-login-button (1/3):" in _log_heading(FRIDS, "add-login-button")


def test_the_log_heading_keeps_the_attempt_after_the_position():
heading = _log_heading(FRIDS, "add-login-button", attempts=2)

assert "Rendering functionality add-login-button (1/3), attempt number 2/" in heading


def test_the_log_heading_without_a_position_reads_as_before():
assert "Rendering functionality add-login-button:" in _log_heading([], "add-login-button")
4 changes: 3 additions & 1 deletion tui/state_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@ def handle(self, _: list[str], snapshot: RenderContextSnapshot, _previous_state_
# Update FRID text

functionality = qualified_frid(snapshot.module_name, snapshot.frid_context.frid)
# The position says how far the module has come; the func-id alone does not.
position = f" ({snapshot.functionality_position})" if snapshot.functionality_position else ""
rendering_functionality_text = (
f"{tui_components.FRIDProgress.RENDERING_FUNCTIONALITY_TEXT}{functionality}: "
f"{tui_components.FRIDProgress.RENDERING_FUNCTIONALITY_TEXT}{functionality}{position}: "
f"{snapshot.frid_context.functional_requirement_text}"
)
get_frid_progress(self.tui).update_functionality_text(rendering_functionality_text)
Expand Down
Loading