codeplain 0.3.1__py3-none-any.whl → 0.3.2__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.
_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.3.1"
1
+ __version__ = "0.3.2"
@@ -15,7 +15,12 @@ def print_exit_summary(
15
15
  """Print render outcome after the TUI exits (terminal restored)."""
16
16
  console.quiet = False
17
17
 
18
- msg = "\n[#79FC96]✓ rendering completed\n\n" if run_state.render_succeeded else "[#FF6B6B]✗ rendering failed\n\n"
18
+ if run_state.render_succeeded:
19
+ msg = "\n[#79FC96]✓ rendering completed\n\n"
20
+ elif run_state.render_cancelled:
21
+ msg = "\n[#FFFFFF]— rendering canceled\n\n"
22
+ else:
23
+ msg = "\n[#FF6B6B]✗ rendering failed\n\n"
19
24
  msg += f" [#8E8F91]render id:\t\t\t[#FFFFFF]{run_state.render_id}\n"
20
25
  msg += f" [#8E8F91]input file:\t\t\t[#FFFFFF]{spec_filename}\n"
21
26
  msg += f" [#8E8F91]generated code folder:\t[#FFFFFF]{run_state.render_generated_code_path or '-'}\n\n"
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codeplain
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
7
7
  Classifier: Intended Audience :: Developers
8
8
  Classifier: Operating System :: OS Independent
9
9
  Classifier: Topic :: Software Development :: Code Generators
10
- Requires-Python: ==3.11
10
+ Requires-Python: >=3.11
11
11
  Requires-Dist: gitpython==3.1.50
12
12
  Requires-Dist: mistletoe==1.3.0
13
13
  Requires-Dist: networkx==3.6.1
@@ -29,31 +29,28 @@ Description-Content-Type: text/markdown
29
29
 
30
30
  # Codeplain plain2code renderer
31
31
 
32
- Render ***plain source to software code using the Codeplain API.
32
+ Render \*\*\*plain source to software code using the Codeplain API.
33
33
 
34
34
  ## Codeplain.ai - Code Generation as a Service
35
35
 
36
- Codeplain is a platform that generates software code using large language models based on requirements you specify in ***plain specification language.
36
+ Codeplain is a platform that generates software code using large language models based on requirements you specify in \*\*\*plain specification language.
37
37
 
38
38
  Schematic overview of the Codeplain's code generation service
39
39
 
40
40
  <img src="https://raw.githubusercontent.com/Codeplain-ai/plain2code_client/main/resources/codeplain_overview.png">
41
41
 
42
- ### Abstracting Away Code Generation Complexity with ***plain
42
+ ### Abstracting Away Code Generation Complexity with \*\*\*plain
43
43
 
44
+ \*\*\*plain is a novel specification language that helps abstracting away complexity of using large language models for code generation.
44
45
 
45
- ***plain is a novel specification language that helps abstracting away complexity of using large language models for code generation.
46
-
47
- An example application in ***plain
46
+ An example application in \*\*\*plain
48
47
 
49
48
  <img src="resources/plain_example.png" width="70%" height="70%">
50
49
 
51
-
52
50
  ## Getting started
53
51
 
54
52
  ### Prerequisites
55
53
 
56
-
57
54
  #### System requirements
58
55
 
59
56
  To run the plain2code client, you need Python 3.11 or a later version.
@@ -92,18 +89,19 @@ export CODEPLAIN_API_KEY="your_actual_api_key_here"
92
89
  After completing the installation steps above, you can immediately test the system with a simple "Hello World" example:
93
90
 
94
91
  - Change to the example folder and run the example:
95
- ```
96
- cd examples/example_hello_world_python
97
- python ../../plain2code.py hello_world_python.plain
98
- ```
99
92
 
100
- *Note: Rendering will take a few minutes to complete.*
93
+ ```
94
+ cd examples/example_hello_world_python
95
+ python ../../plain2code.py hello_world_python.plain
96
+ ```
97
+
98
+ _Note: Rendering will take a few minutes to complete._
101
99
 
102
100
  - The system will generate a Python application in the `build` directory. You can run it with:
103
- ```
104
- cd build
105
- python hello_world.py
106
- ```
101
+ ```
102
+ cd build
103
+ python hello_world.py
104
+ ```
107
105
 
108
106
  ## Releasing
109
107
 
@@ -112,7 +110,7 @@ Releases are built and published with [uv](https://docs.astral.sh/uv/). The vers
112
110
  1. Bump the version in `_version.py` and commit/tag (`git tag v<version>`).
113
111
  2. Build the distributions:
114
112
  ```bash
115
- uv build
113
+ uv build --python 3.11
116
114
  ```
117
115
  This produces `dist/codeplain-<version>-py3-none-any.whl` and `dist/codeplain-<version>.tar.gz`.
118
116
  3. (Optional) Smoke-test the wheel:
@@ -135,13 +133,11 @@ Releases are built and published with [uv](https://docs.astral.sh/uv/). The vers
135
133
  ### Examples and Sample Projects
136
134
 
137
135
  - See the [examples](examples) folder for sample projects in Golang, Python, and React.
138
- - For example application how to implement task manager in ***plain see [example-task-manager](https://github.com/Codeplain-ai/example-task-manager) repository.
139
- - For example application how to implement SaaS connectors in ***plain see [example-saas-connectors](https://github.com/Codeplain-ai/example-saas-connectors) repository.
136
+ - For example application how to implement task manager in \*\*\*plain see [example-task-manager](https://github.com/Codeplain-ai/example-task-manager) repository.
137
+ - For example application how to implement SaaS connectors in \*\*\*plain see [example-saas-connectors](https://github.com/Codeplain-ai/example-saas-connectors) repository.
140
138
 
141
139
  ### Documentation
142
140
 
143
141
  - For more details on the ***plain format, see the [***plain language specification](https://www.plainlang.org/docs/).
144
142
  - 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).
145
143
  - For complete CLI documentation and usage examples, see [plain2code CLI documentation](docs/plain2code_cli.md).
146
-
147
-
@@ -1,31 +1,30 @@
1
- _version.py,sha256=r4xAFihOf72W9TD-lpMi6ntWSTKTP2SlzKP1ytkjRbI,22
2
- codeplain_REST_api.py,sha256=5O7-PgdxDZZZ95tq4tDyaYeouQdwedU3P6KBeJ0krwo,21550
3
- concept_utils.py,sha256=VLHur80-XSJTveClLiHb-CMuqsXd8sIQAr0Ehe1Hid0,7930
1
+ _version.py,sha256=vNiWJ14r_cw5t_7UDqDQIVZvladKFGyHH2avsLpN7Vg,22
2
+ codeplain_REST_api.py,sha256=h_XoNws8fvf7OTtoVXhHh6OfLGg77UNFuggMBwaqBwE,21533
3
+ concept_utils.py,sha256=D4qVHFBRvfJZwiIq1AD_SV0Ev-RnOMg2yGWo6Cal7PM,7913
4
4
  diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
5
5
  event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
6
- file_utils.py,sha256=eO_SHwyjJlkwT3wTlIjIEQ3t8y3k0i-fWKAn5_1SMkY,12597
6
+ file_utils.py,sha256=grPx2Rl6-q2ijSRoaAOZcsrpOjTqb4i_mtF-jzwhKcQ,10953
7
7
  git_utils.py,sha256=LQQnL5DAFpH5fDBQgUZX2WfjWY4YC5OEGBPIXryTmnY,16896
8
- hash_key.py,sha256=lMKgKpOhPeC4UpFf9e7eCNK20FEaDsXTCJ8j3eCWidE,837
9
8
  memory_management.py,sha256=nHNudgaOqp5UGkIMi62TRCc2nUeFOM0w-I1FfxBFaQE,6120
10
9
  module_renderer.py,sha256=pXMXKKGMhUwLLlSxppE_hpZlwpJ3WSHSfR9FQxt6rSI,6860
11
10
  partial_rendering.py,sha256=prWEJo9m1TbF7SKzpNInbcefyTF0o3oXE7Qf1l4SmBs,8409
12
- plain2code.py,sha256=eYmXKAsdvladjoG6ujLWpZCXIOq5FsIabBkCwZd2tDs,13767
11
+ plain2code.py,sha256=gyPeZtLe7VkGChWwGVNNZRXvkqpH8uC27szHbrZgmeY,14010
13
12
  plain2code_arguments.py,sha256=J05NwaY-O04rIWVF2yIXysNB_ofZS0wJFn0u1WaCCSA,15399
14
13
  plain2code_console.py,sha256=YdE-Kz9qRAzIIAg7aYA1bnXRcwy_aTpbFtkR2zHbd-I,4918
15
14
  plain2code_events.py,sha256=AM8KJS68uiVXRMLt861KaGhs2H4yzujlMB_xiYPfHQc,1478
16
- plain2code_exceptions.py,sha256=g3wMOnjpQlk2pdKRvVarnuh9BlOsRV2AKm_CoNidmH4,1629
15
+ plain2code_exceptions.py,sha256=Fj1pwCffn06aivEplsTJJGdg6lAOR8gmWd4ctLl9cj4,1745
17
16
  plain2code_logger.py,sha256=kNKIfkS6Ydtut7Ptw5jmv5bFPgXlxB-_Ds5nd-j4d9o,4609
18
17
  plain2code_nodes.py,sha256=IwjQqJxRSJKgP6eEnkYGy3VQHIyoazgkFxAEkxkjfJ8,4426
19
18
  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=QwSjX2SC62bQBR-UZM8NSzlepwEs3uSPXgiCqdjoWy4,14281
19
+ plain2code_state.py,sha256=gmqbGfM2dx6ieiqRQRlLPD49JsMAyb3lOWspljcTj9g,1944
20
+ plain2code_utils.py,sha256=HYDJ4lTC3RwBgcJadPvcoU3-Y6krNiuDP4_t2xYKA2Q,874
21
+ plain_file.py,sha256=vJKCkIA12o7LHAGugIsTbzcpoQrTQX2uqlXEC_EqfPo,29470
22
+ plain_modules.py,sha256=NXUtxW8fEpJAuEIeVE_iwRBCbHFh2ji-p9xdkgojLqU,14423
24
23
  plain_spec.py,sha256=E2Yp2eXMExE7kx7GPQ7v9boHN8XhFUcqprn1DGMaFMs,15669
25
- system_config.py,sha256=DMyfiq_uzxWH_uXz5vMWNHkcRFPkIA-M8nr64SeVhPo,1350
24
+ system_config.py,sha256=trgytfXKXoCn0Pgy9u00x2D0ZRHlh_tADlbkwZnF8QE,1038
26
25
  cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
27
26
  cli_output/dry_run.py,sha256=6NKhNWPtD0CBK_QHSsSqIKLTzibTwxS9d8ltP9d1bgU,1473
28
- cli_output/render_summary.py,sha256=X9wHvyaQa8niO87GR2gnzI0rwF6XU55OktwzgmMpJaQ,1186
27
+ cli_output/render_summary.py,sha256=QrW3Iq15xUoWXuVXpqClAKncAvHdQR0lWG1_OMUBsWk,1307
29
28
  cli_output/status.py,sha256=5jr9XFU1MWD3pN404qw7SNYCGdvqfA9j4xv9z1tZG4s,5890
30
29
  config/__init__.py,sha256=beYSsJWmBNHDP5rYmVDouqgEeP3t1lkkepbXJ-oq0F8,37
31
30
  config/system_config.yaml,sha256=of5R9vukSohU9AlkGJOcAVQA1UJgxYmDy6islU6iFuE,508
@@ -33,11 +32,10 @@ docs/generate_cli.py,sha256=0FHVhICbM8g7ahdInTNih3hNKpty1wbV5rdGECMJj48,656
33
32
  examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py,sha256=dwTowrHiVKKbrDv21v8xJC30Q57AXZkQasdGOO5JsBE,470
34
33
  render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
34
  render_machine/code_renderer.py,sha256=bLdoZcWaLGvuR0GygnWY9y9kycGySD-IEWY5DFGcGsg,3781
36
- render_machine/conformance_test_helpers.py,sha256=6Ru7Dh24SLNIKWfz_sP5hE_EmWpvk6nfgN1T1yaCZus,2887
37
35
  render_machine/conformance_tests.py,sha256=MeEveEGJTClLGxWw04P4TR6U9kX9Qgz9qtxNrB9a8QI,7085
38
36
  render_machine/implementation_code_helpers.py,sha256=LdHNLoKgVtrDaGH2APX8C9vjNjLBUhu2V7OAaJvm4rM,2009
39
- render_machine/render_context.py,sha256=ke2HWKNTh3QoPLv_T3b-8ohrM2mzqsCtlP3lsk011uA,28921
40
- render_machine/render_types.py,sha256=iqRd9MyrRnXasoA8rvssZbCN66BbVCVXH5uFA8QJVXY,9222
37
+ render_machine/render_context.py,sha256=NHDtJkhQP8tW0iyRH6PR7oSk-PO_pEGMKkX38x52YMc,29128
38
+ render_machine/render_types.py,sha256=cT3D0lmxQ3enIo0jGp3LEGCY2t3X1Q1_tqwcadzSAv0,8761
41
39
  render_machine/render_utils.py,sha256=ao0e64VSOGYfb8hwJXIZlbqer5HjNMYJWHlLDoHnJyc,9123
42
40
  render_machine/state_machine_config.py,sha256=uqrU9Pu4cAKxu_-e1eiuUQK-tZWxZko0U0Bfvl67v_c,28813
43
41
  render_machine/states.py,sha256=niFXqayyA_rezCrlzo0hYeHAruErcPzQVF-BeIBGnL8,1968
@@ -56,8 +54,8 @@ render_machine/actions/prepare_testing_environment.py,sha256=GQNVzc_Ap5n74OAmeQu
56
54
  render_machine/actions/refactor_code.py,sha256=_1C16bQbpxdzJkEwl8jDhEKVyXcyGTd-5YYDNTq2vA8,1782
57
55
  render_machine/actions/render_conformance_tests.py,sha256=5gOQn-G1T9UWEI8sDfALcVdArk0ln9DgUprOPjccS-U,9268
58
56
  render_machine/actions/render_functional_requirement.py,sha256=2SbPGsLbrrQx3kasXpSpphzaMsq9GD8ASnSzvIdodT4,3748
59
- render_machine/actions/run_conformance_tests.py,sha256=jmpB-yklUeUjywSpOfbOM0bFGOAiXBQkZYh13prwL6Q,3976
60
- render_machine/actions/run_unit_tests.py,sha256=-X7IOrZ7d9fuy_pL7CiON5wU1mI4ccSLuMyq-768aD8,2093
57
+ render_machine/actions/run_conformance_tests.py,sha256=4sJTORl6YLFXQIdLMBQHDeJd2hztGyt3tsVvrCviBko,4008
58
+ render_machine/actions/run_unit_tests.py,sha256=A4WXtSzpCLDHYR8fQAbfxUf_E19GYflZSHZaSreoFPk,2101
61
59
  render_machine/actions/summarize_conformance_tests.py,sha256=Vy1kRK8vyazK_BbsKuz1MLDN_TQWVxxzkwEEm-50byc,1639
62
60
  standard_template_library/__init__.py,sha256=LtsHl0c3VV46DUInIqyOw4KKVoxKEMOx8dAZpMXtAXc,43
63
61
  standard_template_library/golang-console-app-template.plain,sha256=JOusEjCj7jagnXUzaI-SdIr3hj1wTo2uShK017aNtVM,949
@@ -66,14 +64,14 @@ standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOz
66
64
  standard_template_library/typescript-react-app-template.plain,sha256=DN5wEJcT8ZqQTf0vFKi9Z1isi9cin2UVNOZb3r28d3M,715
67
65
  tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
68
66
  tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
69
- tests/test_cli_output.py,sha256=Zh4-zWXnW5Znjoj7L33CUYUjNqYzXyFScvWhKPIRn4A,14504
67
+ tests/test_cli_output.py,sha256=YdIQtplncTeMf82E0nWNrGYoYAFI8Man4zneYkWmbxk,14425
70
68
  tests/test_file_utils.py,sha256=-O5zImiFVz9JJ4xi8TbsUwNgPFQoQUOl9Frls4BHCBI,1358
71
69
  tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
72
70
  tests/test_imports.py,sha256=EWYLUxGunUUdsG9iCiY_tts6T7DIVdhdB_MZlB1LsmU,2081
73
71
  tests/test_partial_rendering.py,sha256=4xXZhy9yStn0awSw8B-yrJ3kVYyzJrlR-dKwg9ODXMY,11188
74
72
  tests/test_plain_modules.py,sha256=9bWsketDnjpCWkCbMZzYvLqT1ojPhLyxxKljPUq42oY,11393
75
73
  tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
76
- tests/test_plainfileparser.py,sha256=iVTAJWxQZbv96uHXe-qvGSgsC9uJk5Tu10Nod3kqflI,18666
74
+ tests/test_plainfileparser.py,sha256=7AassxH7AKX4lfW6E_58FYxRe-IzIiA16BMVWPjWqCM,18750
77
75
  tests/test_plainspec.py,sha256=JrzjJ4VFYDmWTZDq-8WFyt2oMx76IEuO1CAUPQeGOsw,2404
78
76
  tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
79
77
  tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
@@ -81,16 +79,16 @@ tests/e2e/conftest.py,sha256=5Dm67UTOE-M6hMb-mqZvwa6Ek_wmZgsR7fUv9ZEqCwY,4164
81
79
  tests/e2e/test_hello_world_python.py,sha256=PQy4elzB7WvpiWLKa6_HFO0K8HA6_6hGLaAqRi0J3OU,1627
82
80
  tests/e2e/test_hello_world_python_windows.py,sha256=kJQZIuzj8OEEtgBVxnzFvyQJ_wvP2oBu9L-E9geg8sE,2010
83
81
  tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
84
- tui/components.py,sha256=G6ktJx211WrYmj-1aZL3mFtnBTOE4WRF3hA1LG8J0jU,25810
82
+ tui/components.py,sha256=DoMDhwNVVpv--OY6o8YQYPzOQPG9Dyh9kgMOqUYtNiE,25412
85
83
  tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
86
- tui/plain2code_tui.py,sha256=Migz-ZdylT8I5ilgvPXxIsTvF30EMC18jyMBGMdattI,13608
87
- tui/plain_module_render_choice_tui.py,sha256=8aQBwjFmKoKOo-s_sCWuEs4ax1Ww0GKadbMiZAdpKSM,9366
84
+ tui/plain2code_tui.py,sha256=t-0DonjrK6_x6jPUMYW0w6tvjZZlk89E-DEor7GzjG4,13785
85
+ tui/plain_module_render_choice_tui.py,sha256=MLc-Ps3O2DMBKRCPJAxUY059SdDIYq2ha7-sWxuguj0,9611
88
86
  tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
89
87
  tui/state_handlers.py,sha256=4r_6m5YMcprQtgiTP0De3XFfGAF8VzgaWXHxPweYPbQ,16325
90
- tui/styles.css,sha256=9vI6V7n7p76i2Dn62KDyOkRYp__d3v2bCzYxO9PWnm8,6685
91
- tui/widget_helpers.py,sha256=DdXCAJEvFVMgLM2GNBqE-yMtgoNnk7sHG6ehaEV3zdk,6577
92
- codeplain-0.3.1.dist-info/METADATA,sha256=Rg-b_OM3ViTIsGXtcIk55V_ZXLoKw3lKhA1r3BaAFLw,5112
93
- codeplain-0.3.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
94
- codeplain-0.3.1.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
95
- codeplain-0.3.1.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
96
- codeplain-0.3.1.dist-info/RECORD,,
88
+ tui/styles.css,sha256=c13WY1nOgmtkT_FH9Ad2PjXrfi8LYwr6zzD6rBOh9tU,6685
89
+ tui/widget_helpers.py,sha256=mWDPrALsxLOzJg8KD9MXXafJKRiqG-EIp797jbOT77o,5972
90
+ codeplain-0.3.2.dist-info/METADATA,sha256=_m-JTZCg09Ur5aGhureD24GwhQpQravmQnsFxfT37bc,5134
91
+ codeplain-0.3.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
92
+ codeplain-0.3.2.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
93
+ codeplain-0.3.2.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
94
+ codeplain-0.3.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.29.0
2
+ Generator: hatchling 1.30.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
codeplain_REST_api.py CHANGED
@@ -108,7 +108,7 @@ class CodeplainAPI:
108
108
  if error_code == "InternalServerError":
109
109
  raise plain2code_exceptions.InternalServerError(
110
110
  "Internal server error.\n\n"
111
- f"Please report the error to support@codeplain.ai with the attached {self.log_file_name} file."
111
+ "Please report the error to support@codeplain.ai with the attached .log file."
112
112
  )
113
113
  exception_class = ERROR_CODE_EXCEPTIONS[error_code]
114
114
  raise exception_class(message)
concept_utils.py CHANGED
@@ -2,7 +2,6 @@ import re
2
2
  from copy import deepcopy
3
3
  from typing import Optional
4
4
 
5
- import mistletoe
6
5
  import mistletoe.block_token
7
6
  import networkx as nx
8
7
 
file_utils.py CHANGED
@@ -54,15 +54,6 @@ def is_system_folder_path(file_path: str) -> bool:
54
54
  return bool(parts) and parts[0] in SYSTEM_FOLDERS
55
55
 
56
56
 
57
- def get_file_type(file_name):
58
-
59
- # Extract the file extension
60
- ext = Path(file_name).suffix.lower() # Convert to lowercase to handle case-insensitive matching
61
-
62
- # Use the dictionary to get the file type, defaulting to 'unknown' if the extension is not found
63
- return FILE_EXTENSION_MAPPING.get(ext, "unknown")
64
-
65
-
66
57
  def list_all_text_files(directory):
67
58
  all_files = []
68
59
  for root, dirs, files in os.walk(directory, topdown=True):
@@ -99,64 +90,29 @@ def list_folders_in_directory(directory):
99
90
  return folders
100
91
 
101
92
 
102
- # delete a folder and all its subfolders and files
103
- def delete_folder(folder_name):
104
- if os.path.exists(folder_name):
105
- # Use writable-aware deletion so read-only files (e.g. .git objects on Windows) don't cause PermissionError
106
- delete_files_and_subfolders(folder_name)
107
- _make_writable(folder_name)
108
- os.rmdir(folder_name)
93
+ def _on_rm_error(func, path, _exc_info):
94
+ """On Windows, clear read-only flag and retry the removal."""
95
+ os.chmod(path, stat.S_IWRITE)
96
+ func(path)
109
97
 
110
98
 
111
- def _make_writable(path: str) -> None:
112
- """On Windows, clear read-only so deletion can succeed."""
113
- # TODO - Check if this can be done in a cleaner way.
114
- is_windows = os.name == "nt"
115
- if is_windows:
116
- try:
117
- mode = os.stat(path).st_mode
118
- os.chmod(path, mode | stat.S_IWRITE)
119
- except OSError:
120
- pass
99
+ def delete_folder(folder_name):
100
+ """Delete a folder and all its subfolders and files."""
101
+ if os.path.exists(folder_name):
102
+ shutil.rmtree(folder_name, onerror=_on_rm_error)
121
103
 
122
104
 
123
105
  def delete_files_and_subfolders(directory):
124
- total_files_deleted = 0
125
- total_folders_deleted = 0
126
-
127
- # Walk the directory in reverse order (bottom-up)
128
- for root, dirs, files in os.walk(directory, topdown=False):
129
- # Delete files
130
- for file in files:
131
- file_path = os.path.join(root, file)
132
- _make_writable(file_path)
133
- os.remove(file_path)
134
- total_files_deleted += 1
135
-
136
- # Delete directories
137
- for dir_ in dirs:
138
- dir_path = os.path.join(root, dir_)
139
- _make_writable(dir_path)
140
- os.rmdir(dir_path)
141
- total_folders_deleted += 1
142
-
143
-
144
- def copy_file(source_path, destination_path):
145
- # Ensure the destination directory exists
146
- os.makedirs(os.path.dirname(destination_path), exist_ok=True)
147
-
148
- # Open the source file in read-binary ('rb') mode
149
- with open(source_path, "rb") as source_file:
150
- # Open the destination file in write-binary ('wb') mode
151
- with open(destination_path, "wb") as destination_file:
152
- # Copy the content from source to destination
153
- while True:
154
- # Read a chunk of the source file
155
- chunk = source_file.read(4096) # Reading in chunks of 4KB
156
- if not chunk:
157
- break # End of file reached
158
- # Write the chunk to the destination file
159
- destination_file.write(chunk)
106
+ """Delete all contents of a directory but keep the directory itself."""
107
+ for entry in os.scandir(directory):
108
+ if entry.is_dir(follow_symlinks=False):
109
+ shutil.rmtree(entry.path, onerror=_on_rm_error)
110
+ else:
111
+ try:
112
+ os.remove(entry.path)
113
+ except PermissionError:
114
+ os.chmod(entry.path, stat.S_IWRITE)
115
+ os.remove(entry.path)
160
116
 
161
117
 
162
118
  def add_current_path_if_no_path(filename):
plain2code.py CHANGED
@@ -25,6 +25,7 @@ from plain2code_console import console
25
25
  from plain2code_events import RenderFailed
26
26
  from plain2code_exceptions import (
27
27
  ConflictingRequirements,
28
+ GitNotInstalledError,
28
29
  InvalidAPIKey,
29
30
  InvalidFridArgument,
30
31
  MissingAPIKey,
@@ -181,6 +182,7 @@ def render(args, run_state: RunState, event_bus: EventBus, default_log_level: st
181
182
  render_choices,
182
183
  system_config.client_version,
183
184
  run_state.render_id,
185
+ on_cancel=run_state.set_render_cancelled,
184
186
  css_path="styles.css",
185
187
  )
186
188
  render_choice = app.run()
@@ -189,6 +191,7 @@ def render(args, run_state: RunState, event_bus: EventBus, default_log_level: st
189
191
  and render_choice.render_range is None
190
192
  and render_choice.choice_type == "quit"
191
193
  ):
194
+ run_state.set_render_cancelled()
192
195
  sys.exit(0)
193
196
  elif ask_user and args.headless:
194
197
  # ignore the default choice if it requires user input due to headless mode
@@ -217,7 +220,7 @@ def render(args, run_state: RunState, event_bus: EventBus, default_log_level: st
217
220
  try:
218
221
  module_renderer.render_module()
219
222
  except RenderCancelledError:
220
- pass # TUI already closed, nothing to report
223
+ run_state.set_render_cancelled() # TUI already closed, nothing to report
221
224
  except Exception as e:
222
225
  run_state.set_render_succeeded(False)
223
226
  render_error.append(e)
@@ -228,8 +231,7 @@ def render(args, run_state: RunState, event_bus: EventBus, default_log_level: st
228
231
  try:
229
232
  module_renderer.render_module()
230
233
  except RenderCancelledError:
231
- run_state.set_render_succeeded(False)
232
- pass
234
+ run_state.set_render_cancelled()
233
235
  return
234
236
  else:
235
237
  render_thread = threading.Thread(target=run_render, daemon=True)
@@ -242,6 +244,7 @@ def render(args, run_state: RunState, event_bus: EventBus, default_log_level: st
242
244
  prepare_environment_script=args.prepare_environment_script,
243
245
  state_machine_version=system_config.client_version,
244
246
  enter_pause_event=enter_pause_event,
247
+ on_cancel=run_state.set_render_cancelled,
245
248
  default_log_level=default_log_level,
246
249
  css_path="styles.css",
247
250
  )
@@ -354,6 +357,7 @@ def main(): # noqa: C901
354
357
  NetworkConnectionError,
355
358
  ModuleDoesNotExistError,
356
359
  UnsupportedResourceType,
360
+ GitNotInstalledError,
357
361
  ),
358
362
  ):
359
363
  exc_info = sys.exc_info()
plain2code_exceptions.py CHANGED
@@ -89,3 +89,9 @@ class RenderCancelledError(Exception):
89
89
  """Raised when the render is cancelled by the user closing the TUI."""
90
90
 
91
91
  pass
92
+
93
+
94
+ class GitNotInstalledError(Exception):
95
+ """Raised when git is not installed or not found on PATH."""
96
+
97
+ pass
plain2code_state.py CHANGED
@@ -11,6 +11,7 @@ class RunState:
11
11
  def __init__(self, spec_filename: str, replay_with: Optional[str] = None):
12
12
  self.replay: bool = replay_with is not None
13
13
  self.render_succeeded: bool = False
14
+ self.render_cancelled: bool = False
14
15
  self.render_generated_code_path: Optional[str] = None
15
16
  self.rendered_functionalities: int = 0
16
17
  if replay_with:
@@ -20,7 +21,6 @@ class RunState:
20
21
  self.spec_filename: str = spec_filename
21
22
  self.call_count: int = 0
22
23
  self.unittest_batch_id: int = 0
23
- self.frid_render_anaysis: dict[str, str] = {}
24
24
  self.render_time_accumulated: int = 0
25
25
  self.last_render_start_timestamp: float = time.monotonic()
26
26
 
@@ -30,12 +30,12 @@ class RunState:
30
30
  def increment_unittest_batch_id(self):
31
31
  self.unittest_batch_id += 1
32
32
 
33
- def add_rendering_analysis_for_frid(self, frid, rendering_analysis) -> None:
34
- self.frid_render_anaysis[frid] = rendering_analysis
35
-
36
33
  def set_render_succeeded(self, succeeded: bool):
37
34
  self.render_succeeded = succeeded
38
35
 
36
+ def set_render_cancelled(self):
37
+ self.render_cancelled = True
38
+
39
39
  def set_render_generated_code_path(self, generated_code_path: str):
40
40
  self.render_generated_code_path = generated_code_path
41
41
 
@@ -55,6 +55,3 @@ class RunState:
55
55
  "replay": self.replay,
56
56
  "spec_filename": self.spec_filename,
57
57
  }
58
-
59
- def get_render_func_id(self, frid: str) -> str:
60
- return f"{self.render_id}-{frid}"
plain2code_utils.py CHANGED
@@ -1,9 +1,3 @@
1
- from typing import Optional
2
-
3
- import plain_spec
4
- from plain2code_console import console
5
-
6
-
7
1
  def format_duration_hms(total_seconds: int) -> str:
8
2
  """Format a duration in seconds as hours, minutes, and seconds (e.g. ``1h 2m 3.45s``, ``45.67s``)."""
9
3
  if total_seconds < 0:
@@ -26,31 +20,3 @@ AMBIGUITY_CAUSES = {
26
20
  "functional_requirement_ambiguity": "Ambiguity is in the functionality",
27
21
  "other": "Ambiguity in the other parts of the specification",
28
22
  }
29
-
30
-
31
- def print_dry_run_output(plain_source_tree: dict, render_range: Optional[list[str]]):
32
- frid = plain_spec.get_first_frid(plain_source_tree)
33
-
34
- while frid is not None:
35
- is_inside_range = render_range is None or frid in render_range
36
-
37
- if is_inside_range:
38
- specifications, _ = plain_spec.get_specifications_for_frid(plain_source_tree, frid)
39
- functional_requirement_text = specifications[plain_spec.FUNCTIONAL_REQUIREMENTS][-1]
40
- console.info(
41
- "-------------------------------------\n"
42
- f"Rendering functionality {frid}:\n"
43
- f"{functional_requirement_text}\n"
44
- "-------------------------------------\n"
45
- )
46
- if plain_spec.ACCEPTANCE_TESTS in specifications:
47
- for i, acceptance_test in enumerate(specifications[plain_spec.ACCEPTANCE_TESTS], 1):
48
- console.info(f"Generating acceptance test #{i}:\n\n{acceptance_test}\n")
49
- else:
50
- console.info(
51
- "-------------------------------------\n"
52
- f"Skipping rendering iteration: {frid}\n"
53
- "-------------------------------------"
54
- )
55
-
56
- frid = plain_spec.get_next_frid(plain_source_tree, frid)
plain_file.py CHANGED
@@ -91,6 +91,12 @@ def check_section_for_linked_resources(section):
91
91
  f"Plain syntax error: Only relative links are allowed (text: {link.node.children[0].content}, target: {link.node.target})."
92
92
  )
93
93
 
94
+ if not os.path.exists(link.node.target):
95
+ raise PlainSyntaxError(f"Plain syntax error: Link {link.node.target} does not exist.")
96
+
97
+ if not os.path.isfile(link.node.target):
98
+ raise PlainSyntaxError(f"Plain syntax error: Link {link.node.target} must be a file.")
99
+
94
100
  if len(link.node.children) != 1:
95
101
  raise PlainSyntaxError(f"Plain syntax error: Link must have text specified (link: {link.node.target}).")
96
102
 
plain_modules.py CHANGED
@@ -5,13 +5,17 @@ import os
5
5
  import shutil
6
6
  from functools import cached_property
7
7
 
8
- from git.exc import NoSuchPathError
8
+ from plain2code_exceptions import GitNotInstalledError, MissingPreviousFunctionalitiesError, ModuleDoesNotExistError
9
+
10
+ try:
11
+ from git.exc import NoSuchPathError
12
+ except ImportError:
13
+ raise GitNotInstalledError("git is not installed. Please install git and try again.")
9
14
 
10
15
  import git_utils
11
16
  import plain_file
12
17
  import plain_spec
13
18
  from plain2code_console import console
14
- from plain2code_exceptions import MissingPreviousFunctionalitiesError, ModuleDoesNotExistError
15
19
  from render_machine.implementation_code_helpers import ImplementationCodeHelpers
16
20
 
17
21
  CODEPLAIN_MEMORY_SUBFOLDER = ".memory"
@@ -40,7 +40,7 @@ class RunConformanceTests(BaseAction):
40
40
  + f"in module {render_context.conformance_tests_running_context.current_testing_module_name}"
41
41
  + ")."
42
42
  )
43
- exit_code, conformance_tests_issue, conformance_tests_temp_file_path = render_utils.execute_script(
43
+ exit_code, conformance_tests_issue, conformance_tests_temp_log_file_path = render_utils.execute_script(
44
44
  conformance_tests_script,
45
45
  [render_context.build_folder, conformance_tests_folder_name],
46
46
  "Conformance Tests",
@@ -49,7 +49,9 @@ class RunConformanceTests(BaseAction):
49
49
  timeout=render_context.test_script_timeout,
50
50
  stop_event=render_context.stop_event,
51
51
  )
52
- render_context.script_execution_history.latest_conformance_test_output_path = conformance_tests_temp_file_path
52
+ render_context.script_execution_history.latest_conformance_test_output_path = (
53
+ conformance_tests_temp_log_file_path
54
+ )
53
55
  render_context.script_execution_history.should_update_script_outputs = True
54
56
 
55
57
  render_context.memory_manager.create_conformance_tests_memory(
@@ -21,7 +21,7 @@ class RunUnitTests(BaseAction):
21
21
  console.info(
22
22
  f"Running unit tests script {unittests_script}. (attempt: {render_context.unit_tests_running_context.fix_attempts + 1})"
23
23
  )
24
- exit_code, unittests_issue, unittests_temp_file_path = render_utils.execute_script(
24
+ exit_code, unittests_issue, unittests_temp_log_file_path = render_utils.execute_script(
25
25
  unittests_script,
26
26
  [render_context.build_folder],
27
27
  "Unit Tests",
@@ -29,7 +29,7 @@ class RunUnitTests(BaseAction):
29
29
  stop_event=render_context.stop_event,
30
30
  )
31
31
 
32
- render_context.script_execution_history.latest_unit_test_output_path = unittests_temp_file_path
32
+ render_context.script_execution_history.latest_unit_test_output_path = unittests_temp_log_file_path
33
33
  render_context.script_execution_history.should_update_script_outputs = True
34
34
  if exit_code == 0:
35
35
  return self.SUCCESSFUL_OUTCOME, None
@@ -179,7 +179,11 @@ class RenderContext:
179
179
  )
180
180
 
181
181
  def has_next_frid(self) -> bool:
182
- return plain_spec.get_next_frid(self.plain_source_tree, self.frid_context.frid) is not None
182
+ next_frid = plain_spec.get_next_frid(self.plain_source_tree, self.frid_context.frid)
183
+ if self.render_range is None or len(self.render_range) == 0:
184
+ return next_frid is not None
185
+
186
+ return next_frid is not None and int(next_frid) <= int(self.render_range[-1])
183
187
 
184
188
  def finish_implementing_frid(self):
185
189
  self.functional_requirements_render_attempts_failed_unit_during_conformance_tests = 0
@@ -248,7 +252,8 @@ class RenderContext:
248
252
  conformance_tests_running_context = self.conformance_tests_running_context
249
253
  if conformance_tests_running_context.current_testing_module_name == self.module_name:
250
254
  conformance_tests_running_context.current_testing_frid = plain_spec.get_next_frid(
251
- self.plain_source_tree, self.conformance_tests_running_context.current_testing_frid
255
+ self.plain_source_tree,
256
+ self.conformance_tests_running_context.current_testing_frid,
252
257
  )
253
258
  else:
254
259
  all_frids = list(
@@ -139,16 +139,6 @@ class ConformanceTestsRunningContext:
139
139
  "test_summary"
140
140
  ] = summary
141
141
 
142
- def get_context_summary(self) -> dict:
143
- return {
144
- "current_testing_module_name": self.current_testing_module_name,
145
- "current_testing_frid": self.current_testing_frid,
146
- "fix_attempts": self.fix_attempts,
147
- "execution_phase": self.execution_phase.name,
148
- "acceptance_test_phase": self.acceptance_test_phase.name,
149
- "acceptance_tests_completed": self.acceptance_tests_completed,
150
- }
151
-
152
142
 
153
143
  @dataclass
154
144
  class ScriptExecutionHistory:
system_config.py CHANGED
@@ -29,12 +29,6 @@ class SystemConfig:
29
29
  console.error(f"Failed to load system configuration: {e}")
30
30
  sys.exit(69)
31
31
 
32
- def get_error_message(self, message_key, **kwargs):
33
- """Get a formatted error message by its key."""
34
- if message_key not in self.error_messages:
35
- raise KeyError(f"Unknown error message key: {message_key}")
36
- return self.error_messages[message_key]["message"].format(**kwargs)
37
-
38
32
 
39
33
  # Create a singleton instance
40
34
  system_config = SystemConfig()
tests/test_cli_output.py CHANGED
@@ -1,11 +1,7 @@
1
1
  """Unit tests for cli_output module."""
2
2
 
3
- from datetime import datetime, timezone
4
- from io import StringIO
5
3
  from unittest.mock import Mock, patch
6
4
 
7
- import pytest
8
-
9
5
  from cli_output.status import (
10
6
  _create_progress_bar,
11
7
  _display_credit_line,
@@ -66,7 +62,7 @@ class TestDisplayCreditLine:
66
62
  "type": "free",
67
63
  "total": 50,
68
64
  "remaining": 4,
69
- "period_end": "2026-06-01T00:00:00+00:00",
65
+ "period_end": "2028-12-01T00:00:00+00:00",
70
66
  }
71
67
  _display_credit_line(plan_credits)
72
68
 
@@ -74,7 +70,7 @@ class TestDisplayCreditLine:
74
70
  call_args = mock_console.print.call_args[0][0]
75
71
  assert "Free trial" in call_args
76
72
  assert "4 of 50 remaining" in call_args
77
- assert "expires Jun 1, 2026" in call_args
73
+ assert "expires Dec 1, 2028" in call_args
78
74
 
79
75
  @patch("cli_output.status.console")
80
76
  def test_display_active_pro_plan(self, mock_console):
@@ -114,13 +110,13 @@ class TestDisplayCreditLine:
114
110
  "type": "free",
115
111
  "total": 50,
116
112
  "remaining": 0,
117
- "period_end": "2026-06-01T00:00:00+00:00",
113
+ "period_end": "2028-12-01T00:00:00+00:00",
118
114
  }
119
115
  _display_credit_line(plan_credits)
120
116
 
121
117
  call_args = mock_console.print.call_args[0][0]
122
118
  assert "0 of 50 remaining" in call_args
123
- assert "expires Jun 1, 2026" in call_args
119
+ assert "expires Dec 1, 2028" in call_args
124
120
 
125
121
  @patch("cli_output.status.console")
126
122
  def test_timezone_naive_datetime(self, mock_console):
@@ -145,14 +141,14 @@ class TestDisplayPurchasedCreditLine:
145
141
  bucket = {
146
142
  "total": 100,
147
143
  "remaining": 20,
148
- "expiry_date": "2026-06-12T00:00:00+00:00",
144
+ "expiry_date": "2028-12-12T00:00:00+00:00",
149
145
  }
150
146
  _display_purchased_credit_line(bucket)
151
147
 
152
148
  call_args = mock_console.print.call_args[0][0]
153
149
  assert "Purchased" in call_args
154
150
  assert "20 of 100 remaining" in call_args
155
- assert "expires Jun 12, 2026" in call_args
151
+ assert "expires Dec 12, 2028" in call_args
156
152
 
157
153
  @patch("cli_output.status.console")
158
154
  def test_display_expired_purchased_credits(self, mock_console):
@@ -189,7 +185,7 @@ class TestDisplayStatusMessage:
189
185
  """Test when user has active plan credits."""
190
186
  plan_credits = {
191
187
  "remaining": 10,
192
- "period_end": "2026-06-01T00:00:00+00:00",
188
+ "period_end": "2028-12-01T00:00:00+00:00",
193
189
  }
194
190
  _display_status_message(plan_credits, [])
195
191
 
@@ -215,7 +211,7 @@ class TestDisplayStatusMessage:
215
211
  """Test when user has no credits remaining."""
216
212
  plan_credits = {
217
213
  "remaining": 0,
218
- "period_end": "2026-06-01T00:00:00+00:00",
214
+ "period_end": "2028-12-01T00:00:00+00:00",
219
215
  }
220
216
  _display_status_message(plan_credits, [])
221
217
 
@@ -272,7 +268,7 @@ class TestPrintStatus:
272
268
  "type": "free",
273
269
  "total": 50,
274
270
  "remaining": 10,
275
- "period_end": "2026-06-01T00:00:00+00:00",
271
+ "period_end": "2028-12-01T00:00:00+00:00",
276
272
  },
277
273
  "purchased_credits": [],
278
274
  }
@@ -115,8 +115,8 @@ def test_reference_link_parsing(get_test_data_path):
115
115
  [get_test_data_path("data/plainfile")],
116
116
  )
117
117
  asserted_resources = [
118
- "task_list_ui_specification.yaml",
119
- "add_new_task_modal_specification.yaml",
118
+ "tests/data/plainfile/task_list_ui_specification.yaml",
119
+ "tests/data/plainfile/add_new_task_modal_specification.yaml",
120
120
  ]
121
121
  for functional_requirement in plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS]:
122
122
  if "linked_resources" not in functional_requirement:
@@ -143,13 +143,13 @@ def test_reference_link_parsing(get_test_data_path):
143
143
  assert plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS][1]["linked_resources"] == [
144
144
  {
145
145
  "text": "task_list_ui_specification.yaml",
146
- "target": "task_list_ui_specification.yaml",
146
+ "target": "tests/data/plainfile/task_list_ui_specification.yaml",
147
147
  }
148
148
  ]
149
149
  assert plain_sections[plain_spec.FUNCTIONAL_REQUIREMENTS][2]["linked_resources"] == [
150
150
  {
151
151
  "text": "add_new_task_modal_specification.yaml",
152
- "target": "add_new_task_modal_specification.yaml",
152
+ "target": "tests/data/plainfile/add_new_task_modal_specification.yaml",
153
153
  }
154
154
  ]
155
155
 
tui/components.py CHANGED
@@ -136,16 +136,10 @@ class ScriptOutputType(str, Enum):
136
136
 
137
137
 
138
138
  class TUIComponents(str, Enum):
139
- RENDER_MODULE_NAME_WIDGET = "render-module-name-widget"
140
- RENDER_ID_WIDGET = "render-id-widget"
141
139
  RENDER_STATUS_WIDGET = "render-status-widget"
142
- UNIT_TEST_SCRIPT_OUTPUT_WIDGET = "unit-test-script-output-widget"
143
- CONFORMANCE_TESTS_SCRIPT_OUTPUT_WIDGET = "conformance-tests-script-output-widget"
144
- TESTING_ENVIRONMENT_SCRIPT_OUTPUT_WIDGET = "testing-environment-script-output-widget"
145
140
 
146
141
  # FRID Progress widgets
147
142
  FRID_PROGRESS = "frid-progress"
148
- FRID_PROGRESS_HEADER = "frid-progress-header"
149
143
  FRID_PROGRESS_RENDER_FR = "frid-progress-render-fr"
150
144
  FRID_PROGRESS_UNIT_TEST = "frid-progress-unit-test"
151
145
  FRID_PROGRESS_REFACTORING = "frid-progress-refactoring"
tui/plain2code_tui.py CHANGED
@@ -70,6 +70,7 @@ class Plain2CodeTUI(App):
70
70
  prepare_environment_script: str,
71
71
  state_machine_version: str,
72
72
  enter_pause_event: threading.Event | None = None,
73
+ on_cancel: Callable[[], None] | None = None,
73
74
  default_log_level: str = "INFO",
74
75
  **kwargs,
75
76
  ):
