Skip to content

Commit ad2f838

Browse files
committed
improve error output after rendering failed
1 parent 0076da7 commit ad2f838

17 files changed

Lines changed: 205 additions & 222 deletions

codeplain_REST_api.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Optional
33

44
import requests
5-
from requests.exceptions import ConnectionError, Timeout
5+
from requests.exceptions import ConnectionError, RequestException, Timeout
66

77
import plain2code_exceptions
88
from plain2code_state import RunState
@@ -76,23 +76,41 @@ def _handle_retry_logic(
7676
if not silent:
7777
self.console.error(f"Max retries ({num_retries}) exceeded. Last error: {error}")
7878
if is_connection_error:
79-
raise plain2code_exceptions.NetworkConnectionError("Failed to connect to API server.")
80-
else:
81-
raise error
79+
raise plain2code_exceptions.NetworkConnectionError(
80+
"Connection error: Failed to connect to API server.\n\nPlease check that your internet connection is working."
81+
)
82+
if isinstance(error, RequestException):
83+
raise Exception(f"Error rendering plain code: {error}\n") from error
84+
raise error
8285

8386
def _raise_for_error_code(self, response_json):
8487
"""Raise appropriate exception based on error code in response."""
8588
error_code = response_json.get("error_code")
8689
if error_code not in ERROR_CODE_EXCEPTIONS:
8790
return
8891

89-
exception_class = ERROR_CODE_EXCEPTIONS[error_code]
9092
message = response_json.get("message", "")
9193

92-
# FunctionalRequirementTooComplex has an extra parameter
9394
if error_code == "FunctionalRequirementTooComplex":
94-
raise exception_class(message, response_json.get("proposed_breakdown"))
95-
95+
raise plain2code_exceptions.FunctionalRequirementTooComplex(
96+
message, response_json.get("proposed_breakdown")
97+
)
98+
if error_code == "MissingResource":
99+
raise plain2code_exceptions.MissingResource(f"Missing resource: {message}\n")
100+
if error_code == "ConflictingRequirements":
101+
raise plain2code_exceptions.ConflictingRequirements(f"Conflicting requirements: {message}\n")
102+
if error_code == "CreditBalanceTooLow":
103+
raise plain2code_exceptions.RenderingCreditBalanceTooLow(f"Credit balance too low: {message}\n")
104+
if error_code == "LLMInternalError":
105+
raise plain2code_exceptions.LLMInternalError(f"LLM internal error: {message}\n")
106+
if error_code == "PlainSyntaxError":
107+
raise plain2code_exceptions.plain_syntax_error(message or "Unknown error")
108+
if error_code == "InternalServerError":
109+
raise plain2code_exceptions.InternalServerError(
110+
"Internal server error.\n\n"
111+
f"Please report the error to support@codeplain.ai with the attached {self.log_file_name} file."
112+
)
113+
exception_class = ERROR_CODE_EXCEPTIONS[error_code]
96114
raise exception_class(message)
97115

98116
def post_request(
@@ -118,7 +136,7 @@ def post_request(
118136
response_json = response.json()
119137
except requests.exceptions.JSONDecodeError as e:
120138
self.console.debug(f"Failed to decode JSON response: {e}. Response text: {response.text}")
121-
raise
139+
raise Exception(f"Error rendering plain code: Failed to decode API response ({e}).\n") from e
122140

123141
if response.status_code == requests.codes.bad_request and "error_code" in response_json:
124142
self._raise_for_error_code(response_json)

concept_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def sort_definitions(definitions: list[dict]) -> list[dict]:
206206
msg += "\n".join(cyclic_definitions)
207207
msg += "\n"
208208

209-
raise PlainSyntaxError(msg)
209+
raise PlainSyntaxError(f"Plain syntax error: {msg}")
210210

211211
order = list(nx.topological_sort(concept_graph))
212212
if len(order) > 0:

file_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ def load_linked_resources(template_dirs: list[str], resources_list):
239239
content = open_from(template_dirs, file_name)
240240

241241
if content is None:
242-
raise FileNotFoundError(f"""
242+
raise FileNotFoundError(f"""File not found:
243243
Resource file {file_name} not found. Resource files are searched in the following order (highest to lowest precedence):
244244
245245
1. The directory containing your .plain file

module_renderer.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,18 @@ def _ensure_module_folders_exist(self, module_name: str, first_render_frid: str)
6161

6262
if not os.path.exists(build_folder_path):
6363
raise MissingPreviousFunctionalitiesError(
64+
"Error rendering plain code: "
6465
f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the source code folder does not exist.\n\n"
6566
f"To fix this, please render the module from the beginning by running:\n"
66-
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}"
67+
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}\n"
6768
)
6869

6970
if not os.path.exists(conformance_tests_path) and self.args.render_conformance_tests:
7071
raise MissingPreviousFunctionalitiesError(
72+
"Error rendering plain code: "
7173
f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the conformance tests folder does not exist.\n\n"
7274
f"To fix this, please render the module from the beginning by running:\n"
73-
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}"
75+
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}\n"
7476
)
7577

7678
return build_folder_path, conformance_tests_path
@@ -99,18 +101,20 @@ def _ensure_frid_commit_exists(
99101
# Check in build folder
100102
if not git_utils.has_commit_for_frid(build_folder_path, frid, module_name):
101103
raise MissingPreviousFunctionalitiesError(
104+
"Error rendering plain code: "
102105
f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the implementation of the previous functionality ({frid}) hasn't been completed yet.\n\n"
103106
f"To fix this, please render the missing functionality ({frid}) first by running:\n"
104-
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}"
107+
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}\n"
105108
)
106109

107110
# Check in conformance tests folder (only if conformance tests are enabled)
108111
if self.args.render_conformance_tests:
109112
if not git_utils.has_commit_for_frid(conformance_tests_path, frid, module_name):
110113
raise MissingPreviousFunctionalitiesError(
114+
"Error rendering plain code: "
111115
f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the conformance tests for the previous functionality ({frid}) haven't been completed yet.\n\n"
112116
f"To fix this, please render the missing functionality ({frid}) first by running:\n"
113-
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}"
117+
f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}\n"
114118
)
115119

116120
def _ensure_previous_frid_commits_exist(

plain2code.py

Lines changed: 46 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
import yaml
1212
from liquid2.exceptions import TemplateNotFoundError
13-
from requests.exceptions import RequestException
1413

1514
import codeplain_REST_api as codeplain_api
1615
import file_utils
@@ -23,11 +22,8 @@
2322
from plain2code_events import RenderFailed
2423
from plain2code_exceptions import (
2524
ConflictingRequirements,
26-
InternalClientError,
27-
InternalServerError,
2825
InvalidAPIKey,
2926
InvalidFridArgument,
30-
LLMInternalError,
3127
MissingAPIKey,
3228
MissingPreviousFunctionalitiesError,
3329
MissingResource,
@@ -58,15 +54,20 @@
5854
def print_exit_summary(
5955
run_state: RunState,
6056
spec_filename: str,
57+
error_message: Optional[str] = None,
6158
) -> None:
6259
console.quiet = False
6360
"""Print render outcome after the TUI exits (terminal restored)."""
61+
6462
msg = "\n[#79FC96]✓ rendering completed\n\n" if run_state.render_succeeded else "[#FF6B6B]✗ rendering failed\n\n"
6563
msg += f" [#8E8F91]render id:\t\t\t[#FFFFFF]{run_state.render_id}\n"
6664
msg += f" [#8E8F91]input file:\t\t\t[#FFFFFF]{spec_filename}\n"
6765
msg += f" [#8E8F91]generated code folder:\t[#FFFFFF]{run_state.render_generated_code_path or '-'}\n\n"
6866
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_accumulated)}\n"
6967
console.info(msg)
68+
69+
if not run_state.render_succeeded and error_message:
70+
console.error(error_message)
7071
console.quiet = True
7172

7273

@@ -104,20 +105,20 @@ def _get_frids_range(plain_source, start, end=None):
104105
start = str(start)
105106

106107
if start not in frids:
107-
raise InvalidFridArgument(f"Invalid start functionality ID: {start}. Valid IDs are: {frids}.")
108+
raise InvalidFridArgument(f"Invalid start functionality ID: {start}. Valid IDs are: {frids}.\n")
108109

109110
if end is not None:
110111
end = str(end)
111112
if end not in frids:
112-
raise InvalidFridArgument(f"Invalid end functionality ID: {end}. Valid IDs are: {frids}.")
113+
raise InvalidFridArgument(f"Invalid end functionality ID: {end}. Valid IDs are: {frids}.\n")
113114

