codeplain 0.2.19__py3-none-any.whl → 0.3.0__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 (48) hide show
  1. _version.py +1 -1
  2. {codeplain-0.2.19.dist-info → codeplain-0.3.0.dist-info}/METADATA +5 -5
  3. codeplain-0.3.0.dist-info/RECORD +88 -0
  4. codeplain_REST_api.py +30 -10
  5. concept_utils.py +1 -1
  6. file_utils.py +10 -9
  7. git_utils.py +70 -2
  8. memory_management.py +13 -2
  9. module_renderer.py +62 -168
  10. partial_rendering.py +224 -0
  11. plain2code.py +98 -139
  12. plain2code_arguments.py +6 -4
  13. plain2code_events.py +6 -1
  14. plain2code_exceptions.py +4 -0
  15. plain2code_logger.py +8 -4
  16. plain2code_nodes.py +14 -3
  17. plain_file.py +39 -25
  18. plain_modules.py +256 -44
  19. plain_spec.py +53 -1
  20. render_machine/actions/analyze_specification_ambiguity.py +1 -1
  21. render_machine/actions/exit_with_error.py +7 -1
  22. render_machine/actions/fix_conformance_test.py +59 -27
  23. render_machine/actions/fix_unit_tests.py +13 -14
  24. render_machine/actions/prepare_repositories.py +14 -3
  25. render_machine/actions/refactor_code.py +8 -10
  26. render_machine/actions/render_conformance_tests.py +76 -59
  27. render_machine/actions/summarize_conformance_tests.py +9 -12
  28. render_machine/code_renderer.py +20 -9
  29. render_machine/conformance_tests.py +1 -1
  30. render_machine/render_context.py +250 -70
  31. render_machine/render_types.py +58 -9
  32. render_machine/render_utils.py +6 -3
  33. tests/test_file_utils.py +41 -0
  34. tests/test_git_utils.py +74 -0
  35. tests/test_partial_rendering.py +316 -0
  36. tests/test_plain_modules.py +294 -0
  37. tests/test_plainfile.py +1 -1
  38. tests/test_plainfileparser.py +2 -2
  39. tui/components.py +73 -11
  40. tui/plain2code_tui.py +21 -30
  41. tui/plain_module_render_choice_tui.py +186 -0
  42. tui/state_handlers.py +9 -4
  43. tui/styles.css +42 -3
  44. tui/widget_helpers.py +24 -1
  45. codeplain-0.2.19.dist-info/RECORD +0 -83
  46. {codeplain-0.2.19.dist-info → codeplain-0.3.0.dist-info}/WHEEL +0 -0
  47. {codeplain-0.2.19.dist-info → codeplain-0.3.0.dist-info}/entry_points.txt +0 -0
  48. {codeplain-0.2.19.dist-info → codeplain-0.3.0.dist-info}/licenses/LICENSE +0 -0
_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.19"
1
+ __version__ = "0.3.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeplain
3
- Version: 0.2.19
3
+ Version: 0.3.0
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
@@ -8,13 +8,13 @@ Classifier: Intended Audience :: Developers
8
8
  Classifier: Operating System :: OS Independent
9
9
  Classifier: Topic :: Software Development :: Code Generators
10
10
  Requires-Python: ==3.11
11
- Requires-Dist: gitpython==3.1.42
11
+ Requires-Dist: gitpython==3.1.50
12
12
  Requires-Dist: mistletoe==1.3.0
13
13
  Requires-Dist: networkx==3.6.1
14
14
  Requires-Dist: python-frontmatter==1.1.0
15
15
  Requires-Dist: python-liquid2==0.3.0
16
- Requires-Dist: pyyaml==6.0.2
17
- Requires-Dist: requests==2.32.4
16
+ Requires-Dist: pyyaml==6.0.3
17
+ Requires-Dist: requests==2.34.0
18
18
  Requires-Dist: rich==14.2.0
19
19
  Requires-Dist: textual>=7.5.0
20
20
  Requires-Dist: tiktoken==0.12.0
@@ -115,7 +115,7 @@ After completing the installation steps above, you can immediately test the syst
115
115
 
116
116
  ### Documentation
117
117
 