@@ -83,6 +84,7 @@ class Plain2CodeTUI(App):
83
84
  self.prepare_environment_script: Optional[str] = prepare_environment_script
84
85
  self.state_machine_version = state_machine_version
85
86
  self.enter_pause_event = enter_pause_event
87
+ self._on_cancel = on_cancel
86
88
  self.default_log_level = default_log_level
87
89
  self._render_finished = False
88
90
 
@@ -311,4 +313,6 @@ class Plain2CodeTUI(App):
311
313
  Note: Force exit may leave files partially written if interrupted during file I/O operations.
312
314
  This is acceptable since the folders in which we are writing are git versioned and are reset in the next render.
313
315
  """
316
+ if not self._render_finished and self._on_cancel:
317
+ self._on_cancel()
314
318
  self.exit()
@@ -1,3 +1,5 @@
1
+ from typing import Callable, Optional
2
+
1
3
  from textual import on
2
4
  from textual.app import App, ComposeResult
3
5
  from textual.binding import Binding
@@ -24,6 +26,7 @@ class PlainModuleRenderChoiceTUI(App):
24
26
  render_choices: dict[str, RenderChoice],
25
27
  state_machine_version: str,
26
28
  render_id: str,
29
+ on_cancel: Optional[Callable[[], None]] = None,
27
30
  **kwargs,
28
31
  ):
29
32
  super().__init__(**kwargs)
@@ -33,6 +36,7 @@ class PlainModuleRenderChoiceTUI(App):
33
36
  self.render_id = render_id
34
37
  self._expandable_labels: list[dict] = []
35
38
  self.render_choices = render_choices
39
+ self._on_cancel = on_cancel
36
40
 
37
41
  def get_msg_from_choice(self, render_choice: RenderChoice) -> str:
38
42
  if render_choice.choice_type == "module_start":
@@ -191,6 +195,11 @@ class PlainModuleRenderChoiceTUI(App):
191
195
  self.selected_choice = self.render_choices[key]
192
196
  self.exit(self.selected_choice)
193
197
 
198
+ def action_quit(self) -> None:
199
+ if self._on_cancel:
200
+ self._on_cancel()
201
+ self.exit()
202
+
194
203
  async def action_copy_selection(self) -> None:
195
204
  """Handle ctrl+c: copy selected text if any.
