diff --git a/codeplain_REST_api.py b/codeplain_REST_api.py index 5b8f7d2..0e52697 100644 --- a/codeplain_REST_api.py +++ b/codeplain_REST_api.py @@ -2,7 +2,7 @@ from typing import Optional import requests -from requests.exceptions import ConnectionError, Timeout +from requests.exceptions import ConnectionError, RequestException, Timeout import plain2code_exceptions from plain2code_state import RunState @@ -76,9 +76,12 @@ def _handle_retry_logic( if not silent: self.console.error(f"Max retries ({num_retries}) exceeded. Last error: {error}") if is_connection_error: - raise plain2code_exceptions.NetworkConnectionError("Failed to connect to API server.") - else: - raise error + raise plain2code_exceptions.NetworkConnectionError( + "Connection error: Failed to connect to API server.\n\nPlease check that your internet connection is working." + ) + if isinstance(error, RequestException): + raise Exception(f"Error rendering plain code: {error}\n") from error + raise error def _raise_for_error_code(self, response_json): """Raise appropriate exception based on error code in response.""" @@ -86,13 +89,28 @@ def _raise_for_error_code(self, response_json): if error_code not in ERROR_CODE_EXCEPTIONS: return - exception_class = ERROR_CODE_EXCEPTIONS[error_code] message = response_json.get("message", "") - # FunctionalRequirementTooComplex has an extra parameter if error_code == "FunctionalRequirementTooComplex": - raise exception_class(message, response_json.get("proposed_breakdown")) - + raise plain2code_exceptions.FunctionalRequirementTooComplex( + message, response_json.get("proposed_breakdown") + ) + if error_code == "MissingResource": + raise plain2code_exceptions.MissingResource(f"Missing resource: {message}\n") + if error_code == "ConflictingRequirements": + raise plain2code_exceptions.ConflictingRequirements(f"Conflicting requirements: {message}\n") + if error_code == "CreditBalanceTooLow": + raise plain2code_exceptions.RenderingCreditBalanceTooLow(f"Credit balance too low: {message}\n") + if error_code == "LLMInternalError": + raise plain2code_exceptions.LLMInternalError(f"LLM internal error: {message}\n") + if error_code == "PlainSyntaxError": + raise plain2code_exceptions.plain_syntax_error(message or "Unknown error") + if error_code == "InternalServerError": + raise plain2code_exceptions.InternalServerError( + "Internal server error.\n\n" + f"Please report the error to support@codeplain.ai with the attached {self.log_file_name} file." + ) + exception_class = ERROR_CODE_EXCEPTIONS[error_code] raise exception_class(message) def post_request( @@ -118,7 +136,7 @@ def post_request( response_json = response.json() except requests.exceptions.JSONDecodeError as e: self.console.debug(f"Failed to decode JSON response: {e}. Response text: {response.text}") - raise + raise Exception(f"Error rendering plain code: Failed to decode API response ({e}).\n") from e if response.status_code == requests.codes.bad_request and "error_code" in response_json: self._raise_for_error_code(response_json) diff --git a/concept_utils.py b/concept_utils.py index 584e508..70791a3 100644 --- a/concept_utils.py +++ b/concept_utils.py @@ -206,7 +206,7 @@ def sort_definitions(definitions: list[dict]) -> list[dict]: msg += "\n".join(cyclic_definitions) msg += "\n" - raise PlainSyntaxError(msg) + raise PlainSyntaxError(f"Plain syntax error: {msg}") order = list(nx.topological_sort(concept_graph)) if len(order) > 0: diff --git a/file_utils.py b/file_utils.py index bb178cc..761d731 100644 --- a/file_utils.py +++ b/file_utils.py @@ -239,7 +239,7 @@ def load_linked_resources(template_dirs: list[str], resources_list): content = open_from(template_dirs, file_name) if content is None: - raise FileNotFoundError(f""" + raise FileNotFoundError(f"""File not found: Resource file {file_name} not found. Resource files are searched in the following order (highest to lowest precedence): 1. The directory containing your .plain file diff --git a/module_renderer.py b/module_renderer.py index 15c1e36..75d65c0 100644 --- a/module_renderer.py +++ b/module_renderer.py @@ -61,16 +61,18 @@ def _ensure_module_folders_exist(self, module_name: str, first_render_frid: str) if not os.path.exists(build_folder_path): raise MissingPreviousFunctionalitiesError( + "Error rendering plain code: " f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the source code folder does not exist.\n\n" f"To fix this, please render the module from the beginning by running:\n" - f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}" + f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}\n" ) if not os.path.exists(conformance_tests_path) and self.args.render_conformance_tests: raise MissingPreviousFunctionalitiesError( + "Error rendering plain code: " f"Cannot start rendering from functionality {first_render_frid} for module '{module_name}' because the conformance tests folder does not exist.\n\n" f"To fix this, please render the module from the beginning by running:\n" - f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}" + f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION}\n" ) return build_folder_path, conformance_tests_path @@ -99,18 +101,20 @@ def _ensure_frid_commit_exists( # Check in build folder if not git_utils.has_commit_for_frid(build_folder_path, frid, module_name): raise MissingPreviousFunctionalitiesError( + "Error rendering plain code: " 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" f"To fix this, please render the missing functionality ({frid}) first by running:\n" - f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}" + f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}\n" ) # Check in conformance tests folder (only if conformance tests are enabled) if self.args.render_conformance_tests: if not git_utils.has_commit_for_frid(conformance_tests_path, frid, module_name): raise MissingPreviousFunctionalitiesError( + "Error rendering plain code: " 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" f"To fix this, please render the missing functionality ({frid}) first by running:\n" - f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}" + f" codeplain {module_name}{plain_file.PLAIN_SOURCE_FILE_EXTENSION} --render-from {frid}\n" ) def _ensure_previous_frid_commits_exist( diff --git a/plain2code.py b/plain2code.py index fd02097..8655d71 100644 --- a/plain2code.py +++ b/plain2code.py @@ -10,7 +10,6 @@ import yaml from liquid2.exceptions import TemplateNotFoundError -from requests.exceptions import RequestException import codeplain_REST_api as codeplain_api import file_utils @@ -23,11 +22,8 @@ from plain2code_events import RenderFailed from plain2code_exceptions import ( ConflictingRequirements, - InternalClientError, - InternalServerError, InvalidAPIKey, InvalidFridArgument, - LLMInternalError, MissingAPIKey, MissingPreviousFunctionalitiesError, MissingResource, @@ -58,15 +54,20 @@ def print_exit_summary( run_state: RunState, spec_filename: str, + error_message: Optional[str] = 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_accumulated)}\n" console.info(msg) + + if not run_state.render_succeeded and error_message: + console.error(error_message) console.quiet = True @@ -104,12 +105,16 @@ def _get_frids_range(plain_source, start, end=None): start = str(start) if start not in frids: - raise InvalidFridArgument(f"Invalid start functionality ID: {start}. Valid IDs are: {frids}.") + raise InvalidFridArgument( + f"Invalid FRID argument: Invalid start functionality ID: {start}. Valid IDs are: {frids}.\n" + ) if end is not None: end = str(end) if end not in frids: - raise InvalidFridArgument(f"Invalid end functionality ID: {end}. Valid IDs are: {frids}.") + raise InvalidFridArgument( + f"Invalid FRID argument: Invalid end functionality ID: {end}. Valid IDs are: {frids}.\n" + ) end_idx = frids.index(end) + 1 else: @@ -117,7 +122,9 @@ def _get_frids_range(plain_source, start, end=None): start_idx = frids.index(start) if start_idx >= end_idx: - raise InvalidFridArgument(f"Start functionality ID: {start} must be before end functionality ID: {end}.") + raise InvalidFridArgument( + f"Invalid FRID argument: Start functionality ID: {start} must be before end functionality ID: {end}.\n" + ) return frids[start_idx:end_idx] @@ -186,16 +193,17 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI): if not response.get("api_key_valid", False): raise InvalidAPIKey( - "Provided API key is invalid. Please provide a valid API key using the CODEPLAIN_API_KEY environment variable " - "or the --api-key argument." + "Invalid API key: Provided API key is invalid. Please provide a valid API key using the CODEPLAIN_API_KEY environment variable " + "or the --api-key argument.\n" ) if not response.get("client_version_valid", False): min_version = response.get("min_client_version", "unknown") raise OutdatedClientVersion( + "Outdated client version: " f"Your client version ({system_config.client_version}) is outdated. Minimum required version is {min_version}. " "Please update using:" - " uv tool upgrade codeplain" + " uv tool upgrade codeplain\n" ) @@ -317,84 +325,48 @@ def main(): # noqa: C901 setup_logging(args, event_bus, run_state, 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." + "Missing API key: API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n" ) render(args, run_state, event_bus) - except InvalidFridArgument as e: - exc_info = sys.exc_info() - console.error(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}") - 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")) - except PlainSyntaxError as e: - exc_info = sys.exc_info() - console.error(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}") - 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}") - 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}") - except MissingAPIKey as e: - console.error(f"Missing API key: {str(e)}\n") - except InvalidAPIKey as e: - console.error(f"Invalid API key: {str(e)}\n") - except OutdatedClientVersion as e: - console.error(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}") - 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}") - 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}") - 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}") - 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.") - 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}") - 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}") + except BaseException as e: + if isinstance(e, KeyboardInterrupt): + error_message = "Keyboard interrupt" + else: + error_message = str(e) if str(e) else repr(e) + + if not isinstance( + e, + ( + InvalidFridArgument, + FileNotFoundError, + MissingResource, + TemplateNotFoundError, + PlainSyntaxError, + MissingPreviousFunctionalitiesError, + MissingAPIKey, + InvalidAPIKey, + OutdatedClientVersion, + ConflictingRequirements, + RenderingCreditBalanceTooLow, + NetworkConnectionError, + ModuleDoesNotExistError, + ), + ): + exc_info = sys.exc_info() finally: - print_exit_summary(run_state, args.filename) + print_exit_summary( + run_state, + args.filename, + error_message=error_message, + ) 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) diff --git a/plain2code_arguments.py b/plain2code_arguments.py index ec8a308..9f6fc1f 100644 --- a/plain2code_arguments.py +++ b/plain2code_arguments.py @@ -31,7 +31,7 @@ def process_test_script_path(script_arg_name, config): if isinstance(script_input_path, str) and script_input_path.startswith("/"): if not os.path.exists(script_input_path): raise FileNotFoundError( - f"Path for {script_arg_name} not found: {script_input_path}. Set it to the absolute path or relative to the config file." + 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" ) return config @@ -47,7 +47,7 @@ def process_test_script_path(script_arg_name, config): setattr(config, script_arg_name, renderer_relative_path) else: raise FileNotFoundError( - f"Path for {script_arg_name} not found: {script_input_path}. Set it to the absolute path or relative to the config file." + 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" ) return config diff --git a/plain2code_nodes.py b/plain2code_nodes.py index 397077d..b3cad37 100644 --- a/plain2code_nodes.py +++ b/plain2code_nodes.py @@ -29,9 +29,20 @@ def render_to_output(self, context: RenderContext, buffer: TextIO) -> int: try: template = context.env.get_template(str(name), context=context, tag=self.tag, whitespaces=whitespaces) except TemplateNotFoundError as err: - err.token = self.name.token - err.template_name = context.template.full_name() - raise + detail = str(err) + long_message = f"""Template not found: {detail} +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. +""" + wrapped = TemplateNotFoundError(long_message) + wrapped.token = self.name.token + wrapped.template_name = context.template.full_name() + raise wrapped from err namespace: dict[str, object] = dict(arg.evaluate(context) for arg in self.args) diff --git a/plain_file.py b/plain_file.py index 5ebd414..9074e37 100644 --- a/plain_file.py +++ b/plain_file.py @@ -80,11 +80,11 @@ def check_section_for_linked_resources(section): parsed_url = urlparse(link.node.target) if parsed_url.scheme != "" or os.path.isabs(link.node.target): raise PlainSyntaxError( - f"Only relative links are allowed (text: {link.node.children[0].content}, target: {link.node.target})." + f"Plain syntax error: Only relative links are allowed (text: {link.node.children[0].content}, target: {link.node.target})." ) if len(link.node.children) != 1: - raise PlainSyntaxError(f"Link must have text specified (link: {link.node.target}).") + raise PlainSyntaxError(f"Plain syntax error: Link must have text specified (link: {link.node.target}).") linked_resources.append({"text": link.node.children[0].content, "target": link.node.target}) @@ -136,7 +136,7 @@ def check_if_functional_requirements_are_specified(plain_source, non_functional_ found_functional_requirements = plain_spec.FUNCTIONAL_REQUIREMENTS in plain_source if found_functional_requirements and len(non_functional_requirements) == 0: - raise PlainSyntaxError("Syntax error: functionality with no implementation reqs specified.") + raise PlainSyntaxError("Plain syntax error: functionality with no implementation reqs specified.") return found_functional_requirements @@ -207,13 +207,13 @@ def _process_single_acceptance_test_requirement(functional_requirement: mistleto # - Writing `acceptance test` instead of `acceptance tests` (or any other syntax diffs). # - Instead of specifying `acceptance tests` below the functionality, creator of the plain file # might have specified some other building block (e.g. `implementation reqs`) - raise PlainSyntaxError(acceptance_test_heading_problem) + raise PlainSyntaxError(f"Plain syntax error: {acceptance_test_heading_problem}") if is_acceptance_test_heading: if acceptance_tests_found_already: # Handle edge case of duplicated ***acceptance tests*** heading raise PlainSyntaxError( - f"Syntax error at line {functional_requirement_child.line_number}: Duplicate 'acceptance tests' heading found within the same functionality. Only one block of acceptance tests is allowed per functionality." + f"Plain syntax error: Syntax error at line {functional_requirement_child.line_number}: Duplicate 'acceptance tests' heading found within the same functionality. Only one block of acceptance tests is allowed per functionality." ) try: @@ -333,7 +333,7 @@ def process_imports( required_concepts = list[str]() for module_name in imports: if module_name in modules_trace: - raise PlainSyntaxError(f"Circular import detected: {module_name}.") + raise PlainSyntaxError(f"Plain syntax error: Circular import detected: {module_name}.") if module_name in imported_modules: continue @@ -343,12 +343,12 @@ def process_imports( ) if check_if_functional_requirements_are_specified(plain_file_parse_result.plain_source, []): - raise PlainSyntaxError("Imported module must not contain functionalities.") + raise PlainSyntaxError("Plain syntax error: Imported module must not contain functionalities.") for specification_heading in plain_file_parse_result.plain_source: if specification_heading not in plain_spec.ALLOWED_IMPORT_SPECIFICATION_HEADINGS: raise PlainSyntaxError( - f"Syntax error: Invalid specification heading (`{specification_heading}`). Allowed headings: {', '.join(plain_spec.ALLOWED_IMPORT_SPECIFICATION_HEADINGS)}" + f"Plain syntax error: Invalid specification heading (`{specification_heading}`). Allowed headings: {', '.join(plain_spec.ALLOWED_IMPORT_SPECIFICATION_HEADINGS)}" ) if plain_source[specification_heading] is None: @@ -377,7 +377,7 @@ def read_plain_source_metadata(plain_source_text): try: plain_source_obj = frontmatter.loads(plain_source_text) except Exception as e: - raise PlainSyntaxError(f"Syntax error: Invalid frontmatter: {e}") + raise PlainSyntaxError(f"Plain syntax error: Invalid frontmatter: {e}") for directive in [EXPORTED_CONCEPTS_DIRECTIVE, REQUIRED_CONCEPTS_DIRECTIVE]: if directive in plain_source_obj.metadata: @@ -393,7 +393,7 @@ def read_plain_source_metadata(plain_source_text): prepared_metadata.append(f"- {item}") else: raise PlainSyntaxError( - f"Syntax error: Invalid {directive} metadata. Expected a dictionary or a string." + f"Plain syntax error: Invalid {directive} metadata. Expected a dictionary or a string." ) plain_source_obj.metadata[directive] = prepared_metadata @@ -446,14 +446,14 @@ def parse_plain_source( # noqa: C901 ): raise PlainSyntaxError( - f"Syntax error at line {token.line_number}: Invalid specification (`{token_text}`)" + f"Plain syntax error: Syntax error at line {token.line_number}: Invalid specification (`{token_text}`)" ) specification_heading = token.children[0].children[0].children[0].content if specification_heading not in plain_spec.ALLOWED_SPECIFICATION_HEADINGS: raise PlainSyntaxError( - f"Syntax error at line {token.line_number}: Invalid specification heading (`{specification_heading}`). Allowed headings: {', '.join(plain_spec.ALLOWED_SPECIFICATION_HEADINGS)}" + f"Plain syntax error: Syntax error at line {token.line_number}: Invalid specification heading (`{specification_heading}`). Allowed headings: {', '.join(plain_spec.ALLOWED_SPECIFICATION_HEADINGS)}" ) if ( @@ -464,12 +464,12 @@ def parse_plain_source( # noqa: C901 if specification_heading in processed_specification_headings: raise PlainSyntaxError( - f"Syntax error at line {token.line_number}: Duplicate specification heading (`{specification_heading}`)" + f"Plain syntax error: Syntax error at line {token.line_number}: Duplicate specification heading (`{specification_heading}`)" ) if specification_heading == plain_spec.DEFINITIONS and current_specification_heading is not None: raise PlainSyntaxError( - f"Syntax error at line {token.line_number}: Definitions specification must be the first specification in the section (`{token_text}`)" + f"Plain syntax error: Syntax error at line {token.line_number}: Definitions specification must be the first specification in the section (`{token_text}`)" ) current_specification_heading = specification_heading @@ -480,13 +480,13 @@ def parse_plain_source( # noqa: C901 elif isinstance(token, List): if current_specification_heading is None: raise PlainSyntaxError( - f"Syntax error at line {token.line_number}: Missing specification heading (`{token_text}`)" + f"Plain syntax error: Syntax error at line {token.line_number}: Missing specification heading (`{token_text}`)" ) plain_source[current_specification_heading].children.extend(token.children) else: raise PlainSyntaxError( - f"Syntax error at line {token.line_number}: Invalid source structure (`{token_text}`)" + f"Plain syntax error: Syntax error at line {token.line_number}: Invalid source structure (`{token_text}`)" ) if plain_source[plain_spec.DEFINITIONS] is not None: @@ -545,7 +545,7 @@ def process_required_modules( exported_definitions = list[mistletoe.block_token.token]() for module_name in required_modules: if module_name in modules_trace: - raise PlainSyntaxError(f"Circular required module detected: {module_name}.") + raise PlainSyntaxError(f"Plain syntax error: Circular required module detected: {module_name}.") if len(all_required_modules) > 0 and module_name == all_required_modules[-1]: continue @@ -560,7 +560,7 @@ def process_required_modules( # In the future we will support the cases where required modules can be rendered independently # and then merged (somehow). raise PlainSyntaxError( - f"There must be a fixed order how required modules are dependent ({module_name})." + f"Plain syntax error: There must be a fixed order how required modules are dependent ({module_name})." ) else: process_required_modules( @@ -576,13 +576,13 @@ def process_required_modules( for concept in plain_file_parse_result.plain_source_obj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]: if concept in concept_utils.DEFAULT_CONCEPTS: raise PlainSyntaxError( - f"Syntax error: Default concept cannot be exported: {concept}. Only user-defined concepts can be exported." + f"Plain syntax error: Default concept cannot be exported: {concept}. Only user-defined concepts can be exported." ) if isinstance(concept, str): exported_concepts.extend(concept_utils.extract_concepts_from_definition(concept)[0]) else: - raise PlainSyntaxError(f"Syntax error: Invalid exported concept: {concept}.") + raise PlainSyntaxError(f"Plain syntax error: Invalid exported concept: {concept}.") with PlainRenderer() as renderer: for exported_concept in exported_concepts: @@ -626,7 +626,7 @@ def plain_file_parser( # noqa: C901 plain_source_file_path = Path(plain_source_file_name) if plain_source_file_path.suffix != PLAIN_SOURCE_FILE_EXTENSION: raise PlainSyntaxError( - f"Invalid plain file extension: {plain_source_file_path.suffix}. Expected: {PLAIN_SOURCE_FILE_EXTENSION}." + f"Plain syntax error: Invalid plain file extension: {plain_source_file_path.suffix}. Expected: {PLAIN_SOURCE_FILE_EXTENSION}." ) module_name = plain_source_file_path.stem @@ -645,11 +645,13 @@ def plain_file_parser( # noqa: C901 missing_required_concepts_msg = "Missing required concepts: " missing_required_concepts_msg += ", ".join(plain_file_parse_result.required_concepts) raise PlainSyntaxError( - f"Syntax error: Not all required concepts were defined. {missing_required_concepts_msg}." + f"Plain syntax error: Not all required concepts were defined. {missing_required_concepts_msg}." ) if not check_if_functional_requirements_are_specified(plain_file_parse_result.plain_source, []): - raise PlainSyntaxError(f"Module '{module_name}' was required but does not contain functional requirements.") + raise PlainSyntaxError( + f"Plain syntax error: Module '{module_name}' was required but does not contain functional requirements." + ) exported_definitions = process_required_modules( plain_file_parse_result.required_modules, @@ -673,7 +675,7 @@ def plain_file_parser( # noqa: C901 if len(validation_errors) > 0: errors_msg = "\n".join(validation_errors) msg = f"Found {len(validation_errors)} errors in the plain file:\n{errors_msg}" - raise PlainSyntaxError(msg) + raise PlainSyntaxError(f"Plain syntax error: {msg}") if plain_spec.DEFINITIONS in marshalled_plain_source: concept_utils.sort_definitions(marshalled_plain_source[plain_spec.DEFINITIONS]) diff --git a/plain_modules.py b/plain_modules.py index e90ef02..e254ffb 100644 --- a/plain_modules.py +++ b/plain_modules.py @@ -91,7 +91,7 @@ def _get_module_functional_requirements(self, plain_source: dict) -> list[str]: def get_functionalities(self) -> dict[str, list[str]]: module_metadata = self.load_module_metadata() if module_metadata is None: - raise ModuleDoesNotExistError(f"Module {self.name} does not exist or has no metadata.") + raise ModuleDoesNotExistError(f"Module {self.name} does not exist or has no metadata.\n") if REQUIRED_MODULES_FUNCTIONALITIES in module_metadata: functionalities = module_metadata[REQUIRED_MODULES_FUNCTIONALITIES] diff --git a/render_machine/actions/analyze_specification_ambiguity.py b/render_machine/actions/analyze_specification_ambiguity.py index 248954f..eaddb87 100644 --- a/render_machine/actions/analyze_specification_ambiguity.py +++ b/render_machine/actions/analyze_specification_ambiguity.py @@ -21,7 +21,7 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | ) if fixed_implementation_code_diff is None: raise InternalClientError( - "Fixes to the implementation code found during conformance testing are not committed to git." + "Internal client error: Fixes to the implementation code found during conformance testing are not committed to git." ) previous_frid = plain_spec.get_previous_frid(render_context.plain_source_tree, render_context.frid_context.frid) diff --git a/render_machine/actions/fix_conformance_test.py b/render_machine/actions/fix_conformance_test.py index 6f436e2..0732b0d 100644 --- a/render_machine/actions/fix_conformance_test.py +++ b/render_machine/actions/fix_conformance_test.py @@ -21,7 +21,9 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any | ) if not previous_action_payload.get("previous_conformance_tests_issue"): - raise InternalClientError("Previous action payload does not contain previous conformance tests issue.") + raise InternalClientError( + "Internal client error: Previous action payload does not contain previous conformance tests issue." + ) previous_conformance_tests_issue = previous_action_payload["previous_conformance_tests_issue"] render_context.conformance_tests_running_context.previous_conformance_tests_issue_old = ( @@ -34,11 +36,6 @@ def execute(self, render_context: RenderContext, previous_action_payload: Any | render_context.conformance_tests_running_context.current_testing_module_name ) - 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 ) @@ -81,7 +78,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, diff --git a/render_machine/actions/fix_unit_tests.py b/render_machine/actions/fix_unit_tests.py index 977a7dd..5a9e10b 100644 --- a/render_machine/actions/fix_unit_tests.py +++ b/render_machine/actions/fix_unit_tests.py @@ -16,7 +16,9 @@ class FixUnitTests(BaseAction): def execute(self, render_context: RenderContext, previous_action_payload: Any | None): if not previous_action_payload.get("previous_unittests_issue"): - raise InternalClientError("Previous action payload does not contain previous unit tests issue.") + raise InternalClientError( + "Internal client error: Previous action payload does not contain previous unit tests issue." + ) previous_unittests_issue = previous_action_payload["previous_unittests_issue"] if previous_unittests_issue and len(previous_unittests_issue) > MAX_ISSUE_LENGTH: @@ -33,19 +35,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 diff --git a/render_machine/actions/refactor_code.py b/render_machine/actions/refactor_code.py index d7ab098..8f65e58 100644 --- a/render_machine/actions/refactor_code.py +++ b/render_machine/actions/refactor_code.py @@ -26,16 +26,14 @@ 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, - module_name=render_context.module_name, - 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, + module_name=render_context.module_name, + 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: diff --git a/render_machine/actions/render_conformance_tests.py b/render_machine/actions/render_conformance_tests.py index 240394f..8ea9c45 100644 --- a/render_machine/actions/render_conformance_tests.py +++ b/render_machine/actions/render_conformance_tests.py @@ -33,22 +33,19 @@ def _render_conformance_tests(self, render_context: RenderContext): ) if not render_context.conformance_tests_running_context.current_conformance_tests_exist(): - with console.status( - f"[{console.INFO_STYLE}]Generating folder name for conformance tests for functionality {render_context.conformance_tests_running_context.current_testing_frid}...\n" - ): - fr_subfolder_name = render_context.codeplain_api.generate_folder_name_from_functional_requirement( - frid=render_context.conformance_tests_running_context.current_testing_frid, - module_name=render_context.conformance_tests_running_context.current_testing_module_name, - functional_requirement=render_context.conformance_tests_running_context.current_testing_frid_specifications[ - plain_spec.FUNCTIONAL_REQUIREMENTS - ][ - -1 - ], - existing_folder_names=render_context.conformance_tests.fetch_existing_conformance_test_folder_names( - render_context.conformance_tests_running_context.current_testing_module_name - ), - run_state=render_context.run_state, - ) + fr_subfolder_name = render_context.codeplain_api.generate_folder_name_from_functional_requirement( + frid=render_context.conformance_tests_running_context.current_testing_frid, + module_name=render_context.conformance_tests_running_context.current_testing_module_name, + functional_requirement=render_context.conformance_tests_running_context.current_testing_frid_specifications[ + plain_spec.FUNCTIONAL_REQUIREMENTS + ][ + -1 + ], + existing_folder_names=render_context.conformance_tests.fetch_existing_conformance_test_folder_names( + render_context.conformance_tests_running_context.current_testing_module_name + ), + run_state=render_context.run_state, + ) conformance_tests_folder_name = os.path.join( render_context.conformance_tests.get_module_conformance_tests_folder(render_context.module_name), @@ -96,25 +93,23 @@ def _render_conformance_tests(self, render_context: RenderContext): ) all_acceptance_tests = render_context.frid_context.specifications.get(plain_spec.ACCEPTANCE_TESTS, []) - with console.status( - f"[{console.INFO_STYLE}]Rendering conformance test for functionality {render_context.conformance_tests_running_context.current_testing_frid}...\n" - ): - response_files, implementation_plan_summary = render_context.codeplain_api.render_conformance_tests( - render_context.frid_context.frid, - render_context.conformance_tests_running_context.current_testing_frid, - render_context.plain_source_tree, - render_context.frid_context.linked_resources, - existing_files_content, - memory_files_content, - render_context.module_name, - render_context.get_required_modules_functionalities(), - conformance_tests_folder_name, - render_context.conformance_tests_running_context.get_conformance_tests_json( - render_context.conformance_tests_running_context.current_testing_module_name - ), - all_acceptance_tests, - run_state=render_context.run_state, - ) + + response_files, implementation_plan_summary = render_context.codeplain_api.render_conformance_tests( + render_context.frid_context.frid, + render_context.conformance_tests_running_context.current_testing_frid, + render_context.plain_source_tree, + render_context.frid_context.linked_resources, + existing_files_content, + memory_files_content, + render_context.module_name, + render_context.get_required_modules_functionalities(), + conformance_tests_folder_name, + render_context.conformance_tests_running_context.get_conformance_tests_json( + render_context.conformance_tests_running_context.current_testing_module_name + ), + all_acceptance_tests, + run_state=render_context.run_state, + ) render_context.conformance_tests_running_context.current_testing_frid_high_level_implementation_plan = ( implementation_plan_summary @@ -152,21 +147,18 @@ def _render_acceptance_test(self, render_context: RenderContext): if render_context.verbose: console.info(f"Generating acceptance test:\n {acceptance_test}") - with console.status( - f"[{console.INFO_STYLE}]Generating acceptance test for functionality {render_context.frid_context.frid}...\n" - ): - response_files = render_context.codeplain_api.render_acceptance_tests( - render_context.frid_context.frid, - render_context.plain_source_tree, - render_context.frid_context.linked_resources, - existing_files_content, - memory_files_content, - conformance_tests_files_content, - render_context.module_name, - render_context.get_required_modules_functionalities(), - acceptance_test, - run_state=render_context.run_state, - ) + response_files = render_context.codeplain_api.render_acceptance_tests( + render_context.frid_context.frid, + render_context.plain_source_tree, + render_context.frid_context.linked_resources, + existing_files_content, + memory_files_content, + conformance_tests_files_content, + render_context.module_name, + render_context.get_required_modules_functionalities(), + acceptance_test, + run_state=render_context.run_state, + ) conformance_tests_folder_name = ( render_context.conformance_tests_running_context.get_current_conformance_test_folder_name() ) diff --git a/render_machine/actions/summarize_conformance_tests.py b/render_machine/actions/summarize_conformance_tests.py index e400d12..14e4d71 100644 --- a/render_machine/actions/summarize_conformance_tests.py +++ b/render_machine/actions/summarize_conformance_tests.py @@ -20,18 +20,15 @@ def execute(self, render_context: RenderContext, _previous_action_payload: Any | ) ) - with console.status( - f"[{console.INFO_STYLE}]Summarizing finished conformance tests for functionality {render_context.frid_context.frid}...\n" - ): - summary = render_context.codeplain_api.summarize_finished_conformance_tests( - frid=render_context.frid_context.frid, - plain_source_tree=render_context.plain_source_tree, - linked_resources=render_context.frid_context.linked_resources, - conformance_test_files_content=existing_conformance_test_files_content, - module_name=render_context.module_name, - required_modules=render_context.get_required_modules_functionalities(), - run_state=render_context.run_state, - ) + summary = render_context.codeplain_api.summarize_finished_conformance_tests( + frid=render_context.frid_context.frid, + plain_source_tree=render_context.plain_source_tree, + linked_resources=render_context.frid_context.linked_resources, + conformance_test_files_content=existing_conformance_test_files_content, + module_name=render_context.module_name, + required_modules=render_context.get_required_modules_functionalities(), + run_state=render_context.run_state, + ) render_context.conformance_tests_running_context.set_conformance_tests_summary(summary) diff --git a/tests/test_plainfile.py b/tests/test_plainfile.py index d4e70fd..24b2bcd 100644 --- a/tests/test_plainfile.py +++ b/tests/test_plainfile.py @@ -258,7 +258,7 @@ def test_psart_with_duplicate_acceptance_test_heading(): ] # Call the function under test and expect a PlainSyntaxError about duplicate headings - expected_error_message = f"Syntax error at line {mock_at_heading_paragraph2.line_number}: Duplicate 'acceptance tests' heading found within the same functionality. Only one block of acceptance tests is allowed per functionality." + expected_error_message = f"Plain syntax error: Syntax error at line {mock_at_heading_paragraph2.line_number}: Duplicate 'acceptance tests' heading found within the same functionality. Only one block of acceptance tests is allowed per functionality." with pytest.raises(PlainSyntaxError) as exc_info: plain_file._process_single_acceptance_test_requirement(functional_requirement_mock) diff --git a/tests/test_plainfileparser.py b/tests/test_plainfileparser.py index 90d6ddf..a27408c 100644 --- a/tests/test_plainfileparser.py +++ b/tests/test_plainfileparser.py @@ -176,7 +176,7 @@ def test_duplicate_specification_heading(get_test_data_path): def test_missing_non_functional_requirements(get_test_data_path): with pytest.raises( Exception, - match="Syntax error: functionality with no implementation reqs specified.", + match="Plain syntax error: functionality with no implementation reqs specified.", ): plain_file.plain_file_parser( "missing_non_functional_requirements.plain", @@ -187,7 +187,7 @@ def test_missing_non_functional_requirements(get_test_data_path): def test_without_non_functional_requirement(get_test_data_path): with pytest.raises( Exception, - match="Syntax error: functionality with no implementation reqs specified.", + match="Plain syntax error: functionality with no implementation reqs specified.", ): plain_file.plain_file_parser( "without_non_functional_requirement.plain",