Skip to content
1 change: 1 addition & 0 deletions module_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,5 @@ def render_module(self) -> None:
last_module_name = self.filename.replace(plain_file.PLAIN_SOURCE_FILE_EXTENSION, "")
rendered_code_path = f"{os.path.join(self.args.build_folder, last_module_name)}/"

self.run_state.set_render_generated_code_path(rendered_code_path)
self.event_bus.publish(RenderCompleted(rendered_code_path=rendered_code_path))
124 changes: 70 additions & 54 deletions plain2code.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import signal
import sys
import threading
import traceback
from pathlib import Path
from types import TracebackType
from typing import Optional

import yaml
Expand Down Expand Up @@ -47,14 +49,38 @@
get_log_file_path,
)
from plain2code_state import RunState
from plain2code_utils import print_dry_run_output
from plain2code_utils import format_duration_hms, print_dry_run_output
from system_config import system_config
from tui.plain2code_tui import Plain2CodeTUI

DEFAULT_TEMPLATE_DIRS = importlib.resources.files("standard_template_library")
RENDER_THREAD_SHUTDOWN_TIMEOUT = 0.7


def print_exit_summary(
run_state: RunState,
spec_filename: str,
error_message: Optional[str] = None,
verbose: bool = False,
exc_info: Optional[tuple[type[BaseException] | None, BaseException | None, TracebackType | None]] = None,
) -> None:
console.quiet = False
"""Print render outcome after the TUI exits (terminal restored)."""

msg = "\n[#79FC96]✓ rendering completed\n\n" if run_state.render_succeeded else "[#FF6B6B]✗ rendering failed\n\n"
msg += f" [#8E8F91]render id:\t\t\t[#FFFFFF]{run_state.render_id}\n"
msg += f" [#8E8F91]input file:\t\t\t[#FFFFFF]{spec_filename}\n"
msg += f" [#8E8F91]generated code folder:\t[#FFFFFF]{run_state.render_generated_code_path or '-'}\n\n"
msg += f"[#8E8F91]functionalities [#FFFFFF]{run_state.rendered_functionalities} [#8E8F91]used credits [#FFFFFF]{run_state.rendered_functionalities} [#8E8F91]render time [#FFFFFF]{format_duration_hms(run_state.render_time)}\n"
console.info(msg)

if not run_state.render_succeeded and error_message:
console.error(error_message)
if verbose and exc_info and exc_info[0] is not None:
console.error("".join(traceback.format_exception(*exc_info)))
console.quiet = True


def get_render_range(render_range, plain_source):
render_range = render_range.split(",")
range_end = render_range[1] if len(render_range) == 2 else render_range[0]
Expand Down Expand Up @@ -186,8 +212,6 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI):
def render(args, run_state: RunState, event_bus: EventBus): # noqa: C901
template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)

console.info(f"Rendering {args.filename} to target code.")

# Compute render range from either --render-range or --render-from
render_range = None
if args.render_range or args.render_from:
Expand Down Expand Up @@ -219,22 +243,26 @@ def render(args, run_state: RunState, event_bus: EventBus): # noqa: C901
)

render_error: list[Exception] = []
run_state.render_succeeded = False

def run_render():
try:
module_renderer.render_module()
console.info(f"[#79FC96]Render {run_state.render_id} completed successfully.[/#79FC96]")
run_state.set_render_succeeded(True)
except RenderCancelledError:
pass # TUI already closed, nothing to report
except Exception as e:
run_state.set_render_succeeded(False)
render_error.append(e)
event_bus.publish(RenderFailed(error_message=str(e)))

if args.headless:
console.info(f"Render started. Render ID: {run_state.render_id}")
try:
module_renderer.render_module()
run_state.set_render_succeeded(True)
except RenderCancelledError:
run_state.set_render_succeeded(False)
pass
return
else:
Expand All @@ -251,6 +279,7 @@ def run_render():
css_path="styles.css",
)
app.run()

stop_event.set()
render_thread.join(timeout=RENDER_THREAD_SHUTDOWN_TIMEOUT)

Expand Down Expand Up @@ -298,85 +327,72 @@ def main(): # noqa: C901
setup_logging(args, event_bus, args.log_to_file, args.log_file_name, args.filename, args.headless)

exc_info = None
error_message = None

try:
# Validate API key is present
if not args.api_key:
raise MissingAPIKey(
"API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument."
)