196
205
 
tui/styles.css CHANGED
@@ -137,7 +137,7 @@ TestScriptsContainer {
137
137
  height: auto;
138
138
  color: #888;
139
139
  border: solid #888;
140
- padding-top: 1;
140
+ padding-top: 0;
141
141
  }
142
142
 
143
143
  .test-scripts-title {
tui/widget_helpers.py CHANGED
@@ -67,24 +67,6 @@ def update_progress_item_status(tui, widget_id: str, status: str) -> None:
67
67
  clear_progress_item_substates(tui, widget_id)
68
68
 
69
69
 
70
- def update_widget_text(tui, widget_id: str, text: str) -> None:
71
- """Helper function to safely update a widget's text.
72
-
73
- Args:
74
- tui: The Plain2CodeTUI instance
75
- widget_id: The widget ID to update
76
- text: The new text value
77
- """
78
- try:
79
- widget = tui.query_one(f"#{widget_id}")
80
- if widget and hasattr(widget, "update"):
81
- widget.update(text)
82
- except NoMatches as e:
83
- log_to_widget(tui, "WARNING", f"Widget {widget_id} not found: {e}")
84
- except Exception as e:
85
- log_to_widget(tui, "ERROR", f"Error updating widget {widget_id}: {e}")
86
-
87
-
88
70
  def get_frid_progress(tui) -> FRIDProgress:
89
71
  """Helper function to safely get the FRIDProgress widget.
90
72
 
hash_key.py DELETED
@@ -1,29 +0,0 @@
1
- import hashlib
2
- import sys
3
-
4
-
5
- def hash_api_key(api_key):
6
- """Hash the provided API key using SHA-256 and return the hash as a hex string."""
7
- try:
8
- hash_object = hashlib.sha256(api_key.encode())
9
- hex_dig = hash_object.hexdigest()
10
- return hex_dig
11
- except Exception as e:
12
- error_message = f"An error occurred while hashing the API key: {str(e)}"
13
- raise Exception(error_message)
14
-
15
-
16
- if __name__ == "__main__":
17
- if len(sys.argv) != 2:
18
- print("Error: Exactly one argument must be provided for the API key.")
19
- print(f"Usage: python {sys.argv[0]} <api_key>")
20
- sys.exit(1)
21
-
22
- api_key = sys.argv[1]
23
-
24
- try:
25
- hashed_key = hash_api_key(api_key)
26
- print(f"Hashed API Key: {hashed_key}")
27
- except Exception as e:
28
- print(f"Error: {str(e)}")
29
- sys.exit(1)
@@ -1,68 +0,0 @@
1
- import os
2
- from typing import Optional
3
-
4
- import file_utils
5
- import plain_spec
6
- from render_machine.render_types import ConformanceTestsRunningContext
7
-
8
-
9
- class ConformanceTestHelpers:
10
- @staticmethod
11
- def fetch_existing_conformance_test_folder_names(conformance_tests_folder: str):
12
- if os.path.isdir(conformance_tests_folder):
13
- existing_folder_names = file_utils.list_folders_in_directory(conformance_tests_folder)
14
- else:
15
- # This happens if we're rendering the first FRID (without previously created conformance tests)
16
- existing_folder_names = []
17
- return existing_folder_names
18
-
19
- @staticmethod
20
- def fetch_existing_conformance_test_files(conformance_tests_running_context: ConformanceTestsRunningContext):
21
- conformance_test_folder_name = ConformanceTestHelpers.get_current_conformance_test_folder_name(
22
- conformance_tests_running_context
23
- )
24
- existing_conformance_test_files = file_utils.list_all_text_files(conformance_test_folder_name)
25
- existing_conformance_test_files_content = file_utils.get_existing_files_content(
26
- conformance_test_folder_name, existing_conformance_test_files
27
- )
28
- return existing_conformance_test_files, existing_conformance_test_files_content
29
-
30
- @staticmethod
31
- def current_conformance_tests_exist(conformance_tests_running_context: ConformanceTestsRunningContext) -> bool:
32
- return (
33
- conformance_tests_running_context.conformance_tests_json.get(
34
- conformance_tests_running_context.current_testing_frid
35
- )
36
- is not None
37
- )
38
-
39
- @staticmethod
40
- def get_current_conformance_test_folder_name(
41
- conformance_tests_running_context: ConformanceTestsRunningContext,
42
- ) -> str:
43
- return conformance_tests_running_context.conformance_tests_json[
44
- conformance_tests_running_context.current_testing_frid
45
- ]["folder_name"]
46
-
47
- @staticmethod
48
- def get_current_acceptance_tests(
49
- conformance_tests_running_context: ConformanceTestsRunningContext,
50
- ) -> Optional[list[str]]:
51
- if (
52
- plain_spec.ACCEPTANCE_TESTS
53
- in conformance_tests_running_context.conformance_tests_json[
54
- conformance_tests_running_context.current_testing_frid
55
- ]
56
- ):
57
- return conformance_tests_running_context.conformance_tests_json[
58
- conformance_tests_running_context.current_testing_frid
59
- ][plain_spec.ACCEPTANCE_TESTS]
60
- return None
61
-
62
- @staticmethod
63
- def set_current_conformance_tests_summary(
64
- conformance_tests_running_context: ConformanceTestsRunningContext, summary: list[dict]
65
- ):
66
- conformance_tests_running_context.conformance_tests_json[
67
- conformance_tests_running_context.current_testing_frid
68
- ]["test_summary"] = summary