From e9b308cc9d71742d275ef566dedc76a8b41f24aa Mon Sep 17 00:00:00 2001 From: Nejc Stebe Date: Tue, 3 Nov 2026 07:35:46 +0100 Subject: [PATCH] fix: render the whole project again when --force-render is set With --force-render the client no longer asks where to start. It renders every module again, from the first one, and replaces the code rendered before. This also works in headless mode, where the question could not be answered and the render stopped. Without the option nothing changes: the client still asks when a choice can destroy rendered code. --- docs/plain2code_cli.md | 4 ++- partial_rendering.py | 21 +++++++++++++--- plain2code.py | 7 +++--- plain2code_arguments.py | 3 ++- tests/test_partial_rendering.py | 44 +++++++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/docs/plain2code_cli.md b/docs/plain2code_cli.md index 13f17e32..dd10bed5 100644 --- a/docs/plain2code_cli.md +++ b/docs/plain2code_cli.md @@ -62,7 +62,9 @@ options: with this ID will be included in the output. The functionality ID must match one of the functionalities in your plain file. - --force-render Force re-render of all the required modules. + --force-render Render every module of the project again, from the + first one. The client does not ask where to start and + the code rendered before is replaced. --unittests-script UNITTESTS_SCRIPT Shell script to run unit tests on generated code. Receives the module's code folder path as its first diff --git a/partial_rendering.py b/partial_rendering.py index 768e1ff0..d6eb712a 100644 --- a/partial_rendering.py +++ b/partial_rendering.py @@ -237,7 +237,6 @@ def change_is_only_future_work( def _resume_render_choice( plain_module: PlainModule, plain_module_render_state: PlainModuleRenderState, - force_render: bool, ) -> RenderChoice: """The natural next step from the last-rendered position when no decision is needed: start an unrendered module, continue a partially-rendered one, or advance to the next. @@ -261,8 +260,7 @@ def _resume_render_choice( render_range = plain_spec.get_render_range_from(next_frid, next_module.plain_source) return RenderChoice( module=next_module, - render_range=None if force_render else render_range, - wipe_later_modules=force_render, + render_range=render_range, choice_type="continue_from_frid", ) @@ -280,6 +278,21 @@ def get_render_choices( plain_module_render_state: PlainModuleRenderState, force_render: bool = False, ) -> dict[str, RenderChoice]: + if force_render: + # --force-render re-renders the whole chain from the first module, whatever changed and + # wherever the last render stopped. There is nothing to decide, so a single choice is + # returned and the user is not asked. + first_module = plain_module.all_required_modules[0] if plain_module.all_required_modules else plain_module + return { + "1": RenderChoice( + module=first_module, + render_range=None, + choice_type="rerender_from_first", + wipe_later_modules=True, + is_destructive=True, + ) + } + render_choices = list[RenderChoice]() module_start_points = list[str]() @@ -298,7 +311,7 @@ def get_render_choices( # Continue / resume from the last-rendered position. The change-driven blocks below decide # the start otherwise — once a change touches already-rendered work, resuming from the old # position would build on stale code. - render_choices.append(_resume_render_choice(plain_module, plain_module_render_state, force_render)) + render_choices.append(_resume_render_choice(plain_module, plain_module_render_state)) else: # change is set here (otherwise treat_as_continue would be True), so change_type is always diff --git a/plain2code.py b/plain2code.py index 9a2544b1..93842b8d 100644 --- a/plain2code.py +++ b/plain2code.py @@ -375,10 +375,11 @@ def render( # noqa: C901 render_choices = get_render_choices(plain_module, plain_module_render_state, args.force_render) ask_user = True render_choice = None - if len(render_choices) <= 2: - # Last choice is Quit, first choice is the only other actionable choice + if args.force_render or len(render_choices) <= 2: + # With --force-render there is a single choice. Otherwise the last choice is Quit, + # so the first choice is the only other actionable one. render_choice = render_choices[list(render_choices.keys())[0]] - ask_user = render_choice.is_destructive + ask_user = render_choice.is_destructive and not args.force_render if ask_user and not args.headless: app = PlainModuleRenderChoiceTUI( diff --git a/plain2code_arguments.py b/plain2code_arguments.py index 44512c9a..ff7b9355 100644 --- a/plain2code_arguments.py +++ b/plain2code_arguments.py @@ -352,7 +352,8 @@ def create_parser(): "--force-render", action="store_true", default=False, - help="Force re-render of all the required modules.", + help="Render every module of the project again, from the first one. " + "The client does not ask where to start and the code rendered before is replaced.", ) _add_arg( diff --git a/tests/test_partial_rendering.py b/tests/test_partial_rendering.py index c2f58655..9b50cbaa 100644 --- a/tests/test_partial_rendering.py +++ b/tests/test_partial_rendering.py @@ -569,6 +569,50 @@ def test_render_choices_always_ends_with_quit(): assert choices[last_key].module is None +# --- Block 3: --force-render leaves nothing to choose --- + + +def test_render_choices_force_render_rerenders_whole_chain_without_a_choice(): + """--force-render renders the whole chain again from the first module, so a single, + non-interactive choice is returned (no Quit, because the user is never asked).""" + root, _, leaf = _build_choices_tree(fully_rendered=True) + pr = PlainModuleRenderState(last_render_module=root, last_render_frid="fr-e") + + choices = get_render_choices(root, pr, force_render=True) + + assert _choice_types(choices) == ["rerender_from_first"] + assert choices["1"].module is leaf + assert choices["1"].render_range is None + assert choices["1"].wipe_later_modules is True + + +def test_render_choices_force_render_ignores_a_detected_change(monkeypatch): + """A change that would otherwise be offered as a partial render is ignored: the chain is + rendered again from the first module.""" + root, middle, leaf = _build_choices_tree(fully_rendered=True) + monkeypatch.setattr( + partial_rendering, "determine_partial_render_start", lambda pm: PartialRenderStart(module=middle, frid="fr-c") + ) + pr = PlainModuleRenderState( + last_render_module=root, last_render_frid="fr-e", change=middle, change_type="spec_change" + ) + + choices = get_render_choices(root, pr, force_render=True) + + assert _choice_types(choices) == ["rerender_from_first"] + assert choices["1"].module is leaf + + +def test_render_choices_force_render_on_a_module_without_required_modules(): + root = FakeModule("root", fully_rendered=True) + pr = PlainModuleRenderState(last_render_module=root, last_render_frid="fr-e") + + choices = get_render_choices(root, pr, force_render=True) + + assert _choice_types(choices) == ["rerender_from_first"] + assert choices["1"].module is root + + # ------------------------- # change_is_only_future_work # -------------------------