console.info(f"Render ID: {run_state.render_id}")
render(args, run_state, event_bus)
except InvalidFridArgument as e:
exc_info = sys.exc_info()
console.error(f"Invalid FRID argument: {str(e)}.\n")
error_message = f"Invalid FRID argument: {str(e)}.\n"
except FileNotFoundError as e:
exc_info = sys.exc_info()
console.error(f"File not found: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
error_message = f"File not found: {str(e)}\n"
except MissingResource as e:
error_message = f"Missing resource: {str(e)}\n"
except TemplateNotFoundError as e:
exc_info = sys.exc_info()
console.error(f"Template not found: {str(e)}\n")
console.error(system_config.get_error_message("template_not_found"))
error_message = f"""Template not found: {str(e)}\n
The required template could not be found. Templates are searched in the following order (highest to lowest precedence):

1. The directory containing your .plain file
2. The directory specified by --template-dir (if provided)
3. The built-in 'standard_template_library' directory

Please ensure that the missing template exists in one of these locations, or specify the correct --template-dir if using custom templates.
"""
except PlainSyntaxError as e:
exc_info = sys.exc_info()
console.error(f"Plain syntax error: {str(e)}\n")
error_message = f"Plain syntax error: {str(e)}\n"
except KeyboardInterrupt:
exc_info = sys.exc_info()
console.error("Keyboard interrupt")
console.debug(f"Render ID: {run_state.render_id}")
error_message = "Keyboard interrupt"
except RequestException as e:
exc_info = sys.exc_info()
console.error(f"Error rendering plain code: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
error_message = f"Error rendering plain code: {str(e)}\n"
except MissingPreviousFunctionalitiesError as e:
exc_info = sys.exc_info()
console.error(f"Error rendering plain code: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
error_message = f"Error rendering plain code: {str(e)}\n"
except MissingAPIKey as e:
console.error(f"Missing API key: {str(e)}\n")
error_message = f"Missing API key: {str(e)}\n"
except InvalidAPIKey as e:
console.error(f"Invalid API key: {str(e)}\n")
error_message = f"Invalid API key: {str(e)}\n"
except OutdatedClientVersion as e:
console.error(f"Outdated client version: {str(e)}\n")
error_message = f"Outdated client version: {str(e)}\n"
except (InternalServerError, InternalClientError):
exc_info = sys.exc_info()
console.error(
f"Internal server error.\n\nPlease report the error to support@codeplain.ai with the attached {args.log_file_name} file."
)
console.debug(f"Render ID: {run_state.render_id}")
error_message = f"Internal server error.\n\nPlease report the error to support@codeplain.ai with the attached {args.log_file_name} file."
except ConflictingRequirements as e:
exc_info = sys.exc_info()
console.error(f"Conflicting requirements: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
error_message = f"Conflicting requirements: {str(e)}\n"
except RenderingCreditBalanceTooLow as e:
exc_info = sys.exc_info()
console.error(f"Credit balance too low: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
error_message = f"Credit balance too low: {str(e)}\n"
except LLMInternalError as e:
exc_info = sys.exc_info()
console.error(f"LLM internal error: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
except MissingResource as e:
exc_info = sys.exc_info()
console.error(f"Missing resource: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
error_message = f"LLM internal error: {str(e)}\n"
except NetworkConnectionError as e:
exc_info = sys.exc_info()
console.error(f"Connection error: {str(e)}\n")
console.error("Please check that your internet connection is working.")
error_message = f"Connection error: {str(e)}\n\nPlease check that your internet connection is working."
except ModuleDoesNotExistError as e:
exc_info = sys.exc_info()
console.error(f"Module does not exist: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
error_message = str(e)
except Exception as e:
exc_info = sys.exc_info()
console.error(f"Error rendering plain code: {str(e)}\n")
console.debug(f"Render ID: {run_state.render_id}")
error_message = f"Error rendering plain code: {str(e)}\n"
finally:
print_exit_summary(
run_state,
args.filename,
error_message=error_message,
verbose=args.verbose,
exc_info=exc_info,
)
if exc_info:
# Log traceback using the logging system
logging.error("Render crashed with exception:", exc_info=exc_info)
# Log traceback
dump_crash_logs(args)


Expand Down
21 changes: 21 additions & 0 deletions plain2code_state.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Contains all state and context information we need for the rendering process."""

import time
import uuid
from typing import Optional

Expand All @@ -9,6 +10,9 @@ class RunState:

def __init__(self, spec_filename: str, replay_with: Optional[str] = None):
self.replay: bool = replay_with is not None
self.render_succeeded: bool = False
self.render_generated_code_path: Optional[str] = None
self.rendered_functionalities: int = 0
if replay_with:
self.render_id: str = replay_with
else:
Expand All @@ -17,6 +21,8 @@ def __init__(self, spec_filename: str, replay_with: Optional[str] = None):
self.call_count: int = 0
self.unittest_batch_id: int = 0
self.frid_render_anaysis: dict[str, str] = {}
self.render_time: int = 0
self.last_render_resume_timestamp: float = time.monotonic()

def increment_call_count(self):
self.call_count += 1
Expand All @@ -27,6 +33,21 @@ def increment_unittest_batch_id(self):
def add_rendering_analysis_for_frid(self, frid, rendering_analysis) -> None:
self.frid_render_anaysis[frid] = rendering_analysis

def set_render_succeeded(self, succeeded: bool):
self.render_succeeded = succeeded

def set_render_generated_code_path(self, generated_code_path: str):
self.render_generated_code_path = generated_code_path

def increment_rendered_functionalities(self):
self.rendered_functionalities += 1

def add_to_render_time(self):
self.render_time += int(time.monotonic() - self.last_render_resume_timestamp)

def set_last_render_resume_timestamp(self):
self.last_render_resume_timestamp = time.monotonic()

def to_dict(self):
return {
"render_id": self.render_id,
Expand Down
16 changes: 16 additions & 0 deletions plain2code_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@
import plain_spec
from plain2code_console import console


def format_duration_hms(total_seconds: int) -> str:
"""Format a duration in seconds as hours, minutes, and seconds (e.g. ``1h 2m 3.45s``, ``45.67s``)."""
if total_seconds < 0:
total_seconds = 0
h = int(total_seconds // 3600)
m = int((total_seconds % 3600) // 60)
s = total_seconds % 60
if h:
return f"{h}h {m}m {s}s"
if m:
return f"{m}m {s}s"
text = f"{s}".rstrip("0").rstrip(".")
return f"{text}s" if text else "0s"


AMBIGUITY_CAUSES = {
"reference_resource_ambiguity": "Ambiguity is in the reference resources",
"definition_ambiguity": "Ambiguity is in the definitions",
Expand Down
6 changes: 0 additions & 6 deletions render_machine/actions/fix_conformance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any |
render_context.conformance_tests_running_context.current_testing_frid
)

if render_context.conformance_tests_running_context.current_testing_frid == render_context.frid_context.frid:
console_message = f"Fixing conformance test for functionality {render_context.conformance_tests_running_context.current_testing_frid} in module {render_context.conformance_tests_running_context.current_testing_module_name}."
else:
console_message = f"While implementing functionality {render_context.frid_context.frid}, conformance tests for functionality {render_context.conformance_tests_running_context.current_testing_frid} in module {render_context.conformance_tests_running_context.current_testing_module_name} broke. Fixing them..."

existing_files, existing_files_content = ImplementationCodeHelpers.fetch_existing_files(
render_context.build_folder
)
Expand Down Expand Up @@ -78,7 +73,6 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any |
style=console.INPUT_STYLE,
)

with console.status(console_message):
[conformance_tests_fixed, response_files] = render_context.codeplain_api.fix_conformance_tests_issue(
render_context.frid_context.frid,
render_context.conformance_tests_running_context.current_testing_frid,
Expand Down
23 changes: 10 additions & 13 deletions render_machine/actions/fix_unit_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,16 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any |
render_context, existing_files_content, "Files sent as input to unit tests fixing:"
)

with console.status(
f"[{console.INFO_STYLE}]Fixing unit tests issue for functionality {render_context.frid_context.frid}...\n"
):
response_files = render_context.codeplain_api.fix_unittests_issue(
render_context.frid_context.frid,
render_context.plain_source_tree,
render_context.frid_context.linked_resources,
existing_files_content,
render_context.module_name,
render_context.get_required_modules_functionalities(),
previous_unittests_issue,
run_state=render_context.run_state,
)
response_files = render_context.codeplain_api.fix_unittests_issue(
render_context.frid_context.frid,
render_context.plain_source_tree,
render_context.frid_context.linked_resources,
existing_files_content,
render_context.module_name,
render_context.get_required_modules_functionalities(),
previous_unittests_issue,
run_state=render_context.run_state,
)

_, changed_files = file_utils.update_build_folder_with_rendered_files(
render_context.build_folder, existing_files, response_files
Expand Down
16 changes: 7 additions & 9 deletions render_machine/actions/refactor_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,13 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any |
existing_files_content,
style=console.INPUT_STYLE,
)
with console.status(
f"[{console.INFO_STYLE}]Refactoring the generated code for functionality {render_context.frid_context.frid}..."
):
response_files = render_context.codeplain_api.refactor_source_files_if_needed(
frid=render_context.frid_context.frid,
files_to_check=render_context.frid_context.changed_files,
existing_files_content=existing_files_content,
run_state=render_context.run_state,
)

response_files = render_context.codeplain_api.refactor_source_files_if_needed(
frid=render_context.frid_context.frid,
files_to_check=render_context.frid_context.changed_files,
existing_files_content=existing_files_content,
run_state=render_context.run_state,
)

if len(response_files) == 0:
if render_context.verbose:
Expand Down
Loading
Loading