114115
end_idx = frids.index(end) + 1
115116
else:
116117
end_idx = len(frids)
117118

118119
start_idx = frids.index(start)
119120
if start_idx >= end_idx:
120-
raise InvalidFridArgument(f"Start functionality ID: {start} must be before end functionality ID: {end}.")
121+
raise InvalidFridArgument(f"Start functionality ID: {start} must be before end functionality ID: {end}.\n")
121122

122123
return frids[start_idx:end_idx]
123124

@@ -186,16 +187,17 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI):
186187

187188
if not response.get("api_key_valid", False):
188189
raise InvalidAPIKey(
189-
"Provided API key is invalid. Please provide a valid API key using the CODEPLAIN_API_KEY environment variable "
190-
"or the --api-key argument."
190+
"The provided API key is invalid. Please provide a valid API key using the CODEPLAIN_API_KEY environment variable "
191+
"or the --api-key argument.\n"
191192
)
192193

193194
if not response.get("client_version_valid", False):
194195
min_version = response.get("min_client_version", "unknown")
195196
raise OutdatedClientVersion(
197+
"Outdated client version: "
196198
f"Your client version ({system_config.client_version}) is outdated. Minimum required version is {min_version}. "
197199
"Please update using:"
198-
" uv tool upgrade codeplain"
200+
" uv tool upgrade codeplain\n"
199201
)
200202

201203

@@ -317,84 +319,48 @@ def main(): # noqa: C901
317319
setup_logging(args, event_bus, run_state, args.log_to_file, args.log_file_name, args.filename, args.headless)
318320

319321
exc_info = None
322+
error_message = None
323+
320324
try:
321325
# Validate API key is present
322326
if not args.api_key:
323327
raise MissingAPIKey(
324-
"API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument."
328+
"Your API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n"
325329
)
326330
render(args, run_state, event_bus)
327-
except InvalidFridArgument as e:
328-
exc_info = sys.exc_info()
329-
console.error(f"Invalid FRID argument: {str(e)}.\n")
330-
except FileNotFoundError as e:
331-
exc_info = sys.exc_info()
332-
console.error(f"File not found: {str(e)}\n")
333-
console.debug(f"Render ID: {run_state.render_id}")
334-
except TemplateNotFoundError as e:
335-
exc_info = sys.exc_info()
336-
console.error(f"Template not found: {str(e)}\n")
337-
console.error(system_config.get_error_message("template_not_found"))
338-
except PlainSyntaxError as e:
339-
exc_info = sys.exc_info()
340-
console.error(f"Plain syntax error: {str(e)}\n")
341-
except KeyboardInterrupt:
342-
exc_info = sys.exc_info()
343-
console.error("Keyboard interrupt")
344-
console.debug(f"Render ID: {run_state.render_id}")
345-
except RequestException as e:
346-
exc_info = sys.exc_info()
347-
console.error(f"Error rendering plain code: {str(e)}\n")
348-
console.debug(f"Render ID: {run_state.render_id}")
349-
except MissingPreviousFunctionalitiesError as e:
350-
exc_info = sys.exc_info()
351-
console.error(f"Error rendering plain code: {str(e)}\n")
352-
console.debug(f"Render ID: {run_state.render_id}")
353-
except MissingAPIKey as e:
354-
console.error(f"Missing API key: {str(e)}\n")
355-
except InvalidAPIKey as e:
356-
console.error(f"Invalid API key: {str(e)}\n")
357-
except OutdatedClientVersion as e:
358-
console.error(f"Outdated client version: {str(e)}\n")
359-
except (InternalServerError, InternalClientError):
360-
exc_info = sys.exc_info()
361-
console.error(
362-
f"Internal server error.\n\nPlease report the error to support@codeplain.ai with the attached {args.log_file_name} file."
363-
)
364-
console.debug(f"Render ID: {run_state.render_id}")
365-
except ConflictingRequirements as e:
366-
exc_info = sys.exc_info()
367-
console.error(f"Conflicting requirements: {str(e)}\n")
368-
console.debug(f"Render ID: {run_state.render_id}")
369-
except RenderingCreditBalanceTooLow as e:
370-
exc_info = sys.exc_info()
371-
console.error(f"Credit balance too low: {str(e)}\n")
372-
console.debug(f"Render ID: {run_state.render_id}")
373-
except LLMInternalError as e:
374-
exc_info = sys.exc_info()
375-
console.error(f"LLM internal error: {str(e)}\n")
376-
console.debug(f"Render ID: {run_state.render_id}")
377-
except MissingResource as e:
378-
exc_info = sys.exc_info()
379-
console.error(f"Missing resource: {str(e)}\n")
380-
console.debug(f"Render ID: {run_state.render_id}")
381-
except NetworkConnectionError as e:
382-
exc_info = sys.exc_info()
383-
console.error(f"Connection error: {str(e)}\n")
384-
console.error("Please check that your internet connection is working.")
385-
except ModuleDoesNotExistError as e:
386-
exc_info = sys.exc_info()
387-
console.error(f"Module does not exist: {str(e)}\n")
388-
console.debug(f"Render ID: {run_state.render_id}")
389-
except Exception as e:
390-
exc_info = sys.exc_info()
391-
console.error(f"Error rendering plain code: {str(e)}\n")
392-
console.debug(f"Render ID: {run_state.render_id}")
331+
except BaseException as e:
332+
if isinstance(e, KeyboardInterrupt):
333+
error_message = "Keyboard interrupt"
334+
else:
335+
error_message = str(e) if str(e) else repr(e)
336+
337+
if not isinstance(
338+
e,
339+
(
340+
InvalidFridArgument,
341+
FileNotFoundError,
342+
MissingResource,
343+
TemplateNotFoundError,
344+
PlainSyntaxError,
345+
MissingPreviousFunctionalitiesError,
346+
MissingAPIKey,
347+
InvalidAPIKey,
348+
OutdatedClientVersion,
349+
ConflictingRequirements,
350+
RenderingCreditBalanceTooLow,
351+
NetworkConnectionError,
352+
ModuleDoesNotExistError,
353+
),
354+
):
355+
exc_info = sys.exc_info()
393356
finally:
394-
print_exit_summary(run_state, args.filename)
357+
print_exit_summary(
358+
run_state,
359+
args.filename,
360+
error_message=error_message,
361+
)
395362
if exc_info:
396-
# Log traceback using the logging system
397-
logging.error("Render crashed with exception:", exc_info=exc_info)
363+
# Log traceback
398364
dump_crash_logs(args)
399365

400366

plain2code_arguments.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def process_test_script_path(script_arg_name, config):
3131
if isinstance(script_input_path, str) and script_input_path.startswith("/"):
3232
if not os.path.exists(script_input_path):
3333
raise FileNotFoundError(
34-
f"Path for {script_arg_name} not found: {script_input_path}. Set it to the absolute path or relative to the config file."
34+
f"File not found: Path for {script_arg_name} not found: {script_input_path}. Set it to the absolute path or relative to the config file.\n"
3535
)
3636
return config
3737

@@ -47,7 +47,7 @@ def process_test_script_path(script_arg_name, config):
4747
setattr(config, script_arg_name, renderer_relative_path)
4848
else:
4949
raise FileNotFoundError(
50-
f"Path for {script_arg_name} not found: {script_input_path}. Set it to the absolute path or relative to the config file."
50+
f"File not found: Path for {script_arg_name} not found: {script_input_path}. Set it to the absolute path or relative to the config file.\n"
5151
)
5252
return config
5353

plain2code_nodes.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,20 @@ def render_to_output(self, context: RenderContext, buffer: TextIO) -> int:
2929
try:
3030
template = context.env.get_template(str(name), context=context, tag=self.tag, whitespaces=whitespaces)
3131
except TemplateNotFoundError as err:
32-
err.token = self.name.token
33-
err.template_name = context.template.full_name()
34-
raise
32+
detail = str(err)
33+
long_message = f"""Template not found: {detail}
34+
The required template could not be found. Templates are searched in the following order (highest to lowest precedence):
35+
36+
1. The directory containing your .plain file
37+
2. The directory specified by --template-dir (if provided)
38+
3. The built-in 'standard_template_library' directory
39+
40+
Please ensure that the missing template exists in one of these locations, or specify the correct --template-dir if using custom templates.
41+
"""
42+
wrapped = TemplateNotFoundError(long_message)
43+
wrapped.token = self.name.token
44+
wrapped.template_name = context.template.full_name()
45+
raise wrapped from err
3546

3647
namespace: dict[str, object] = dict(arg.evaluate(context) for arg in self.args)
3748

0 commit comments

Comments
 (0)