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 # -------------------------