codeplain 0.2.3__py3-none-any.whl → 0.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/METADATA +3 -3
  2. {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/RECORD +43 -43
  3. codeplain_REST_api.py +57 -43
  4. config/system_config.yaml +1 -16
  5. file_utils.py +5 -4
  6. git_utils.py +43 -13
  7. memory_management.py +1 -1
  8. module_renderer.py +114 -0
  9. plain2code.py +91 -30
  10. plain2code_console.py +3 -5
  11. plain2code_exceptions.py +26 -6
  12. plain2code_logger.py +11 -5
  13. plain2code_utils.py +7 -5
  14. plain_file.py +15 -37
  15. plain_modules.py +1 -4
  16. plain_spec.py +24 -6
  17. render_machine/actions/create_dist.py +1 -1
  18. render_machine/actions/exit_with_error.py +1 -1
  19. render_machine/actions/prepare_testing_environment.py +1 -1
  20. render_machine/actions/render_conformance_tests.py +2 -4
  21. render_machine/actions/render_functional_requirement.py +6 -6
  22. render_machine/actions/run_conformance_tests.py +3 -2
  23. render_machine/actions/run_unit_tests.py +1 -1
  24. render_machine/render_context.py +3 -3
  25. render_machine/render_utils.py +14 -6
  26. standard_template_library/golang-console-app-template.plain +2 -2
  27. standard_template_library/python-console-app-template.plain +2 -2
  28. standard_template_library/typescript-react-app-template.plain +2 -2
  29. system_config.py +3 -11
  30. tests/test_imports.py +2 -2
  31. tests/test_plainfile.py +2 -2
  32. tests/test_plainfileparser.py +10 -10
  33. tests/test_plainspec.py +2 -2
  34. tests/test_requires.py +2 -1
  35. tui/components.py +311 -103
  36. tui/plain2code_tui.py +101 -52
  37. tui/state_handlers.py +94 -47
  38. tui/styles.css +240 -52
  39. tui/widget_helpers.py +43 -47
  40. {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/WHEEL +0 -0
  41. {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/entry_points.txt +0 -0
  42. {codeplain-0.2.3.dist-info → codeplain-0.2.5.dist-info}/licenses/LICENSE +0 -0
  43. /spinner.py → /tui/spinner.py +0 -0
plain2code.py CHANGED
@@ -2,7 +2,7 @@ import importlib.resources
2
2
  import logging
3
3
  import logging.config
4
4
  import os
5
- import traceback
5
+ import sys
6
6
  from typing import Optional
7
7
 
8
8
  import yaml
@@ -17,7 +17,20 @@ from event_bus import EventBus
17
17
  from module_renderer import ModuleRenderer
18
18
  from plain2code_arguments import parse_arguments
19
19
  from plain2code_console import console
20
- from plain2code_exceptions import MissingAPIKey, PlainSyntaxError
20
+ from plain2code_exceptions import (
21
+ ConflictingRequirements,
22
+ CreditBalanceTooLow,
23
+ InternalServerError,
24
+ InvalidAPIKey,
25
+ InvalidFridArgument,
26
+ LLMInternalError,
27
+ MissingAPIKey,
28
+ MissingPreviousFunctionalitiesError,
29
+ MissingResource,
30
+ OutdatedClientVersion,
31
+ PlainSyntaxError,
32
+ UnexpectedState,
33
+ )
21
34
  from plain2code_logger import (
22
35
  CrashLogHandler,
23
36
  IndentedFormatter,
@@ -46,10 +59,6 @@ UNRECOVERABLE_ERROR_EXIT_CODES = [69]
46
59
  TIMEOUT_ERROR_EXIT_CODE = 124
47
60
 
48
61
 
49
- class InvalidFridArgument(Exception):
50
- pass
51
-
52
-
53
62
  def get_render_range(render_range, plain_source):
54
63
  render_range = render_range.split(",")
55
64
  range_end = render_range[1] if len(render_range) == 2 else render_range[0]
@@ -93,6 +102,7 @@ def setup_logging(
93
102
  log_to_file: bool,
94
103
  log_file_name: str,
95
104
  plain_file_path: Optional[str],
105
+ render_id: str,
96
106
  ):
97
107
  # Set default level to INFO for everything not explicitly configured
98
108
  logging.getLogger().setLevel(logging.INFO)
@@ -152,11 +162,29 @@ def setup_logging(
152
162
  crash_handler.setFormatter(formatter)
153
163
  root_logger.addHandler(crash_handler)
154
164
 
165
+ root_logger.info(f"Render ID: {render_id}") # Ensure render ID is logged in to codeplain.log file
155
166
 
156
- def render(args, run_state: RunState, codeplain_api, event_bus: EventBus): # noqa: C901
157
- # Check system requirements before proceeding
158
- system_config.verify_requirements()
159
167
 
168
+ def _check_connection(codeplainAPI):
169
+ """Check API connectivity and validate API key and client version."""
170
+ response = codeplainAPI.connection_check(system_config.client_version)
171
+
172
+ if not response.get("api_key_valid", False):
173
+ raise InvalidAPIKey(
174
+ "Provided API key is invalid. Please provide a valid API key using the CODEPLAIN_API_KEY environment variable "
175
+ "or the --api-key argument."
176
+ )
177
+
178
+ if not response.get("client_version_valid", False):
179
+ min_version = response.get("min_client_version", "unknown")
180
+ raise OutdatedClientVersion(
181
+ f"Your client version ({system_config.client_version}) is outdated. Minimum required version is {min_version}. "
182
+ "Please update using:"
183
+ " uv tool upgrade codeplain"
184
+ )
185
+
186
+
187
+ def render(args, run_state: RunState, codeplain_api, event_bus: EventBus): # noqa: C901
160
188
  template_dirs = file_utils.get_template_directories(args.filename, args.template_dir, DEFAULT_TEMPLATE_DIRS)
161
189
 
162
190
  console.info(f"Rendering {args.filename} to target code.")
@@ -177,6 +205,8 @@ def render(args, run_state: RunState, codeplain_api, event_bus: EventBus): # no
177
205
  assert args.api is not None and args.api != "", "API URL is required"
178
206
  codeplainAPI.api_url = args.api
179
207
 
208
+ _check_connection(codeplainAPI)
209
+
180
210
  module_renderer = ModuleRenderer(
181
211
  codeplainAPI,
182
212
  args.filename,
@@ -194,6 +224,7 @@ def render(args, run_state: RunState, codeplain_api, event_bus: EventBus): # no
194
224
  unittests_script=args.unittests_script,
195
225
  conformance_tests_script=args.conformance_tests_script,
196
226
  prepare_environment_script=args.prepare_environment_script,
227
+ state_machine_version=system_config.client_version,
197
228
  css_path="styles.css",
198
229
  )
199
230
  result = app.run()
@@ -206,18 +237,19 @@ def render(args, run_state: RunState, codeplain_api, event_bus: EventBus): # no
206
237
  return
207
238
 
208
239
 
209
- def main():
240
+ def main(): # noqa: C901
210
241
  args = parse_arguments()
211
242
 
212
243
  event_bus = EventBus()
213
244
 
214
- setup_logging(args, event_bus, args.log_to_file, args.log_file_name, args.filename)
215
-
216
245
  if not args.api:
217
246
  args.api = "https://api.codeplain.ai"
218
247
 
219
248
  run_state = RunState(spec_filename=args.filename, replay_with=args.replay_with)
220
249
 
250
+ setup_logging(args, event_bus, args.log_to_file, args.log_file_name, args.filename, run_state.render_id)
251
+
252
+ exc_info = None
221
253
  try:
222
254
  # Validate API key is present
223
255
  if not args.api_key:
@@ -225,42 +257,71 @@ def main():
225
257
  "API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument."
226
258
  )
227
259
 
260
+ console.debug(f"Render ID: {run_state.render_id}") # Ensure render ID is logged to the console
228
261
  render(args, run_state, codeplain_api, event_bus)
229
262
  except InvalidFridArgument as e:
230
- console.error(f"Error rendering plain code: {str(e)}.\n")
231
- # No need to print render ID since this error is going to be thrown at the very start so user will be able to
232
- # see the render ID that's printed at the very start of the rendering process.
233
- dump_crash_logs(args)
263
+ exc_info = sys.exc_info()
264
+ console.error(f"Invalid FRID argument: {str(e)}.\n")
234
265
  except FileNotFoundError as e:
235
- console.error(f"Error rendering plain code: {str(e)}\n")
266
+ exc_info = sys.exc_info()
267
+ console.error(f"File not found: {str(e)}\n")
236
268
  console.debug(f"Render ID: {run_state.render_id}")
237
- dump_crash_logs(args)
238
- except plain_file.InvalidPlainFileExtension as e:
239
- console.error(f"Error rendering plain code: {str(e)}\n")
240
- dump_crash_logs(args)
241
269
  except TemplateNotFoundError as e:
242
- console.error(f"Error: Template not found: {str(e)}\n")
270
+ exc_info = sys.exc_info()
271
+ console.error(f"Template not found: {str(e)}\n")
243
272
  console.error(system_config.get_error_message("template_not_found"))
244
- dump_crash_logs(args)
245
273
  except PlainSyntaxError as e:
246
- console.error(f"Error rendering plain code: {str(e)}\n")
247
- dump_crash_logs(args)
274
+ exc_info = sys.exc_info()
275
+ console.error(f"Plain syntax error: {str(e)}\n")
248
276
  except KeyboardInterrupt:
277
+ exc_info = sys.exc_info()
249
278
  console.error("Keyboard interrupt")
250
- # Don't print the traceback here because it's going to be from keyboard interrupt and we don't really care about that
251
279
  console.debug(f"Render ID: {run_state.render_id}")
252
- dump_crash_logs(args)
253
280
  except RequestException as e:
281
+ exc_info = sys.exc_info()
282
+ console.error(f"Error rendering plain code: {str(e)}\n")
283
+ console.debug(f"Render ID: {run_state.render_id}")
284
+ except MissingPreviousFunctionalitiesError as e:
285
+ exc_info = sys.exc_info()
254
286
  console.error(f"Error rendering plain code: {str(e)}\n")
255
287
  console.debug(f"Render ID: {run_state.render_id}")
256
- dump_crash_logs(args)
257
288
  except MissingAPIKey as e:
258
289
  console.error(f"Missing API key: {str(e)}\n")
290
+ except InvalidAPIKey as e:
291
+ console.error(f"Invalid API key: {str(e)}\n")
292
+ except OutdatedClientVersion as e:
293
+ console.error(f"Outdated client version: {str(e)}\n")
294
+ except (InternalServerError, UnexpectedState):
295
+ exc_info = sys.exc_info()
296
+ console.error(
297
+ f"Internal server error.\n\nPlease report the error to support@codeplain.ai with the attached {args.log_file_name} file."
298
+ )
299
+ console.debug(f"Render ID: {run_state.render_id}")
300
+ except ConflictingRequirements as e:
301
+ exc_info = sys.exc_info()
302
+ console.error(f"Conflicting requirements: {str(e)}\n")
303
+ console.debug(f"Render ID: {run_state.render_id}")
304
+ except CreditBalanceTooLow as e:
305
+ exc_info = sys.exc_info()
306
+ console.error(f"Credit balance too low: {str(e)}\n")
307
+ console.debug(f"Render ID: {run_state.render_id}")
308
+ except LLMInternalError as e:
309
+ exc_info = sys.exc_info()
310
+ console.error(f"LLM internal error: {str(e)}\n")
311
+ console.debug(f"Render ID: {run_state.render_id}")
312
+ except MissingResource as e:
313
+ exc_info = sys.exc_info()
314
+ console.error(f"Missing resource: {str(e)}\n")
315
+ console.debug(f"Render ID: {run_state.render_id}")
259
316
  except Exception as e:
317
+ exc_info = sys.exc_info()
260
318
  console.error(f"Error rendering plain code: {str(e)}\n")
261
319
  console.debug(f"Render ID: {run_state.render_id}")
262
- traceback.print_exc()
263
- dump_crash_logs(args)
320
+ finally:
321
+ if exc_info:
322
+ # Log traceback using the logging system
323
+ logging.error("Render crashed with exception:", exc_info=exc_info)
324
+ dump_crash_logs(args)
264
325
 
265
326
 
266
327
  if __name__ == "__main__": # noqa: C901
plain2code_console.py CHANGED
@@ -52,7 +52,7 @@ class Plain2CodeConsole(Console):
52
52
  return
53
53
 
54
54
  tree = self._create_tree_from_files(root_folder, files)
55
- super().print(f"\n[b]{header}[/b]", style=style)
55
+ super().print(f"\n{header}", style=style)
56
56
 
57
57
  super().print(tree, style=style)
58
58
 
@@ -87,9 +87,7 @@ class Plain2CodeConsole(Console):
87
87
  else:
88
88
  file_lines = len(content.splitlines())
89
89
  file_tokens = len(self.llm_encoding.encode(content))
90
- current_level = current_level.add(
91
- f"{part} [b]({file_lines} lines, {file_tokens} tokens)[/b]"
92
- )
90
+ current_level = current_level.add(f"{part} ({file_lines} lines, {file_tokens} tokens)")
93
91
  else:
94
92
  current_level = current_level.add(part)
95
93
  else:
@@ -107,7 +105,7 @@ class Plain2CodeConsole(Console):
107
105
  if resource_name["target"] in linked_resources:
108
106
  file_tokens = len(self.llm_encoding.encode(linked_resources[resource_name["target"]]))
109
107
  self.input(
110
- f"- {resource_name['text']} [b][#4169E1]({resource_name['target']}, {file_tokens} tokens)[/#4169E1][/b]"
108
+ f"- {resource_name['text']} [#4169E1]({resource_name['target']}, {file_tokens} tokens)[/#4169E1]"
111
109
  )
112
110
 
113
111
  self.input()
plain2code_exceptions.py CHANGED
@@ -25,25 +25,45 @@ class PlainSyntaxError(Exception):
25
25
  pass
26
26
 
27
27
 
28
- class OnlyRelativeLinksAllowed(Exception):
28
+ class UnexpectedState(Exception):
29
29
  pass
30
30
 
31
31
 
32
- class LinkMustHaveTextSpecified(Exception):
32
+ class MissingAPIKey(Exception):
33
33
  pass
34
34
 
35
35
 
36
- class NoRenderFound(Exception):
36
+ class InvalidAPIKey(Exception):
37
37
  pass
38
38
 
39
39
 
40
- class MultipleRendersFound(Exception):
40
+ class OutdatedClientVersion(Exception):
41
41
  pass
42
42
 
43
43
 
44
- class UnexpectedState(Exception):
44
+ class InvalidFridArgument(Exception):
45
45
  pass
46
46
 
47
47
 
48
- class MissingAPIKey(Exception):
48
+ class InvalidGitRepositoryError(Exception):
49
+ """Raised when the git repository is in an invalid state."""
50
+
51
+ pass
52
+
53
+
54
+ class InvalidLiquidVariableName(Exception):
55
+ pass
56
+
57
+
58
+ class ModuleDoesNotExistError(Exception):
59
+ pass
60
+
61
+
62
+ class InternalServerError(Exception):
63
+ pass
64
+
65
+
66
+ class MissingPreviousFunctionalitiesError(Exception):
67
+ """Raised when trying to render from a FRID but previous FRID commits are missing."""
68
+
49
69
  pass
plain2code_logger.py CHANGED
@@ -4,7 +4,6 @@ import time
4
4
  from typing import Optional
5
5
 
6
6
  from event_bus import EventBus
7
- from plain2code_console import console
8
7
  from plain2code_events import LogMessageEmitted
9
8
 
10
9
 
@@ -42,6 +41,7 @@ class TuiLoggingHandler(logging.Handler):
42
41
  super().__init__()
43
42
  self.event_bus = event_bus
44
43
  self._buffer = []
44
+ self.start_time = time.time() # Record start time for offset calculation
45
45
 
46
46
  # Register to be notified when event bus is ready
47
47
  self.event_bus.on_ready(self._flush_buffer)
@@ -49,8 +49,11 @@ class TuiLoggingHandler(logging.Handler):
49
49
  def emit(self, record):
50
50
  try:
51
51
  # Extract structured data from the log record
52
-
53
- timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(record.created))
52
+ offset_seconds = record.created - self.start_time
53
+ minutes = int(offset_seconds // 60)
54
+ seconds = int(offset_seconds % 60)
55
+ milliseconds = int((offset_seconds % 1) * 100)
56
+ timestamp = f"{minutes:02d}:{seconds:02d}:{milliseconds:02d}"
54
57
  event = LogMessageEmitted(
55
58
  logger_name=record.name,
56
59
  level=record.levelname,
@@ -63,6 +66,10 @@ class TuiLoggingHandler(logging.Handler):
63
66
  self.event_bus.publish(event)
64
67
  else:
65
68
  self._buffer.append(event)
69
+ except RuntimeError:
70
+ # We're going to get this crash after the TUI app is closed (forcefully).
71
+ # NOTE: This should be more thought out.
72
+ pass
66
73
  except Exception:
67
74
  self.handleError(record)
68
75
 
@@ -123,5 +130,4 @@ def dump_crash_logs(args, formatter=None):
123
130
  if crash_handler and args.filename:
124
131
  log_file_path = get_log_file_path(args.filename, args.log_file_name)
125
132
 
126
- if crash_handler.dump_to_file(log_file_path, formatter):
127
- console.error(f"\nLogs have been dumped to {log_file_path}")
133
+ crash_handler.dump_to_file(log_file_path, formatter)
plain2code_utils.py CHANGED
@@ -24,15 +24,17 @@ def print_dry_run_output(plain_source_tree: dict, render_range: Optional[list[st
24
24
  console.info(
25
25
  "-------------------------------------"
26
26
  f"Rendering functional requirement {frid}"
27
- f"[b]{functional_requirement_text}[/b]"
27
+ f"{functional_requirement_text}"
28
28
  "-------------------------------------"
29
29
  )
30
30
  if plain_spec.ACCEPTANCE_TESTS in specifications:
31
31
  for i, acceptance_test in enumerate(specifications[plain_spec.ACCEPTANCE_TESTS], 1):
32
- console.info(f"\nGenerating acceptance test #{i}:\n\n{acceptance_test}")
32
+ console.info(f"Generating acceptance test #{i}:\n\n{acceptance_test}")
33
33
  else:
34
- console.info("-------------------------------------")
35
- console.info(f"Skipping rendering iteration: {frid}")
36
- console.info("-------------------------------------")
34
+ console.info(
35
+ "-------------------------------------\n"
36
+ f"Skipping rendering iteration: {frid}\n"
37
+ "-------------------------------------"
38
+ )
37
39
 
38
40
  frid = plain_spec.get_next_frid(plain_source_tree, frid)
plain_file.py CHANGED
@@ -44,22 +44,6 @@ class PlainFileParseResult:
44
44
  required_concepts: list[str]
45
45
 
46
46
 
47
- class OnlyRelativeLinksAllowed(Exception):
48
- pass
49
-
50
-
51
- class LinkMustHaveTextSpecified(Exception):
52
- pass
53
-
54
-
55
- class InvalidPlainFileExtension(Exception):
56
- pass
57
-
58
-
59
- class PlainModuleNotFound(Exception):
60
- pass
61
-
62
-
63
47
  class PlainRenderer(MarkdownRenderer):
64
48
  def render_link(self, token: Link) -> Iterable[Fragment]:
65
49
  yield from self.embed_span(
@@ -95,12 +79,12 @@ def check_section_for_linked_resources(section):
95
79
  for link in traverse(section, klass=Link):
96
80
  parsed_url = urlparse(link.node.target)
97
81
  if parsed_url.scheme != "" or os.path.isabs(link.node.target):
98
- raise OnlyRelativeLinksAllowed(
82
+ raise PlainSyntaxError(
99
83
  f"Only relative links are allowed (text: {link.node.children[0].content}, target: {link.node.target})."
100
84
  )
101
85
 
102
86
  if len(link.node.children) != 1:
103
- raise LinkMustHaveTextSpecified(f"Link must have text specified (link: {link.node.target}).")
87
+ raise PlainSyntaxError(f"Link must have text specified (link: {link.node.target}).")
104
88
 
105
89
  linked_resources.append({"text": link.node.children[0].content, "target": link.node.target})
106
90
 
@@ -222,7 +206,7 @@ def _process_single_acceptance_test_requirement(functional_requirement: mistleto
222
206
  # Handle the case when the heading is not valid. This case includes cases such as:
223
207
  # - Writing `acceptance test` instead of `acceptance tests` (or any other syntax diffs).
224
208
  # - Instead of specifying `acceptance tests` below the functional requirement, creator of the plain file
225
- # might have specified some other building block (e.g. `technical specs`)
209
+ # might have specified some other building block (e.g. `implementation reqs`)
226
210
  raise PlainSyntaxError(acceptance_test_heading_problem)
227
211
 
228
212
  if is_acceptance_test_heading:
@@ -529,7 +513,7 @@ def parse_plain_source( # noqa: C901
529
513
  def read_module_plain_source(module_name: str, template_dirs: list[str]) -> str:
530
514
  plain_source_text = file_utils.open_from(template_dirs, module_name + PLAIN_SOURCE_FILE_EXTENSION)
531
515
  if plain_source_text is None:
532
- raise PlainModuleNotFound(f"Module does not exist ({module_name}).")
516
+ raise PlainSyntaxError(f"Module does not exist ({module_name}).")
533
517
  return plain_source_text
534
518
 
535
519
 
@@ -566,12 +550,9 @@ def process_required_modules(
566
550
  if len(all_required_modules) > 0 and module_name == all_required_modules[-1]:
567
551
  continue
568
552
 
569
- try:
570
- plain_file_parse_result = parse_plain_file(
571
- module_name, code_variables, template_dirs, imported_modules=[], modules_trace=[]
572
- )
573
- except PlainModuleNotFound:
574
- raise PlainSyntaxError(f"Required module not found ({module_name}).")
553
+ plain_file_parse_result = parse_plain_file(
554
+ module_name, code_variables, template_dirs, imported_modules=[], modules_trace=[]
555
+ )
575
556
 
576
557
  if len(plain_file_parse_result.required_modules) == 0:
577
558
  if len(all_required_modules) > 0:
@@ -644,7 +625,7 @@ def plain_file_parser( # noqa: C901
644
625
  # and we need to pass them to the marshalled_plain_source_tree after it's rendered
645
626
  plain_source_file_path = Path(plain_source_file_name)
646
627
  if plain_source_file_path.suffix != PLAIN_SOURCE_FILE_EXTENSION:
647
- raise InvalidPlainFileExtension(
628
+ raise PlainSyntaxError(
648
629
  f"Invalid plain file extension: {plain_source_file_path.suffix}. Expected: {PLAIN_SOURCE_FILE_EXTENSION}."
649
630
  )
650
631
 
@@ -652,16 +633,13 @@ def plain_file_parser( # noqa: C901
652
633
 
653
634
  code_variables = {}
654
635
 
655
- try:
656
- plain_file_parse_result = parse_plain_file(
657
- module_name,
658
- code_variables,
659
- template_dirs,
660
- imported_modules=[],
661
- modules_trace=[],
662
- )
663
- except PlainModuleNotFound as e:
664
- raise PlainSyntaxError(e.message)
636
+ plain_file_parse_result = parse_plain_file(
637
+ module_name,
638
+ code_variables,
639
+ template_dirs,
640
+ imported_modules=[],
641
+ modules_trace=[],
642
+ )
665
643
 
666
644
  if len(plain_file_parse_result.required_concepts) > 0:
667
645
  missing_required_concepts_msg = "Missing required concepts: "
plain_modules.py CHANGED
@@ -7,6 +7,7 @@ from git.exc import NoSuchPathError
7
7
 
8
8
  import git_utils
9
9
  import plain_spec
10
+ from plain2code_exceptions import ModuleDoesNotExistError
10
11
  from render_machine.implementation_code_helpers import ImplementationCodeHelpers
11
12
 
12
13
  CODEPLAIN_MEMORY_SUBFOLDER = ".memory"
@@ -16,10 +17,6 @@ MODULE_FUNCTIONALITIES = "functionalities"
16
17
  REQUIRED_MODULES_FUNCTIONALITIES = "required_modules_functionalities"
17
18
 
18
19
 
19
- class ModuleDoesNotExistError(Exception):
20
- pass
21
-
22
-
23
20
  class PlainModule:
24
21
  def __init__(self, name: str, build_folder: str):
25
22
  self.name = name
plain_spec.py CHANGED
@@ -5,9 +5,11 @@ from typing import Optional
5
5
 
6
6
  from liquid2.filter import with_context
7
7
 
8
+ from plain2code_exceptions import InvalidLiquidVariableName
9
+
8
10
  DEFINITIONS = "definitions"
9
- NON_FUNCTIONAL_REQUIREMENTS = "technical specs"
10
- TEST_REQUIREMENTS = "test specs"
11
+ NON_FUNCTIONAL_REQUIREMENTS = "implementation reqs"
12
+ TEST_REQUIREMENTS = "test reqs"
11
13
  FUNCTIONAL_REQUIREMENTS = "functional specs"
12
14
  ACCEPTANCE_TESTS = "acceptance_tests"
13
15
  ACCEPTANCE_TEST_HEADING = "acceptance tests"
@@ -23,10 +25,6 @@ ALLOWED_SPECIFICATION_HEADINGS = [
23
25
  ALLOWED_IMPORT_SPECIFICATION_HEADINGS = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS]
24
26
 
25
27
 
26
- class InvalidLiquidVariableName(Exception):
27
- pass
28
-
29
-
30
28
  def collect_specification_linked_resources(specification, specification_heading, linked_resources_list):
31
29
  linked_resources = []
32
30
  if "linked_resources" in specification:
@@ -180,6 +178,26 @@ def get_previous_frid(plain_source_tree, frid):
180
178
  raise Exception(f"Functional requirement {frid} does not exist.")
181
179
 
182
180
 
181
+ def get_frids_before(plain_source_tree, target_frid: str) -> list[str]:
182
+ """
183
+ Get all FRIDs that appear before the target FRID in the specification.
184
+
185
+ Args:
186
+ plain_source_tree: The plain source tree
187
+ target_frid: The FRID to find predecessors for
188
+
189
+ Returns:
190
+ List of FRIDs that appear before target_frid, in order
191
+ """
192
+ previous_frids = []
193
+ for frid in get_frids(plain_source_tree):
194
+ if frid == target_frid:
195
+ break
196
+ previous_frids.append(frid)
197
+
198
+ return previous_frids
199
+
200
+
183
201
  def get_specification_item_markdown(specification_item, code_variables, replace_code_variables):
184
202
  markdown = specification_item["markdown"]
185
203
  if "code_variables" in specification_item:
@@ -21,6 +21,6 @@ class CreateDist(BaseAction):
21
21
  render_context.conformance_tests.get_module_conformance_tests_folder(render_context.module_name),
22
22
  render_context.conformance_tests_dest,
23
23
  )
24
- console.info(f"Render {render_context.run_state.render_id} completed successfully.")
24
+ console.info(f"[#79FC96]Render {render_context.run_state.render_id} completed successfully.[/#79FC96]")
25
25
 
26
26
  return self.SUCCESSFUL_OUTCOME, None
@@ -18,7 +18,7 @@ class ExitWithError(BaseAction):
18
18
 
19
19
  if render_context.frid_context is not None:
20
20
  console.info(
21
- f"To continue rendering from the last successfully rendered functional requirement, provide the [red][b]--render-from {render_context.frid_context.frid}[/b][/red] flag."
21
+ f"To continue rendering from the last successfully rendered functional requirement, provide the [red]--render-from {render_context.frid_context.frid}[/red] flag."
22
22
  )
23
23
 
24
24
  if render_context.run_state.render_id is not None:
@@ -14,7 +14,7 @@ class PrepareTestingEnvironment(BaseAction):
14
14
  def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
15
15
  if render_context.verbose:
16
16
  console.info(
17
- f"[b]Running testing environment preparation script {render_context.prepare_environment_script} for build folder {render_context.build_folder}.[/b]"
17
+ f"Running testing environment preparation script {render_context.prepare_environment_script} for build folder {render_context.build_folder}."
18
18
  )
19
19
  exit_code, _, preparation_temp_file_path = render_utils.execute_script(
20
20
  render_context.prepare_environment_script,
@@ -24,7 +24,7 @@ class RenderConformanceTests(BaseAction):
24
24
 
25
25
  def _render_conformance_tests(self, render_context: RenderContext):
26
26
  if render_context.verbose:
27
- console.info("[b]Implementing test requirements:[/b]")
27
+ console.info("Implementing test requirements:")
28
28
  console.print_list(
29
29
  render_context.conformance_tests_running_context.current_testing_frid_specifications[
30
30
  plain_spec.TEST_REQUIREMENTS
@@ -149,9 +149,7 @@ class RenderConformanceTests(BaseAction):
149
149
  ]
150
150
 
151
151
  if render_context.verbose:
152
- console.info("\n[b]Generating acceptance test:[/b]")
153
- console.info(f"[b]{acceptance_test}[/b]")
154
- console.info()
152
+ console.info(f"Generating acceptance test:\n {acceptance_test}")
155
153
 
156
154
  with console.status(
157
155
  f"[{console.INFO_STYLE}]Generating acceptance test for functional requirement {render_context.frid_context.frid}...\n"
@@ -25,14 +25,14 @@ class RenderFunctionalRequirement(BaseAction):
25
25
  _, memory_files_content = MemoryManager.fetch_memory_files(render_context.memory_manager.memory_folder)
26
26
 
27
27
  if render_context.verbose:
28
- msg = f"Module: {render_context.module_name}\n"
29
- msg += f"Rendering functional requirement {render_context.frid_context.frid}"
28
+ msg = "-------------------------------------\n"
29
+ msg += f"Module: {render_context.module_name}\n"
30
+ msg += f"Rendering functionality {render_context.frid_context.frid}"
30
31
  if render_context.frid_context.functional_requirement_render_attempts > 1:
31
32
  msg += f", attempt number {render_context.frid_context.functional_requirement_render_attempts}/{MAX_CODE_GENERATION_RETRIES}."
32
- msg += f"\n[b]{render_context.frid_context.functional_requirement_text}[/b]"
33
- console.info("-------------------------------------")
33
+ msg += f"\n{render_context.frid_context.functional_requirement_text}\n"
34
+ msg += "-------------------------------------"
34
35
  console.info(msg)
35
- console.info("-------------------------------------")
36
36
 
37
37
  try:
38
38
  if render_context.verbose:
@@ -54,7 +54,7 @@ class RenderFunctionalRequirement(BaseAction):
54
54
  render_context.run_state,
55
55
  )
56
56
  except FunctionalRequirementTooComplex as e:
57
- error_message = f"The functional requirement:\n[b]{render_context.frid_context.functional_requirement_text}[/b]\n is too complex to be implemented. Please break down the functional requirement into smaller parts ({str(e)})."
57
+ error_message = f"The functional requirement:\n{render_context.frid_context.functional_requirement_text}\n is too complex to be implemented. Please break down the functional requirement into smaller parts ({str(e)})."
58
58
  if e.proposed_breakdown:
59
59
  error_message += "\nProposed breakdown:"
60
60
  for _, part in e.proposed_breakdown["functional_requirements"].items():
@@ -32,17 +32,18 @@ class RunConformanceTests(BaseAction):
32
32
 
33
33
  if render_context.verbose:
34
34
  console.info(
35
- f"\n[b]Running conformance tests script {render_context.conformance_tests_script} "
35
+ f"Running conformance tests script {render_context.conformance_tests_script} "
36
36
  + f"for {conformance_tests_folder_name} ("
37
37
  + f"functional requirement {render_context.conformance_tests_running_context.current_testing_frid} "
38
38
  + f"in module {render_context.conformance_tests_running_context.current_testing_module_name}"
39
- + ").[/b]"
39
+ + ")."
40
40
  )
41
41
  exit_code, conformance_tests_issue, conformance_tests_temp_file_path = render_utils.execute_script(
42
42
  render_context.conformance_tests_script,
43
43
  [render_context.build_folder, conformance_tests_folder_name],
44
44
  render_context.verbose,
45
45
  "Conformance Tests",
46
+ render_context.conformance_tests_running_context.current_testing_frid,
46
47
  )
47
48
  render_context.script_execution_history.latest_conformance_test_output_path = conformance_tests_temp_file_path
48
49
  render_context.script_execution_history.should_update_script_outputs = True
@@ -17,7 +17,7 @@ class RunUnitTests(BaseAction):
17
17
  def execute(self, render_context: RenderContext, _previous_action_payload: Any | None):
18
18
  if render_context.verbose:
19
19
  console.info(
20
- f"[b]Running unit tests script {render_context.unittests_script}.[/b] (attempt: {render_context.unit_tests_running_context.fix_attempts + 1})"
20
+ f"Running unit tests script {render_context.unittests_script}. (attempt: {render_context.unit_tests_running_context.fix_attempts + 1})"
21
21
  )
22
22
  exit_code, unittests_issue, unittests_temp_file_path = render_utils.execute_script(
23
23
  render_context.unittests_script,