118
- - For more details on the ***plain format, see the [***plain language specification](https://www.plainlang.org/docs/intro/).
118
+ - For more details on the ***plain format, see the [***plain language specification](https://www.plainlang.org/docs/).
119
119
  - For step-by-step instructions for creating your first ***plain project see the [Kickstart your ***plain project](docs/starting_a_plain_project_from_scratch.md).
120
120
  - For complete CLI documentation and usage examples, see [plain2code CLI documentation](docs/plain2code_cli.md).
121
121
 
@@ -0,0 +1,88 @@
1
+ _version.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
2
+ codeplain_REST_api.py,sha256=Z1WV67_Xtw2li1wQyVnhrqr-Ymu_BWp9AdB4YjImIhw,21258
3
+ concept_utils.py,sha256=VLHur80-XSJTveClLiHb-CMuqsXd8sIQAr0Ehe1Hid0,7930
4
+ diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
5
+ event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
6
+ file_utils.py,sha256=eO_SHwyjJlkwT3wTlIjIEQ3t8y3k0i-fWKAn5_1SMkY,12597
7
+ git_utils.py,sha256=LQQnL5DAFpH5fDBQgUZX2WfjWY4YC5OEGBPIXryTmnY,16896
8
+ hash_key.py,sha256=lMKgKpOhPeC4UpFf9e7eCNK20FEaDsXTCJ8j3eCWidE,837
9
+ memory_management.py,sha256=G42roiUSgX8sqDnkfb7XYtYWZqlOQ3DIpMbi0pcOKZA,6079
10
+ module_renderer.py,sha256=HfxqzwK5nj0EXC2hx5y6IYY3kVeCMJnPRFoqfFLvaF4,6936
11
+ partial_rendering.py,sha256=kZP23kzR__J4ajpo6pmklFpVOoJcaQkgqC5KuLjS--A,7951
12
+ plain2code.py,sha256=BvFbgFihS6pW6T5Hferu5HpyC5SFlEe__iAu1KJy0NY,13368
13
+ plain2code_arguments.py,sha256=l2K7LVjGe230xwk8K0Q9jkbohBu-4hDJYhueFLPsXMc,14502
14
+ plain2code_console.py,sha256=dSkuwUHS0ufUy24y8sloL3uedDDdXPuFDkBaanp1Cug,4820
15
+ plain2code_events.py,sha256=AM8KJS68uiVXRMLt861KaGhs2H4yzujlMB_xiYPfHQc,1478
16
+ plain2code_exceptions.py,sha256=g3wMOnjpQlk2pdKRvVarnuh9BlOsRV2AKm_CoNidmH4,1629
17
+ plain2code_logger.py,sha256=j376mlCTU-EN_mv06rRY2Rp_47CHzEo6b5wvhlmp0fY,3217
18
+ plain2code_nodes.py,sha256=IwjQqJxRSJKgP6eEnkYGy3VQHIyoazgkFxAEkxkjfJ8,4426
19
+ plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,2235
20
+ plain2code_state.py,sha256=UN3iD3rfJkGK4QQx4RxueQrCy9ngHqExtzxiSEgrW2E,2117
21
+ plain2code_utils.py,sha256=CLvysMiwChAVaKdqJjkUUlDCQ8dGIFPz1NG7cLbFZuI,2236
22
+ plain_file.py,sha256=hiyFdB548sYG9T2NILhGM63dcSgbRzy_y5atX5YabUg,29172
23
+ plain_modules.py,sha256=pC-cFFRmVNfloscqS7uPjaPF0Dw2NFn9fIQNUP2j0ys,14333
24
+ plain_spec.py,sha256=E2Yp2eXMExE7kx7GPQ7v9boHN8XhFUcqprn1DGMaFMs,15669
25
+ system_config.py,sha256=DMyfiq_uzxWH_uXz5vMWNHkcRFPkIA-M8nr64SeVhPo,1350
26
+ config/__init__.py,sha256=beYSsJWmBNHDP5rYmVDouqgEeP3t1lkkepbXJ-oq0F8,37
27
+ config/system_config.yaml,sha256=of5R9vukSohU9AlkGJOcAVQA1UJgxYmDy6islU6iFuE,508
28
+ docs/generate_cli.py,sha256=0FHVhICbM8g7ahdInTNih3hNKpty1wbV5rdGECMJj48,656
29
+ examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py,sha256=dwTowrHiVKKbrDv21v8xJC30Q57AXZkQasdGOO5JsBE,470
30
+ render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ render_machine/code_renderer.py,sha256=TEHdlEOB-seMGeEo1rg5kaIL3pkub7CnU2X8eZNc8G4,3762
32
+ render_machine/conformance_test_helpers.py,sha256=6Ru7Dh24SLNIKWfz_sP5hE_EmWpvk6nfgN1T1yaCZus,2887
33
+ render_machine/conformance_tests.py,sha256=y7dhmsFgq7LpaeuLDM5nc1_1HoPo0wkPd3JdaRPxA0Y,7229
34
+ render_machine/implementation_code_helpers.py,sha256=LdHNLoKgVtrDaGH2APX8C9vjNjLBUhu2V7OAaJvm4rM,2009
35
+ render_machine/render_context.py,sha256=gu1_TjWjiHVlpx3qlDLg0PYI58rYX8YY7xFCoBahems,29133
36
+ render_machine/render_types.py,sha256=iqRd9MyrRnXasoA8rvssZbCN66BbVCVXH5uFA8QJVXY,9222
37
+ render_machine/render_utils.py,sha256=LIeFJPCj4uPLhyi-LVOr_oNXEKWvE86qpwnPk9u5Vgg,9451
38
+ render_machine/state_machine_config.py,sha256=1nVddQBnJsbdUYCNK813QnLZH2D4krz0vqHe2K8LDS8,28212
39
+ render_machine/states.py,sha256=niFXqayyA_rezCrlzo0hYeHAruErcPzQVF-BeIBGnL8,1968
40
+ render_machine/triggers.py,sha256=o1N5c9xC7fsKY6ZaWcWBtALT8i1acSSharAXKAqBu8E,1285
41
+ render_machine/actions/analyze_specification_ambiguity.py,sha256=SykZAozGvr1jySxbpKCgt11nMpZhOK0sN-J66M42T5Y,2847
42
+ render_machine/actions/base_action.py,sha256=vVzMp-LanrZyq-jamHyWaQxpRl-pLqtTdtiEzUBhfyE,534
43
+ render_machine/actions/commit_conformance_tests_changes.py,sha256=08yxCPCpf5TlrzLPsEh5SJXVVvnLfwdNKSr7BlzlYxo,2589
44
+ render_machine/actions/commit_implementation_code_changes.py,sha256=yi51oAArLCgWcmFbynes-BK2IGnZwA7wygrTJGoL8dY,826
45
+ render_machine/actions/create_dist.py,sha256=xMPPyws7swVkaUSQg3dwMGScCi1BuII9y2C_XSP0N80,1067
46
+ render_machine/actions/exit_with_error.py,sha256=-LkQsFhmV-Nr741APTx38dqu473b6OSiIvKayFFsXxI,1270
47
+ render_machine/actions/finish_functional_requirement.py,sha256=tj9CzuxuHcbR-2-jMLcAM0gOAcbWpMLg9HIiyngI6gU,739
48
+ render_machine/actions/fix_conformance_test.py,sha256=aYf3j-q7HnZGB9M1qEMpYqLyCUMqWJR9T6VZqn-oWrA,8972
49
+ render_machine/actions/fix_unit_tests.py,sha256=AWrzaLpd-PuIBt_udzaQCRhlGyPIIgMrlzIDum9Xl8E,2409
50
+ render_machine/actions/prepare_repositories.py,sha256=jaRrOGnTR54e8UdaeaX7T73h2rdPJ3IDAQ5mrKHdFgw,3547
51
+ render_machine/actions/prepare_testing_environment.py,sha256=Rvj0LNuQ6anP-3fBr2FmDurt7XrPM-GUREqz3c87jTg,1950
52
+ render_machine/actions/refactor_code.py,sha256=slDnx37RLDP4ACiIv0b3u6JTsYPyfyLY-2oXGUM8ko4,1970
53
+ render_machine/actions/render_conformance_tests.py,sha256=J2p_urgayUNtGlmdZ8NWuE1nvDgOQHpir2ESHcAfD6U,9587
54
+ render_machine/actions/render_functional_requirement.py,sha256=tr0SqG25v3PwUCVgrx4eB8-Xh6ibab0H5IK3nMmULlg,3955
55
+ render_machine/actions/run_conformance_tests.py,sha256=WtMSFTYfWFR6bBh3rZ7n5oc7jDDsxnTEdRi-kN1cj4o,4075
56
+ render_machine/actions/run_unit_tests.py,sha256=zCu3yJbJ0JkiB7lZbgBwsVs_qiFtrlyN2T3xf5A_gPg,2176
57
+ render_machine/actions/summarize_conformance_tests.py,sha256=Vy1kRK8vyazK_BbsKuz1MLDN_TQWVxxzkwEEm-50byc,1639
58
+ standard_template_library/__init__.py,sha256=LtsHl0c3VV46DUInIqyOw4KKVoxKEMOx8dAZpMXtAXc,43
59
+ standard_template_library/golang-console-app-template.plain,sha256=JOusEjCj7jagnXUzaI-SdIr3hj1wTo2uShK017aNtVM,949
60
+ standard_template_library/python-console-app-template.plain,sha256=HW-REeF8Crk0r_w4-MxnleVHeW6PdOB2C8gO8FDG61g,931
61
+ standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOzWnDHdVpFj-XBTq6s4_2dy7LKV3lVkCzv8i4,114
62
+ standard_template_library/typescript-react-app-template.plain,sha256=DN5wEJcT8ZqQTf0vFKi9Z1isi9cin2UVNOZb3r28d3M,715
63
+ tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
64
+ tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
65
+ tests/test_file_utils.py,sha256=-O5zImiFVz9JJ4xi8TbsUwNgPFQoQUOl9Frls4BHCBI,1358
66
+ tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
67
+ tests/test_imports.py,sha256=EWYLUxGunUUdsG9iCiY_tts6T7DIVdhdB_MZlB1LsmU,2081
68
+ tests/test_partial_rendering.py,sha256=4xXZhy9yStn0awSw8B-yrJ3kVYyzJrlR-dKwg9ODXMY,11188
69
+ tests/test_plain_modules.py,sha256=9bWsketDnjpCWkCbMZzYvLqT1ojPhLyxxKljPUq42oY,11393
70
+ tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
71
+ tests/test_plainfileparser.py,sha256=iVTAJWxQZbv96uHXe-qvGSgsC9uJk5Tu10Nod3kqflI,18666
72
+ tests/test_plainspec.py,sha256=JrzjJ4VFYDmWTZDq-8WFyt2oMx76IEuO1CAUPQeGOsw,2404
73
+ tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
74
+ tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
75
+ tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
76
+ tui/components.py,sha256=G6ktJx211WrYmj-1aZL3mFtnBTOE4WRF3hA1LG8J0jU,25810
77
+ tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
78
+ tui/plain2code_tui.py,sha256=_c8gaEZeFPvr1-uqpQAsacpHd77Ig16x045Q4ihfwuc,13076
79
+ tui/plain_module_render_choice_tui.py,sha256=ZiB7E2mgFN9v0WHZeDfhLvfYURg1IPcQpC5dvHQwzqA,8499
80
+ tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
81
+ tui/state_handlers.py,sha256=4r_6m5YMcprQtgiTP0De3XFfGAF8VzgaWXHxPweYPbQ,16325
82
+ tui/styles.css,sha256=9vI6V7n7p76i2Dn62KDyOkRYp__d3v2bCzYxO9PWnm8,6685
83
+ tui/widget_helpers.py,sha256=DdXCAJEvFVMgLM2GNBqE-yMtgoNnk7sHG6ehaEV3zdk,6577
84
+ codeplain-0.3.0.dist-info/METADATA,sha256=uosx3aB3OZ_wcwt-YeAFzzVHKHyyMf78k_rqFEp6iSs,4203
85
+ codeplain-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
86
+ codeplain-0.3.0.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
87
+ codeplain-0.3.0.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
88
+ codeplain-0.3.0.dist-info/RECORD,,
codeplain_REST_api.py CHANGED
@@ -2,7 +2,7 @@ import time
2
2
  from typing import Optional
3
3
 
4
4
  import requests
5
- from requests.exceptions import ConnectionError, Timeout
5
+ from requests.exceptions import ConnectionError, RequestException, Timeout
6
6
 
7
7
  import plain2code_exceptions
8
8
  from plain2code_state import RunState
@@ -18,7 +18,7 @@ RETRY_ERROR_CODES = [
18
18
  ERROR_CODE_EXCEPTIONS = {
19
19
  "FunctionalRequirementTooComplex": plain2code_exceptions.FunctionalRequirementTooComplex,
20
20
  "ConflictingRequirements": plain2code_exceptions.ConflictingRequirements,
21
- "CreditBalanceTooLow": plain2code_exceptions.RenderingCreditBalanceTooLow,
21
+ "RenderingCreditBalanceTooLow": plain2code_exceptions.RenderingCreditBalanceTooLow,
22
22
  "LLMInternalError": plain2code_exceptions.LLMInternalError,
23
23
  "MissingResource": plain2code_exceptions.MissingResource,
24
24
  "PlainSyntaxError": plain2code_exceptions.PlainSyntaxError,
@@ -76,9 +76,12 @@ class CodeplainAPI:
76
76
  if not silent:
77
77
  self.console.error(f"Max retries ({num_retries}) exceeded. Last error: {error}")
78
78
  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
82
85
 
83
86
  def _raise_for_error_code(self, response_json):
84
87
  """Raise appropriate exception based on error code in response."""
@@ -86,13 +89,28 @@ class CodeplainAPI:
86
89
  if error_code not in ERROR_CODE_EXCEPTIONS:
87
90
  return
88
91
 
89
- exception_class = ERROR_CODE_EXCEPTIONS[error_code]
90
92
  message = response_json.get("message", "")
91
93
 
92
- # FunctionalRequirementTooComplex has an extra parameter
93
94
  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 == "RenderingCreditBalanceTooLow":
103
+ raise plain2code_exceptions.RenderingCreditBalanceTooLow(message)
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]
96
114
  raise exception_class(message)
97
115
 
98
116
  def post_request(
@@ -118,7 +136,7 @@ class CodeplainAPI:
118
136
  response_json = response.json()
119
137
  except requests.exceptions.JSONDecodeError as e:
120
138
  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
122
140
 
123
141
  if response.status_code == requests.codes.bad_request and "error_code" in response_json:
124
142
  self._raise_for_error_code(response_json)
@@ -350,6 +368,7 @@ class CodeplainAPI:
350
368
  implementation_fix_count,
351
369
  conformance_tests_folder_name,
352
370
  current_testing_frid_high_level_implementation_plan: Optional[str],
371
+ conflicting_requirements_count: int,
353
372
  run_state: RunState,
354
373
  ):
355
374
  endpoint_url = f"{self.api_url}/fix_conformance_tests_issue"
@@ -371,6 +390,7 @@ class CodeplainAPI:
371
390
  "implementation_fix_count": implementation_fix_count,
372
391
  "conformance_tests_folder_name": conformance_tests_folder_name,
373
392
  "current_testing_frid_high_level_implementation_plan": current_testing_frid_high_level_implementation_plan,
393
+ "conflicting_requirements_count": conflicting_requirements_count,
374
394
  }
375
395
 
376
396
  if acceptance_tests is not None:
concept_utils.py CHANGED
@@ -206,7 +206,7 @@ def sort_definitions(definitions: list[dict]) -> list[dict]:
206
206
  msg += "\n".join(cyclic_definitions)
207
207
  msg += "\n"
208
208
 
209
- raise PlainSyntaxError(msg)
209
+ raise PlainSyntaxError(f"Plain syntax error: {msg}")
210
210
 
211
211
  order = list(nx.topological_sort(concept_graph))
212
212
  if len(order) > 0:
file_utils.py CHANGED
@@ -8,6 +8,7 @@ from liquid2.exceptions import UndefinedError
8
8
 
9
9
  import plain_spec
10
10
  from plain2code_console import console
11
+ from plain2code_exceptions import UnsupportedResourceType
11
12
  from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
12
13
  from plain_modules import CODEPLAIN_MEMORY_SUBFOLDER, CODEPLAIN_METADATA_FOLDER
13
14
 
@@ -217,18 +218,12 @@ def open_from(dirs, file_name):
217
218
 
218
219
  with open(full_file_name, "rb") as f:
219
220
  content = f.read()
220
- try:
221
- content_text = content.decode("utf-8")
222
- except UnicodeDecodeError:
223
- console.debug(
224
- f"WARNING! Error loading {file_name} ({file_name}). File is not a text file. Skipping it."
225
- )
226
- return content_text
221
+ return content.decode("utf-8")
227
222
 
228
223
  return None
229
224
 
230
225
 
231
- def load_linked_resources(template_dirs: list[str], resources_list):
226
+ def load_linked_resources(template_dirs: list[str], resources_list, module_name: str):
232
227
  linked_resources = {}
233
228
 
234
229
  for resource in resources_list:
@@ -236,7 +231,13 @@ def load_linked_resources(template_dirs: list[str], resources_list):
236
231
  if file_name in linked_resources:
237
232
  continue
238
233
 
239
- content = open_from(template_dirs, file_name)
234
+ try:
235
+ content = open_from(template_dirs, file_name)
236
+ except UnicodeDecodeError:
237
+ raise UnsupportedResourceType(
238
+ f"Referenced resource '{file_name}' in module '{module_name}' is a binary file. "
239
+ f"Only text files (e.g. .md, .txt, .json, .yaml) can be referenced from a .plain file."
240
+ )
240
241
 
241
242
  if content is None:
242
243
  raise FileNotFoundError(f"""
git_utils.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import re
2
3
  from configparser import NoOptionError, NoSectionError
3
4
  from typing import Optional, Union
4
5
 
@@ -60,7 +61,10 @@ def _ensure_git_config(repo: Repo) -> None:
60
61
 
61
62
 
62
63
  def init_git_repo(
63
- path_to_repo: Union[str, os.PathLike], module_name: Optional[str] = None, render_id: Optional[str] = None
64
+ path_to_repo: Union[str, os.PathLike],
65
+ module_name: Optional[str] = None,
66
+ render_id: Optional[str] = None,
67
+ initial_files: Optional[dict[str, str]] = None,
64
68
  ) -> Repo:
65
69
  """
66
70
  Initializes a new git repository in the given path.
@@ -74,6 +78,11 @@ def init_git_repo(
74
78
 
75
79
  repo = Repo.init(path_to_repo)
76
80
  _ensure_git_config(repo)
81
+
82
+ if initial_files:
83
+ file_utils.store_response_files(path_to_repo, initial_files, [])
84
+ repo.git.add(".")
85
+
77
86
  repo.git.commit(
78
87
  "--allow-empty", "-m", _get_full_commit_message(INITIAL_COMMIT_MESSAGE, module_name, None, render_id)
79
88
  )
@@ -82,9 +91,18 @@ def init_git_repo(
82
91
 
83
92
 
84
93
  def clone_repo(
85
- source_repo_path: str, new_repo_path: str, module_name: Optional[str] = None, render_id: Optional[str] = None
94
+ source_repo_path: str,
95
+ new_repo_path: str,
96
+ module_name: Optional[str] = None,
97
+ render_id: Optional[str] = None,
98
+ initial_files: Optional[dict[str, str]] = None,
86
99
  ) -> Repo:
87
100
  repo = Repo.clone_from(source_repo_path, new_repo_path)
101
+
102
+ if initial_files:
103
+ file_utils.store_response_files(new_repo_path, initial_files, [])
104
+ repo.git.add(".")
105
+
88
106
  repo.git.commit(
89
107
  "--allow-empty", "-m", _get_full_commit_message(INITIAL_COMMIT_MESSAGE, module_name, None, render_id)
90
108
  )
@@ -415,3 +433,53 @@ def get_repo_info(repo_path: Union[str, os.PathLike]) -> dict:
415
433
  info["remotes"] = remotes
416
434
 
417
435
  return info
436
+
437
+
438
+ def get_last_rendered_functionality(repo_path: Union[str, os.PathLike]) -> tuple[Optional[str], Optional[str]]:
439
+ if not os.path.exists(repo_path):
440
+ return None, None
441
+
442
+ repo = Repo(repo_path)
443
+ grep_pattern = FUNCTIONAL_REQUIREMENT_FINISHED_COMMIT_MESSAGE.format(".*")
444
+ grep_pattern = grep_pattern.replace("[", "\\[").replace("]", "\\]")
445
+ commit_sha = repo.git.rev_list(repo.active_branch.name, "--grep", grep_pattern, "-n", "1")
446
+
447
+ if not commit_sha:
448
+ # Repo was interrupted during the first functionality, fallback to initial commit and provide only module name
449
+ grep_pattern = INITIAL_COMMIT_MESSAGE
450
+ grep_pattern = grep_pattern.replace("[", "\\[").replace("]", "\\]")
451
+ commit_sha = repo.git.rev_list(repo.active_branch.name, "--grep", grep_pattern, "-n", "1")
452
+ if not commit_sha:
453
+ raise InvalidGitRepositoryError("Git repository is in an invalid state. Initial commit could not be found.")
454
+
455
+ commit_message = repo.commit(commit_sha).message
456
+ if isinstance(commit_message, bytes):
457
+ commit_message = commit_message.decode("utf-8")
458
+
459
+ match = re.search(r"Module name:\s*(\S+)\n", commit_message)
460
+ if not match:
461
+ raise InvalidGitRepositoryError(
462
+ "Git repository is in an invalid state. Could not find module name in initial commit."
463
+ )
464
+
465
+ module_name = match.group(1)
466
+ return module_name, None
467
+
468
+ commit_message = repo.commit(commit_sha).message
469
+ if isinstance(commit_message, bytes):
470
+ commit_message = commit_message.decode("utf-8")
471
+
472
+ match = re.search(r"FRID\):(\S+) fully implemented", commit_message)
473
+ if not match:
474
+ raise InvalidGitRepositoryError(
475
+ "Git repository is in an invalid state. Could not find frid in finished commit."
476
+ )
477
+ frid = match.group(1)
478
+
479
+ match = re.search(r"Module name:\s*(\S+)\n", commit_message)
480
+ if not match:
481
+ raise InvalidGitRepositoryError(
482
+ "Git repository is in an invalid state. Could not find module name in finished commit."
483
+ )
484
+ module_name = match.group(1)
485
+ return module_name, frid
memory_management.py CHANGED
@@ -33,18 +33,29 @@ class MemoryManager:
33
33
  ):
34
34
 
35
35
  current_conformance_tests_issue_frid = render_context.conformance_tests_running_context.current_testing_frid
36
+ current_conformance_tests_issue_module = (
37
+ render_context.conformance_tests_running_context.current_testing_module_name
38
+ )
36
39
  old_conformance_tests_issue_frid = (
37
40
  render_context.conformance_tests_running_context.previous_conformance_tests_issue_frid
38
41
  )
42
+ old_conformance_tests_issue_module = (
43
+ render_context.conformance_tests_running_context.previous_conformance_tests_issue_module
44
+ )
39
45
 
40
46
  old_conformance_tests_issue = (
41
47
  render_context.conformance_tests_running_context.previous_conformance_tests_issue_old
42
48
  )
43
49
 
44
50
  is_first_time_running_conformance_tests = (
45
- old_conformance_tests_issue_frid is None or old_conformance_tests_issue_frid == ""
51
+ old_conformance_tests_issue_frid is None
52
+ or old_conformance_tests_issue_frid == ""
53
+ or old_conformance_tests_issue_module != current_conformance_tests_issue_module
54
+ )
55
+ is_same_frid_as_previous_failing_test = (
56
+ current_conformance_tests_issue_frid == old_conformance_tests_issue_frid
57
+ and current_conformance_tests_issue_module == old_conformance_tests_issue_module
46
58
  )
47
- is_same_frid_as_previous_failing_test = current_conformance_tests_issue_frid == old_conformance_tests_issue_frid
48
59
  is_conformance_test_failed = exit_code != CONFORMANCE_TESTS_SUCCESS_EXIT_CODE
49
60
 
50
61
  should_create_memory = not is_first_time_running_conformance_tests and (