Skip to content

Partial rendering#167

Merged
zanjonke merged 6 commits into
mainfrom
feat/partial_rendering
May 15, 2026
Merged

Partial rendering#167
zanjonke merged 6 commits into
mainfrom
feat/partial_rendering

Conversation

@zanjonke

@zanjonke zanjonke commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds partial rendering — when re-running codeplain on a module that has been previously rendered, detect what has changed and offer to resume from the right checkpoint instead of re-rendering everything from scratch.

Given a module tree (e.g. root → middle → leaf), on startup the tool now:

  • Walks the tree looking for: (a) the last FRID successfully rendered, (b) any module whose plain spec has changed since its last render, (c) any module whose required modules' code has changed since.
  • Picks the earliest-affected module and presents a TUI with the available continuation choices (continue from next FRID, re-render from start of changed module, re-render everything, quit).
  • Feeds the user's choice into ModuleRenderer, which renders the chosen starting point before the root module.

The PR can be split in 3 parts:

  • Refactoring of plain_modules.py and module_renderer.py
  • Implementation of partial rendering
  • Addiiton of unit tests

TODO: this is waiting on the design from @kaja-s DONE

Here are some preliminary screenshots.
Screenshot 2026-04-21 at 11 55 02

Screenshot 2026-04-21 at 11 55 23

@zanjonke zanjonke self-assigned this Apr 21, 2026
@zanjonke zanjonke added the enhancement New feature or request label Apr 21, 2026
@zanjonke zanjonke force-pushed the feat/partial_rendering branch 2 times, most recently from 7c4683a to b4929b2 Compare April 21, 2026 09:22
@kaja-s

kaja-s commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

Here are the final designs for all 13 situations with adjusted content: https://www.figma.com/design/7o1Ql87jRiT7k1Ne0Inugl/design?node-id=11155-2090

@zanjonke zanjonke force-pushed the feat/partial_rendering branch 10 times, most recently from 8e4bb3b to 9b096e6 Compare April 24, 2026 09:31
@zanjonke zanjonke force-pushed the feat/partial_rendering branch from 9b096e6 to 1700ddc Compare May 7, 2026 05:27

@pedjaradenkovic pedjaradenkovic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kaja-s @zanjonke

Before going into reviewing the whole code I did testing. I have 2 modules chickens and hello_world_python that was changed to contain 4 functionalities.

  1. I run codeplain chickens. I rendered 3 functionalities of hello world module and stoped and run the renderer again.
Image

It detected that it can continue rendering from functionality 4 and it gave me that option but I think it should say in which module. Should it say: Continue from functionality 4 in hello_world_python instead?

  1. Module status is IMO slightly confusing. I would rather state something: last rendered functionality or something similar.

  2. Current shortened version of functionality is too short. I suggest we print a couple of lines before concatenating it.

  3. When the whole module is rendered and I start the render of the module again, because it's a very destructive action, I suggest we give a choice to users - start from scratch or quit.

  4. When both modules are fully rendered and I changed something in base module (hello_world) it started implementing everything from scratch without asking me. I think we should ask for that.

@kaja-s

kaja-s commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the feedback, @pedjaradenkovic.

  1. I agree about adding the module name. I created a v4 in Figma where I made the changes: https://www.figma.com/design/7o1Ql87jRiT7k1Ne0Inugl/design?node-id=11460-1275&t=VB2vlpLfpoT36GZl-4
  2. I didn't want to make too many changes to the partial-rendering screen language from the status TUI screen I think we should, for now, keep the language the same on both screens, and on the status TUI, we use "module status".
Screenshot 2026-05-12 at 11 10 02
  1. We can print the first three and then have an option to expand in parentheses.

If I understand correctly, the points 4. and 5. have already been implemented by Zan.

@pedjaradenkovic pedjaradenkovic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are some more testing notes:

  1. The modules are rendered in this order:
    hello_world -> chicken -> eggs

hello_world module was fully rendered by running codeplain hello_world.plain. After that I run codeplain eggs.plain that should be able to continue rendering chicken and eggs modules but I got this:

Image

There was not change in the specs since the module was rendered. I think this is a bug.

Also, module status is not correct. Eggs was not rendered at all and here it's written fully rendered.

  1. When I press quit I get:
✗ rendering failed

  render id:                    f3a14feb-4a24-4aa1-add0-eb1434be7767
  input file:                   hello_world_python.plain
  generated code folder:        -

functionalities  0  used credits  0  render time  0s

We don't have to fix this as a part of this PR - we can sync with @NejcS and do it separately.

Left some comments but will continue tomorrow.

Comment thread plain_modules.py
with open(metadata_path, "w", encoding="utf-8") as f:
json.dump(module_metadata, f, indent=4)

def _ensure_module_folders_exist(self, first_render_frid: str, render_conformance_tests: bool):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method and _ensure_frid_commit_exists and _ensure_previous_frid_commits_exist were copied here but not removed from ModuleRenderer (and they are never used there).

Comment thread partial_rendering.py Outdated
render_range = plain_spec.get_render_range_from(next_frid, next_module.plain_source)
msg = "Continue from"
if next_frid != plain_spec.get_first_frid(next_module.plain_source):
msg += f" functionality [#5593FF]{next_frid}[/] in module [#5593FF]{next_module.module_name}[/]"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we remove everything that is related to presentation layer from this class. IMO, this should stay business logic only, return apropriate data structure that is further shown on TUI with appropriate language and colors. WDYT?

Comment thread plain2code.py Outdated
):
sys.exit(0)

if partial_render_choice is not None and render_range is not None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this change is needed as checks render_range is None in previous statement.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was added defensively since the following code always expects for one or the other to be not None. But their default values are that exactly.

Comment thread partial_rendering.py Outdated
render_range: list[str] | None = None
msg: str | None = None
wipe_later_modules: bool = False
is_desctructive: bool = False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: is_destructive

Comment thread plain2code.py Outdated
if partial_render_choice is None or (
partial_render_choice.module is None
and partial_render_choice.render_range is None
and partial_render_choice.msg == "Quit"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extract strings to constants.

Comment thread partial_rendering.py
continue

module_metadata = _module.load_module_metadata()
previous_module = _module.required_modules[-1]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, what it there were multiple required modules? I think this will check only the first one and ignore the rest.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The source code for the current module is always taken from the last in the requires list. If any previous module code changes it will be captured before this.

Comment thread partial_rendering.py Outdated


@dataclass
class PartialRender:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Partial is giving a wrong impression of what this class represents. I would rename it.

@zanjonke

Copy link
Copy Markdown
Contributor Author

@pedjaradenkovic

Here are some more testing notes:

The modules are rendered in this order:
hello_world -> chicken -> eggs
hello_world module was fully rendered by running codeplain hello_world.plain. After that I run codeplain eggs.plain that should be able to continue rendering chicken and eggs modules but I got this:

I couldnt manage to replicate this behaviour using airplain. I suggest we try to resolve this in person.

@zanjonke zanjonke force-pushed the feat/partial_rendering branch 5 times, most recently from 8770f97 to d4462c3 Compare May 15, 2026 12:58
@zanjonke zanjonke force-pushed the feat/partial_rendering branch from d4462c3 to 68346ec Compare May 15, 2026 13:55

@pedjaradenkovic pedjaradenkovic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving the comments to follow up on them on Monday.

Comment thread git_utils.py
)
frid = match.group(1)

match = re.search(r"Module name:\s*(\S+)\n", commit_message)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: In this method you have duplicated module finding from commit message.

IMO it would be clearer if you would find the relevant commit first and then identify the module and have only one return.

Comment thread plain_modules.py
return os.path.join(self.module_build_folder, CODEPLAIN_METADATA_FOLDER)

def get_module_render_status(self) -> tuple[str | None, str | None]:
if len(self.required_modules) == 0:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is branch is not necessary and it's making confusion.

I'm not a big fun of recursions (Tjaz used to say that I'm old school) but in this case it can be written like:

module_name, frid = git_utils.get_last_rendered_functionality(self.module_build_folder)
      if module_name == self.module_name:
          return module_name, frid

      for required in reversed(self.required_modules):
          module_name, frid = required.get_module_render_status()
          if module_name is not None and frid is not None
              return module_name, frid

      return None, None

@zanjonke zanjonke merged commit b9e0721 into main May 15, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants