codeplain 0.2.12__py3-none-any.whl → 0.2.13__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 +1 -1
- {codeplain-0.2.12.dist-info → codeplain-0.2.13.dist-info}/METADATA +4 -4
- {codeplain-0.2.12.dist-info → codeplain-0.2.13.dist-info}/RECORD +18 -18
- codeplain-0.2.13.dist-info/licenses/LICENSE +201 -0
- codeplain_REST_api.py +2 -0
- event_bus.py +9 -24
- file_utils.py +2 -4
- memory_management.py +2 -2
- plain2code.py +43 -20
- plain2code_arguments.py +8 -0
- plain2code_console.py +23 -4
- plain2code_logger.py +2 -21
- render_machine/actions/render_functional_requirement.py +1 -0
- render_machine/actions/run_conformance_tests.py +7 -1
- tests/test_git_utils.py +22 -44
- tui/plain2code_tui.py +10 -62
- codeplain-0.2.12.dist-info/licenses/LICENSE +0 -21
- {codeplain-0.2.12.dist-info → codeplain-0.2.13.dist-info}/WHEEL +0 -0
- {codeplain-0.2.12.dist-info → codeplain-0.2.13.dist-info}/entry_points.txt +0 -0
_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.2.
|
|
1
|
+
__version__ = "0.2.13"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeplain
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.13
|
|
4
4
|
Summary: Transform plain language specifications into working code
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -20,7 +20,7 @@ Requires-Dist: textual>=7.5.0
|
|
|
20
20
|
Requires-Dist: tiktoken==0.12.0
|
|
21
21
|
Requires-Dist: transitions==0.9.3
|
|
22
22
|
Provides-Extra: dev
|
|
23
|
-
Requires-Dist: black==
|
|
23
|
+
Requires-Dist: black==26.3.1; extra == 'dev'
|
|
24
24
|
Requires-Dist: flake8==7.0.0; extra == 'dev'
|
|
25
25
|
Requires-Dist: isort==5.13.2; extra == 'dev'
|
|
26
26
|
Requires-Dist: mypy==1.11.2; extra == 'dev'
|
|
@@ -46,7 +46,7 @@ Schematic overview of the Codeplain's code generation service
|
|
|
46
46
|
|
|
47
47
|
An example application in ***plain
|
|
48
48
|
|
|
49
|
-
<img src="
|
|
49
|
+
<img src="resources/plain_example.png" width="70%" height="70%">
|
|
50
50
|
|
|
51
51
|
|
|
52
52
|
## Getting started
|
|
@@ -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](docs/
|
|
118
|
+
- For more details on the ***plain format, see the [***plain language specification](https://www.plainlang.org/docs/intro/).
|
|
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
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
_version.py,sha256=
|
|
2
|
-
codeplain_REST_api.py,sha256=
|
|
1
|
+
_version.py,sha256=C0atO05M0rfDTTHt02NxNa4jt0eSqXM4AxShEhb2epA,23
|
|
2
|
+
codeplain_REST_api.py,sha256=GkGLstHNeBu5mSsCqAuStKd9qGhDOO5miOyKmDdkPbE,19570
|
|
3
3
|
concept_utils.py,sha256=vwSY4-FmxyqaDBxhntYzqG8t9pXvAWwiULP0DYQI32o,7905
|
|
4
4
|
diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
|
|
5
|
-
event_bus.py,sha256=
|
|
6
|
-
file_utils.py,sha256
|
|
5
|
+
event_bus.py,sha256=LM6d3WuiwB2M_rgVGDjV6jsJwj4U0tlj3KXepz4njqc,1081
|
|
6
|
+
file_utils.py,sha256=-4NUReb--MllqDJ59qSZdxWC7NTpT0MdknF9H_95C9c,12365
|
|
7
7
|
git_utils.py,sha256=jjiKbVF8k765T1VGyXYsZ7z9XkgiD6mo_OwXD9SeSlg,14411
|
|
8
8
|
hash_key.py,sha256=lMKgKpOhPeC4UpFf9e7eCNK20FEaDsXTCJ8j3eCWidE,837
|
|
9
|
-
memory_management.py,sha256=
|
|
9
|
+
memory_management.py,sha256=QWF1EzQ470Ei7QaI5_sGvrtjr1TXiSokeu-s6oo2RxI,5513
|
|
10
10
|
module_renderer.py,sha256=sslmzxgqIDSyoymcmqq3Y1qBdVOJ15vevRq8jhsW6Eg,12156
|
|
11
|
-
plain2code.py,sha256=
|
|
12
|
-
plain2code_arguments.py,sha256=
|
|
13
|
-
plain2code_console.py,sha256=
|
|
11
|
+
plain2code.py,sha256=tbi2qD8XHxtL6UIm0U5g9lGlA8L22QuE6iFTZRKfSl4,14908
|
|
12
|
+
plain2code_arguments.py,sha256=EfiCo6Igzb-waTw0BTB6GX9LYzMk_hrx8UJ34Ryxo4M,12619
|
|
13
|
+
plain2code_console.py,sha256=BDZlWQGvHfQTZFDXIwfKlTzOE3RfVffmdxTkXcM5ll4,4743
|
|
14
14
|
plain2code_events.py,sha256=nITVjZzj7pR5l6tzTpLvuY76ZfL_IzvKtk0qskeQnS0,1377
|
|
15
15
|
plain2code_exceptions.py,sha256=yX9cwWRI4I7O35UyEsaehBFn8OMNR28i_DMJHYCwlvs,1273
|
|
16
|
-
plain2code_logger.py,sha256=
|
|
16
|
+
plain2code_logger.py,sha256=YbJ3JgLXDhc_dibUUl9AtIG3IjGA8jagrhFegb5Myzc,3724
|
|
17
17
|
plain2code_nodes.py,sha256=Om624mfb5MB1Z09c0Zqtb9G1gGkUY9x-v2hzAf3A2gw,3818
|
|
18
18
|
plain2code_read_config.py,sha256=QnpbW_Bi9LzDIOubM5x7fVXH1HgiFL3-5Oa_O1EUoE4,2449
|
|
19
19
|
plain2code_state.py,sha256=InLXdytMXnm9YC4o8MSTbtB1TmqYYUs6rASGBlyA_YY,1274
|
|
@@ -50,8 +50,8 @@ render_machine/actions/prepare_repositories.py,sha256=1jLGwbJS7jJkhjm-1p2oNv_9n1
|
|
|
50
50
|
render_machine/actions/prepare_testing_environment.py,sha256=AmL6Esg3PvCqsEeYZtUE4mlYDavDrleHX7RFkU8qAtg,1900
|
|
51
51
|
render_machine/actions/refactor_code.py,sha256=2cZViWb4Dj6t7dCN2fo9qniIIrn_VE22dsSORjmr_oA,2112
|
|
52
52
|
render_machine/actions/render_conformance_tests.py,sha256=gQaYImSAD6fK8FhfDMQcKH1Uy9_Qq6jsj5ZEKHLmy9c,8798
|
|
53
|
-
render_machine/actions/render_functional_requirement.py,sha256=
|
|
54
|
-
render_machine/actions/run_conformance_tests.py,sha256=
|
|
53
|
+
render_machine/actions/render_functional_requirement.py,sha256=BTX0jZoH8zX93KUamF7komkDzyxRj7Jwv7OKF8HMKJg,4180
|
|
54
|
+
render_machine/actions/run_conformance_tests.py,sha256=SmEKUS6pO_461b35L0iR4p8Fl83S3APDvXISfA73NRc,3830
|
|
55
55
|
render_machine/actions/run_unit_tests.py,sha256=vNxn1zoKbk8jebTDFE_8SbAxosk1Mbm3UjpvjrkbbWc,2080
|
|
56
56
|
render_machine/actions/summarize_conformance_tests.py,sha256=BbBMJX3HkrZF_BlyNFw6prPgF9NLySapoLeGQ6_e3xU,1867
|
|
57
57
|
standard_template_library/__init__.py,sha256=LtsHl0c3VV46DUInIqyOw4KKVoxKEMOx8dAZpMXtAXc,43
|
|
@@ -61,7 +61,7 @@ standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOz
|
|
|
61
61
|
standard_template_library/typescript-react-app-template.plain,sha256=DN5wEJcT8ZqQTf0vFKi9Z1isi9cin2UVNOZb3r28d3M,715
|
|
62
62
|
tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
|
|
63
63
|
tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
|
|
64
|
-
tests/test_git_utils.py,sha256=
|
|
64
|
+
tests/test_git_utils.py,sha256=2FYxYBs2HUkEgeKwnZWPZ8UpNTtZyK4g6BbBrZTv_ic,12984
|
|
65
65
|
tests/test_imports.py,sha256=EWYLUxGunUUdsG9iCiY_tts6T7DIVdhdB_MZlB1LsmU,2081
|
|
66
66
|
tests/test_plainfile.py,sha256=H0asjkBjdqAgwHHZJTaGYm0BG97ah6yirLk-yaHBzwM,11090
|
|
67
67
|
tests/test_plainfileparser.py,sha256=Pq52n5NzJP_h56BRk7OofRwry0aAW1Al86U3VEkte9U,18724
|
|
@@ -70,13 +70,13 @@ tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
|
|
|
70
70
|
tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
|
|
71
71
|
tui/components.py,sha256=hPtXCKEhfGNWxQrordpJZueKQicWSbOYcIb25BACN8Q,22316
|
|
72
72
|
tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
|
|
73
|
-
tui/plain2code_tui.py,sha256=
|
|
73
|
+
tui/plain2code_tui.py,sha256=nUDgW_HCEixBajql1gyp4EYogKfM5K_5K0G_k-vtfXU,12087
|
|
74
74
|
tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
|
|
75
75
|
tui/state_handlers.py,sha256=acc2JnZypog2X7DUQuzxeul9pcHOS6TETGjBMUEKLcc,15320
|
|
76
76
|
tui/styles.css,sha256=fyO5rjPpRLTAt-ym_tOOXQ8F8ZYxEBvaa-JA-K1DAz0,5840
|
|
77
77
|
tui/widget_helpers.py,sha256=lFKgGSgBMyraOGCXEhwKLnPR9xadr43Ei3OWEYzDjQg,5574
|
|
78
|
-
codeplain-0.2.
|
|
79
|
-
codeplain-0.2.
|
|
80
|
-
codeplain-0.2.
|
|
81
|
-
codeplain-0.2.
|
|
82
|
-
codeplain-0.2.
|
|
78
|
+
codeplain-0.2.13.dist-info/METADATA,sha256=g583ibVMphvlBIadZAKdAQRLbA1Lt1lPP2_7BR1LJ3o,4210
|
|
79
|
+
codeplain-0.2.13.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
80
|
+
codeplain-0.2.13.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
|
|
81
|
+
codeplain-0.2.13.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
|
|
82
|
+
codeplain-0.2.13.dist-info/RECORD,,
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Codeplain, Inc.
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
codeplain_REST_api.py
CHANGED
|
@@ -152,6 +152,7 @@ class CodeplainAPI:
|
|
|
152
152
|
memory_files_content: dict,
|
|
153
153
|
module_name: str,
|
|
154
154
|
required_modules: dict,
|
|
155
|
+
include_unittests: bool,
|
|
155
156
|
run_state: RunState,
|
|
156
157
|
) -> dict[str, str]:
|
|
157
158
|
"""
|
|
@@ -191,6 +192,7 @@ class CodeplainAPI:
|
|
|
191
192
|
"memory_files_content": memory_files_content,
|
|
192
193
|
"module_name": module_name,
|
|
193
194
|
"required_modules": required_modules,
|
|
195
|
+
"include_unittests": include_unittests,
|
|
194
196
|
}
|
|
195
197
|
|
|
196
198
|
return self.post_request(endpoint_url, headers, payload, run_state)
|
event_bus.py
CHANGED
|
@@ -7,26 +7,11 @@ from plain2code_events import BaseEvent
|
|
|
7
7
|
class EventBus:
|
|
8
8
|
def __init__(self):
|
|
9
9
|
self._listeners: defaultdict[Type[BaseEvent], list[Callable[[Any], None]]] = defaultdict(list)
|
|
10
|
-
self.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
self._main_thread_callback = fn
|
|
16
|
-
|
|
17
|
-
# Notify anyone waiting for the event bus to be ready
|
|
18
|
-
for callback in self._ready_callbacks:
|
|
19
|
-
callback()
|
|
20
|
-
self._ready_callbacks.clear()
|
|
21
|
-
|
|
22
|
-
def on_ready(self, callback: Callable[[], None]):
|
|
23
|
-
"""Register a callback to be called when the event bus is ready."""
|
|
24
|
-
if self._main_thread_callback:
|
|
25
|
-
# Already ready, call immediately
|
|
26
|
-
callback()
|
|
27
|
-
else:
|
|
28
|
-
# Not ready yet, queue it
|
|
29
|
-
self._ready_callbacks.append(callback)
|
|
10
|
+
self._dispatch_wrapper: Callable[[Callable], None] | None = None
|
|
11
|
+
|
|
12
|
+
def register_dispatch_wrapper(self, fn: Callable[[Callable], None]):
|
|
13
|
+
"""Set a wrapper for dispatching listeners (e.g., Textual's app.call_from_thread)."""
|
|
14
|
+
self._dispatch_wrapper = fn
|
|
30
15
|
|
|
31
16
|
def subscribe(self, event_type: Type[BaseEvent], listener: Callable[[Any], None]):
|
|
32
17
|
"""Registers a listener for a specific event type."""
|
|
@@ -39,7 +24,7 @@ class EventBus:
|
|
|
39
24
|
for listener in self._listeners[type(event)]:
|
|
40
25
|
listener(event)
|
|
41
26
|
|
|
42
|
-
if
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
if self._dispatch_wrapper:
|
|
28
|
+
self._dispatch_wrapper(_dispatch)
|
|
29
|
+
else:
|
|
30
|
+
_dispatch()
|
file_utils.py
CHANGED
|
@@ -236,8 +236,7 @@ def load_linked_resources(template_dirs: list[str], resources_list):
|
|
|
236
236
|
content = open_from(template_dirs, file_name)
|
|
237
237
|
|
|
238
238
|
if content is None:
|
|
239
|
-
raise FileNotFoundError(
|
|
240
|
-
f"""
|
|
239
|
+
raise FileNotFoundError(f"""
|
|
241
240
|
Resource file {file_name} not found. Resource files are searched in the following order (highest to lowest precedence):
|
|
242
241
|
|
|
243
242
|
1. The directory containing your .plain file
|
|
@@ -245,8 +244,7 @@ def load_linked_resources(template_dirs: list[str], resources_list):
|
|
|
245
244
|
3. The built-in 'standard_template_library' directory
|
|
246
245
|
|
|
247
246
|
Please ensure that the resource exists in one of these locations, or specify the correct --template-dir if using custom templates.
|
|
248
|
-
"""
|
|
249
|
-
)
|
|
247
|
+
""")
|
|
250
248
|
|
|
251
249
|
linked_resources[file_name] = content
|
|
252
250
|
|
memory_management.py
CHANGED
|
@@ -61,7 +61,7 @@ class MemoryManager:
|
|
|
61
61
|
existing_files, existing_files_content = ImplementationCodeHelpers.fetch_existing_files(
|
|
62
62
|
render_context.build_folder
|
|
63
63
|
)
|
|
64
|
-
|
|
64
|
+
memory_files, memory_files_content = MemoryManager.fetch_memory_files(self.memory_folder)
|
|
65
65
|
|
|
66
66
|
conformance_tests_folder_name = (
|
|
67
67
|
render_context.conformance_tests_running_context.get_current_conformance_test_folder_name()
|
|
@@ -96,7 +96,7 @@ class MemoryManager:
|
|
|
96
96
|
)
|
|
97
97
|
if len(response_files) > 0:
|
|
98
98
|
memory_folder_path = os.path.join(self.memory_folder, CONFORMANCE_TEST_MEMORY_SUBFOLDER)
|
|
99
|
-
file_utils.store_response_files(memory_folder_path, response_files,
|
|
99
|
+
file_utils.store_response_files(memory_folder_path, response_files, memory_files)
|
|
100
100
|
|
|
101
101
|
def delete_unresolved_memory_files(self):
|
|
102
102
|
"""Delete memory files whose resolution_status is not 'RESOLVED'."""
|
plain2code.py
CHANGED
|
@@ -3,6 +3,7 @@ import logging
|
|
|
3
3
|
import logging.config
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
|
+
import threading
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
from typing import Optional
|
|
8
9
|
|
|
@@ -18,6 +19,7 @@ from event_bus import EventBus
|
|
|
18
19
|
from module_renderer import ModuleRenderer
|
|
19
20
|
from plain2code_arguments import parse_arguments
|
|
20
21
|
from plain2code_console import console
|
|
22
|
+
from plain2code_events import RenderFailed
|
|
21
23
|
from plain2code_exceptions import (
|
|
22
24
|
ConflictingRequirements,
|
|
23
25
|
CreditBalanceTooLow,
|
|
@@ -122,6 +124,7 @@ def setup_logging(
|
|
|
122
124
|
log_file_name: str,
|
|
123
125
|
plain_file_path: Optional[str],
|
|
124
126
|
render_id: str,
|
|
127
|
+
headless: bool = False,
|
|
125
128
|
):
|
|
126
129
|
# Set default level to INFO for everything not explicitly configured
|
|
127
130
|
logging.getLogger().setLevel(logging.INFO)
|
|
@@ -162,10 +165,12 @@ def setup_logging(
|
|
|
162
165
|
for h in root_logger.handlers[:]:
|
|
163
166
|
root_logger.removeHandler(h)
|
|
164
167
|
|
|
165
|
-
handler = TuiLoggingHandler(event_bus)
|
|
166
168
|
formatter = IndentedFormatter("%(levelname)s:%(name)s:%(message)s")
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
|
|
170
|
+
if not headless:
|
|
171
|
+
handler = TuiLoggingHandler(event_bus)
|
|
172
|
+
handler.setFormatter(formatter)
|
|
173
|
+
root_logger.addHandler(handler)
|
|
169
174
|
|
|
170
175
|
if log_to_file and log_file_path:
|
|
171
176
|
try:
|
|
@@ -232,24 +237,36 @@ def render(args, run_state: RunState, event_bus: EventBus): # noqa: C901
|
|
|
232
237
|
event_bus,
|
|
233
238
|
)
|
|
234
239
|
|
|
235
|
-
|
|
236
|
-
event_bus=event_bus,
|
|
237
|
-
worker_fun=module_renderer.render_module,
|
|
238
|
-
render_id=run_state.render_id,
|
|
239
|
-
unittests_script=args.unittests_script,
|
|
240
|
-
conformance_tests_script=args.conformance_tests_script,
|
|
241
|
-
prepare_environment_script=args.prepare_environment_script,
|
|
242
|
-
state_machine_version=system_config.client_version,
|
|
243
|
-
css_path="styles.css",
|
|
244
|
-
)
|
|
245
|
-
result = app.run()
|
|
240
|
+
render_error: list[Exception] = []
|
|
246
241
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
242
|
+
def run_render():
|
|
243
|
+
try:
|
|
244
|
+
module_renderer.render_module()
|
|
245
|
+
except Exception as e:
|
|
246
|
+
render_error.append(e)
|
|
247
|
+
event_bus.publish(RenderFailed(error_message=str(e)))
|
|
251
248
|
|
|
252
|
-
|
|
249
|
+
if args.headless:
|
|
250
|
+
print(f"Render started. Render ID: {run_state.render_id}")
|
|
251
|
+
module_renderer.render_module()
|
|
252
|
+
return
|
|
253
|
+
else:
|
|
254
|
+
render_thread = threading.Thread(target=run_render, daemon=True)
|
|
255
|
+
app = Plain2CodeTUI(
|
|
256
|
+
event_bus=event_bus,
|
|
257
|
+
on_ready=render_thread.start,
|
|
258
|
+
render_id=run_state.render_id,
|
|
259
|
+
unittests_script=args.unittests_script,
|
|
260
|
+
conformance_tests_script=args.conformance_tests_script,
|
|
261
|
+
prepare_environment_script=args.prepare_environment_script,
|
|
262
|
+
state_machine_version=system_config.client_version,
|
|
263
|
+
css_path="styles.css",
|
|
264
|
+
)
|
|
265
|
+
app.run()
|
|
266
|
+
render_thread.join(timeout=1)
|
|
267
|
+
|
|
268
|
+
if render_error:
|
|
269
|
+
raise render_error[0]
|
|
253
270
|
|
|
254
271
|
|
|
255
272
|
def main(): # noqa: C901
|
|
@@ -285,7 +302,13 @@ def main(): # noqa: C901
|
|
|
285
302
|
|
|
286
303
|
run_state = RunState(spec_filename=args.filename, replay_with=args.replay_with)
|
|
287
304
|
|
|
288
|
-
|
|
305
|
+
if args.headless:
|
|
306
|
+
# Suppress Rich console output.
|
|
307
|
+
console.quiet = True
|
|
308
|
+
|
|
309
|
+
setup_logging(
|
|
310
|
+
args, event_bus, args.log_to_file, args.log_file_name, args.filename, run_state.render_id, args.headless
|
|
311
|
+
)
|
|
289
312
|
|
|
290
313
|
exc_info = None
|
|
291
314
|
try:
|
plain2code_arguments.py
CHANGED
|
@@ -288,6 +288,14 @@ def create_parser():
|
|
|
288
288
|
help="Path to the logging configuration file.",
|
|
289
289
|
)
|
|
290
290
|
|
|
291
|
+
parser.add_argument(
|
|
292
|
+
"--headless",
|
|
293
|
+
action="store_true",
|
|
294
|
+
default=False,
|
|
295
|
+
help="Run in headless mode: no TUI, no terminal output except a single render-started message. "
|
|
296
|
+
"All logs are written to the log file.",
|
|
297
|
+
)
|
|
298
|
+
|
|
291
299
|
return parser
|
|
292
300
|
|
|
293
301
|
|
plain2code_console.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
import tiktoken
|
|
4
3
|
from rich.console import Console
|
|
5
4
|
from rich.style import Style
|
|
6
5
|
from rich.tree import Tree
|
|
7
6
|
|
|
7
|
+
CHARACTERS_TO_TOKENS_RULE_OF_THUMB_RATIO = 4
|
|
8
|
+
|
|
8
9
|
|
|
9
10
|
class Plain2CodeConsole(Console):
|
|
10
11
|
INFO_STYLE = Style()
|
|
@@ -16,7 +17,16 @@ class Plain2CodeConsole(Console):
|
|
|
16
17
|
|
|
17
18
|
def __init__(self):
|
|
18
19
|
super().__init__()
|
|
19
|
-
|
|
20
|
+
try:
|
|
21
|
+
import tiktoken
|
|
22
|
+
|
|
23
|
+
self.llm_encoding = tiktoken.get_encoding("cl100k_base")
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logging.warning(
|
|
26
|
+
"Failed to import optional library tiktoken. Using approximate instead of exact token count."
|
|
27
|
+
)
|
|
28
|
+
logging.debug(f"Exception: {e}")
|
|
29
|
+
self.llm_encoding = None
|
|
20
30
|
|
|
21
31
|
def info(self, *args, **kwargs):
|
|
22
32
|
logging.info(" ".join(map(str, args)))
|
|
@@ -86,7 +96,7 @@ class Plain2CodeConsole(Console):
|
|
|
86
96
|
current_level = current_level.add(f"{part} [red]deleted[/red]")
|
|
87
97
|
else:
|
|
88
98
|
file_lines = len(content.splitlines())
|
|
89
|
-
file_tokens =
|
|
99
|
+
file_tokens = self._count_tokens(content)
|
|
90
100
|
current_level = current_level.add(f"{part} ({file_lines} lines, {file_tokens} tokens)")
|
|
91
101
|
else:
|
|
92
102
|
current_level = current_level.add(part)
|
|
@@ -95,6 +105,15 @@ class Plain2CodeConsole(Console):
|
|
|
95
105
|
|
|
96
106
|
return tree
|
|
97
107
|
|
|
108
|
+
def _count_tokens(self, text):
|
|
109
|
+
"""Count tokens using tiktoken if available, otherwise estimate from character count."""
|
|
110
|
+
if self.llm_encoding is not None:
|
|
111
|
+
try:
|
|
112
|
+
return len(self.llm_encoding.encode(text))
|
|
113
|
+
except Exception:
|
|
114
|
+
pass
|
|
115
|
+
return len(text) // CHARACTERS_TO_TOKENS_RULE_OF_THUMB_RATIO
|
|
116
|
+
|
|
98
117
|
def print_resources(self, resources_list, linked_resources):
|
|
99
118
|
if len(resources_list) == 0:
|
|
100
119
|
self.input("Linked resources: None")
|
|
@@ -103,7 +122,7 @@ class Plain2CodeConsole(Console):
|
|
|
103
122
|
self.input("Linked resources:")
|
|
104
123
|
for resource_name in resources_list:
|
|
105
124
|
if resource_name["target"] in linked_resources:
|
|
106
|
-
file_tokens =
|
|
125
|
+
file_tokens = self._count_tokens(linked_resources[resource_name["target"]])
|
|
107
126
|
self.input(
|
|
108
127
|
f"- {resource_name['text']} [#4169E1]({resource_name['target']}, {file_tokens} tokens)[/#4169E1]"
|
|
109
128
|
)
|
plain2code_logger.py
CHANGED
|
@@ -40,15 +40,10 @@ class TuiLoggingHandler(logging.Handler):
|
|
|
40
40
|
def __init__(self, event_bus: EventBus):
|
|
41
41
|
super().__init__()
|
|
42
42
|
self.event_bus = event_bus
|
|
43
|
-
self.
|
|
44
|
-
self.start_time = time.time() # Record start time for offset calculation
|
|
45
|
-
|
|
46
|
-
# Register to be notified when event bus is ready
|
|
47
|
-
self.event_bus.on_ready(self._flush_buffer)
|
|
43
|
+
self.start_time = time.time()
|
|
48
44
|
|
|
49
45
|
def emit(self, record):
|
|
50
46
|
try:
|
|
51
|
-
# Extract structured data from the log record
|
|
52
47
|
offset_seconds = record.created - self.start_time
|
|
53
48
|
minutes = int(offset_seconds // 60)
|
|
54
49
|
seconds = int(offset_seconds % 60)
|
|
@@ -60,12 +55,7 @@ class TuiLoggingHandler(logging.Handler):
|
|
|
60
55
|
message=record.getMessage(),
|
|
61
56
|
timestamp=timestamp,
|
|
62
57
|
)
|
|
63
|
-
|
|
64
|
-
# Try to publish, fall back to buffering if not ready
|
|
65
|
-
if self.event_bus._main_thread_callback:
|
|
66
|
-
self.event_bus.publish(event)
|
|
67
|
-
else:
|
|
68
|
-
self._buffer.append(event)
|
|
58
|
+
self.event_bus.publish(event)
|
|
69
59
|
except RuntimeError:
|
|
70
60
|
# We're going to get this crash after the TUI app is closed (forcefully).
|
|
71
61
|
# NOTE: This should be more thought out.
|
|
@@ -73,15 +63,6 @@ class TuiLoggingHandler(logging.Handler):
|
|
|
73
63
|
except Exception:
|
|
74
64
|
self.handleError(record)
|
|
75
65
|
|
|
76
|
-
def _flush_buffer(self):
|
|
77
|
-
"""Flush buffered log messages to the event bus."""
|
|
78
|
-
for event in self._buffer:
|
|
79
|
-
try:
|
|
80
|
-
self.event_bus.publish(event)
|
|
81
|
-
except Exception:
|
|
82
|
-
pass
|
|
83
|
-
self._buffer.clear()
|
|
84
|
-
|
|
85
66
|
|
|
86
67
|
class CrashLogHandler(logging.Handler):
|
|
87
68
|
def __init__(self):
|
|
@@ -51,6 +51,7 @@ class RenderFunctionalRequirement(BaseAction):
|
|
|
51
51
|
memory_files_content,
|
|
52
52
|
render_context.module_name,
|
|
53
53
|
render_context.get_required_modules_functionalities(),
|
|
54
|
+
render_context.should_run_unit_tests(),
|
|
54
55
|
render_context.run_state,
|
|
55
56
|
)
|
|
56
57
|
except FunctionalRequirementTooComplex as e:
|
|
@@ -54,7 +54,13 @@ class RunConformanceTests(BaseAction):
|
|
|
54
54
|
)
|
|
55
55
|
|
|
56
56
|
if exit_code == 0:
|
|
57
|
-
|
|
57
|
+
if (
|
|
58
|
+
render_context.conformance_tests_running_context.current_testing_module_name
|
|
59
|
+
== render_context.module_name
|
|
60
|
+
and render_context.conformance_tests_running_context.current_testing_frid
|
|
61
|
+
== render_context.frid_context.frid
|
|
62
|
+
):
|
|
63
|
+
render_context.memory_manager.delete_unresolved_memory_files()
|
|
58
64
|
return self.SUCCESSFUL_OUTCOME, None
|
|
59
65
|
|
|
60
66
|
if exit_code in UNRECOVERABLE_ERROR_EXIT_CODES:
|
tests/test_git_utils.py
CHANGED
|
@@ -63,8 +63,7 @@ def test_single_file_change(temp_repo):
|
|
|
63
63
|
result = diff(temp_repo, "1.1")
|
|
64
64
|
|
|
65
65
|
assert "test.txt" in result
|
|
66
|
-
expected_diff = dedent(
|
|
67
|
-
"""
|
|
66
|
+
expected_diff = dedent("""
|
|
68
67
|
--- a/test.txt
|
|
69
68
|
+++ b/test.txt
|
|
70
69
|
@@ -1,3 +1,3 @@
|
|
@@ -72,8 +71,7 @@ def test_single_file_change(temp_repo):
|
|
|
72
71
|
+modified content
|
|
73
72
|
line2
|
|
74
73
|
line3
|
|
75
|
-
"""
|
|
76
|
-
).strip()
|
|
74
|
+
""").strip()
|
|
77
75
|
assert result["test.txt"] == expected_diff
|
|
78
76
|
|
|
79
77
|
|
|
@@ -98,8 +96,7 @@ def test_multiple_file_changes(temp_repo):
|
|
|
98
96
|
|
|
99
97
|
# Check first file
|
|
100
98
|
assert "test.txt" in result
|
|
101
|
-
expected_diff1 = dedent(
|
|
102
|
-
"""
|
|
99
|
+
expected_diff1 = dedent("""
|
|
103
100
|
--- a/test.txt
|
|
104
101
|
+++ b/test.txt
|
|
105
102
|
@@ -1,3 +1,2 @@
|
|
@@ -107,22 +104,19 @@ def test_multiple_file_changes(temp_repo):
|
|
|
107
104
|
+file1 modified
|
|
108
105
|
line2
|
|
109
106
|
-line3
|
|
110
|
-
"""
|
|
111
|
-
).strip()
|
|
107
|
+
""").strip()
|
|
112
108
|
assert result["test.txt"] == expected_diff1
|
|
113
109
|
|
|
114
110
|
# Check second file
|
|
115
111
|
assert "file2.txt" in result
|
|
116
|
-
expected_diff2 = dedent(
|
|
117
|
-
"""
|
|
112
|
+
expected_diff2 = dedent("""
|
|
118
113
|
--- /dev/null
|
|
119
114
|
+++ b/file2.txt
|
|
120
115
|
@@ -0,0 +1,2 @@
|
|
121
116
|
+file2 modified
|
|
122
117
|
+line2
|
|
123
118
|
\
|
|
124
|
-
"""
|
|
125
|
-
).strip()
|
|
119
|
+
""").strip()
|
|
126
120
|
assert result["file2.txt"] == expected_diff2
|
|
127
121
|
|
|
128
122
|
|
|
@@ -157,8 +151,7 @@ def test_multiple_commits_diff(temp_repo):
|
|
|
157
151
|
|
|
158
152
|
# Check first file
|
|
159
153
|
assert "test.txt" in result
|
|
160
|
-
expected_diff1 = dedent(
|
|
161
|
-
"""
|
|
154
|
+
expected_diff1 = dedent("""
|
|
162
155
|
--- a/test.txt
|
|
163
156
|
+++ b/test.txt
|
|
164
157
|
@@ -1,3 +1,2 @@
|
|
@@ -166,35 +159,30 @@ def test_multiple_commits_diff(temp_repo):
|
|
|
166
159
|
+file1 frid1.2 refactored version
|
|
167
160
|
line2
|
|
168
161
|
-line3
|
|
169
|
-
"""
|
|
170
|
-
).strip()
|
|
162
|
+
""").strip()
|
|
171
163
|
assert result["test.txt"] == expected_diff1
|
|
172
164
|
|
|
173
165
|
# Check second file
|
|
174
166
|
assert "file2.txt" in result
|
|
175
|
-
expected_diff2 = dedent(
|
|
176
|
-
"""
|
|
167
|
+
expected_diff2 = dedent("""
|
|
177
168
|
--- a/file2.txt
|
|
178
169
|
+++ b/file2.txt
|
|
179
170
|
@@ -1,2 +1,2 @@
|
|
180
171
|
-file2 frid1.1 refactored version
|
|
181
172
|
+file2 frid1.2 version
|
|
182
173
|
line2
|
|
183
|
-
"""
|
|
184
|
-
).strip()
|
|
174
|
+
""").strip()
|
|
185
175
|
assert result["file2.txt"] == expected_diff2
|
|
186
176
|
|
|
187
177
|
# Check third file
|
|
188
178
|
assert "file3.txt" in result
|
|
189
|
-
expected_diff3 = dedent(
|
|
190
|
-
"""
|
|
179
|
+
expected_diff3 = dedent("""
|
|
191
180
|
--- /dev/null
|
|
192
181
|
+++ b/file3.txt
|
|
193
182
|
@@ -0,0 +1,2 @@
|
|
194
183
|
+file3 frid1.2 new file
|
|
195
184
|
+line2
|
|
196
|
-
"""
|
|
197
|
-
).strip()
|
|
185
|
+
""").strip()
|
|
198
186
|
assert result["file3.txt"] == expected_diff3
|
|
199
187
|
|
|
200
188
|
|
|
@@ -213,27 +201,23 @@ def test_diff_without_previous_frid_and_no_base_folder(empty_repo):
|
|
|
213
201
|
result = diff(empty_repo)
|
|
214
202
|
|
|
215
203
|
assert "new.txt" in result
|
|
216
|
-
expected_diff = dedent(
|
|
217
|
-
"""
|
|
204
|
+
expected_diff = dedent("""
|
|
218
205
|
--- /dev/null
|
|
219
206
|
+++ b/new.txt
|
|
220
207
|
@@ -0,0 +1,2 @@
|
|
221
208
|
+new file content
|
|
222
209
|
+line2
|
|
223
|
-
"""
|
|
224
|
-
).strip()
|
|
210
|
+
""").strip()
|
|
225
211
|
assert result["new.txt"] == expected_diff
|
|
226
212
|
|
|
227
213
|
assert "new2.txt" in result
|
|
228
|
-
expected_diff2 = dedent(
|
|
229
|
-
"""
|
|
214
|
+
expected_diff2 = dedent("""
|
|
230
215
|
--- /dev/null
|
|
231
216
|
+++ b/new2.txt
|
|
232
217
|
@@ -0,0 +1,2 @@
|
|
233
218
|
+new file content
|
|
234
219
|
+line2
|
|
235
|
-
"""
|
|
236
|
-
).strip()
|
|
220
|
+
""").strip()
|
|
237
221
|
assert result["new2.txt"] == expected_diff2
|
|
238
222
|
|
|
239
223
|
|
|
@@ -251,16 +235,14 @@ def test_diff_without_previous_frid_and_base_folder(temp_repo):
|
|
|
251
235
|
result = diff(temp_repo)
|
|
252
236
|
|
|
253
237
|
assert "new.txt" in result
|
|
254
|
-
expected_diff = dedent(
|
|
255
|
-
"""
|
|
238
|
+
expected_diff = dedent("""
|
|
256
239
|
--- a/new.txt
|
|
257
240
|
+++ b/new.txt
|
|
258
241
|
@@ -1,2 +1,2 @@
|
|
259
242
|
-base folder content
|
|
260
243
|
+updated base folder content
|
|
261
244
|
line2
|
|
262
|
-
"""
|
|
263
|
-
).strip()
|
|
245
|
+
""").strip()
|
|
264
246
|
assert result["new.txt"] == expected_diff
|
|
265
247
|
|
|
266
248
|
|
|
@@ -276,15 +258,13 @@ def test_new_file(temp_repo):
|
|
|
276
258
|
result = diff(temp_repo, "1.1")
|
|
277
259
|
|
|
278
260
|
assert "new.txt" in result
|
|
279
|
-
expected_diff = dedent(
|
|
280
|
-
"""
|
|
261
|
+
expected_diff = dedent("""
|
|
281
262
|
--- /dev/null
|
|
282
263
|
+++ b/new.txt
|
|
283
264
|
@@ -0,0 +1,2 @@
|
|
284
265
|
+new file content
|
|
285
266
|
+line2
|
|
286
|
-
"""
|
|
287
|
-
).strip()
|
|
267
|
+
""").strip()
|
|
288
268
|
assert result["new.txt"] == expected_diff
|
|
289
269
|
|
|
290
270
|
|
|
@@ -300,16 +280,14 @@ def test_deleted_file(temp_repo):
|
|
|
300
280
|
result = diff(temp_repo, "1.1")
|
|
301
281
|
|
|
302
282
|
assert "test.txt" in result
|
|
303
|
-
expected_diff = dedent(
|
|
304
|
-
"""
|
|
283
|
+
expected_diff = dedent("""
|
|
305
284
|
--- a/test.txt
|
|
306
285
|
+++ /dev/null
|
|
307
286
|
@@ -1,3 +0,0 @@
|
|
308
287
|
-initial content
|
|
309
288
|
-line2
|
|
310
289
|
-line3
|
|
311
|
-
"""
|
|
312
|
-
).strip()
|
|
290
|
+
""").strip()
|
|
313
291
|
assert result["test.txt"] == expected_diff
|
|
314
292
|
|
|
315
293
|
|
tui/plain2code_tui.py
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import threading
|
|
3
|
-
import time
|
|
4
1
|
from typing import Callable, Optional
|
|
5
2
|
|
|
6
3
|
from textual.app import App, ComposeResult
|
|
@@ -8,10 +5,8 @@ from textual.binding import Binding
|
|
|
8
5
|
from textual.containers import Vertical, VerticalScroll
|
|
9
6
|
from textual.css.query import NoMatches
|
|
10
7
|
from textual.widgets import ContentSwitcher, Static
|
|
11
|
-
from textual.worker import Worker, WorkerFailed, WorkerState
|
|
12
8
|
|
|
13
9
|
from event_bus import EventBus
|
|
14
|
-
from plain2code_console import console
|
|
15
10
|
from plain2code_events import (
|
|
16
11
|
LogMessageEmitted,
|
|
17
12
|
RenderCompleted,
|
|
@@ -48,8 +43,6 @@ from .state_handlers import (
|
|
|
48
43
|
UnitTestsHandler,
|
|
49
44
|
)
|
|
50
45
|
|
|
51
|
-
FORCE_EXIT_DELAY = 0.5 # seconds
|
|
52
|
-
|
|
53
46
|
|
|
54
47
|
class Plain2CodeTUI(App):
|
|
55
48
|
"""A Textual TUI for plain2code."""
|
|
@@ -64,7 +57,7 @@ class Plain2CodeTUI(App):
|
|
|
64
57
|
def __init__(
|
|
65
58
|
self,
|
|
66
59
|
event_bus: EventBus,
|
|
67
|
-
|
|
60
|
+
on_ready: Callable[[], None],
|
|
68
61
|
render_id: str,
|
|
69
62
|
unittests_script: str,
|
|
70
63
|
conformance_tests_script: str,
|
|
@@ -75,7 +68,7 @@ class Plain2CodeTUI(App):
|
|
|
75
68
|
super().__init__(**kwargs)
|
|
76
69
|
self.dark = True # Set dark mode as default
|
|
77
70
|
self.event_bus = event_bus
|
|
78
|
-
self.
|
|
71
|
+
self._on_ready = on_ready
|
|
79
72
|
self.render_id = render_id
|
|
80
73
|
self.unittests_script: Optional[str] = unittests_script
|
|
81
74
|
self.conformance_tests_script: Optional[str] = conformance_tests_script
|
|
@@ -125,7 +118,7 @@ class Plain2CodeTUI(App):
|
|
|
125
118
|
|
|
126
119
|
def on_mount(self) -> None:
|
|
127
120
|
"""Called when the app is mounted."""
|
|
128
|
-
self.event_bus.
|
|
121
|
+
self.event_bus.register_dispatch_wrapper(self.call_from_thread)
|
|
129
122
|
|
|
130
123
|
self.event_bus.subscribe(RenderStateUpdated, self.on_render_state_updated)
|
|
131
124
|
self.event_bus.subscribe(RenderCompleted, self.on_render_completed)
|
|
@@ -134,39 +127,7 @@ class Plain2CodeTUI(App):
|
|
|
134
127
|
self.event_bus.subscribe(RenderModuleCompleted, self.on_render_module_completed)
|
|
135
128
|
self.event_bus.subscribe(LogMessageEmitted, self.on_log_message_emitted)
|
|
136
129
|
|
|
137
|
-
self.
|
|
138
|
-
|
|
139
|
-
def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
140
|
-
"""Handle worker state changes."""
|
|
141
|
-
if event.worker.state == WorkerState.ERROR:
|
|
142
|
-
# Extract the original exception from WorkerFailed wrapper
|
|
143
|
-
error = event.worker.error
|
|
144
|
-
original_error = error.__cause__ if isinstance(error, WorkerFailed) and error.__cause__ else error
|
|
145
|
-
self.exit(result=original_error)
|
|
146
|
-
|
|
147
|
-
def _handle_exception(self, error: Exception) -> None:
|
|
148
|
-
"""Override Textual's exception handler to suppress console tracebacks for worker errors.
|
|
149
|
-
|
|
150
|
-
Worker exceptions are logged to file via the logging system (configured in file handler in plain2code.py),
|
|
151
|
-
but the verbose Textual/Rich traceback is suppressed from the terminal. The clean error message
|
|
152
|
-
is displayed via the exception handlers in main().
|
|
153
|
-
"""
|
|
154
|
-
# Because TUI is running in main thread and code renderer is running in a worker thread, textual models this
|
|
155
|
-
# by raising a WorkerFailed exception in this case
|
|
156
|
-
if isinstance(error, WorkerFailed):
|
|
157
|
-
# Here, we still print the error to get some additional information to the file, but it's probably
|
|
158
|
-
# not necessary because we either way print the entire traceback to the console. Here, we could in the future
|
|
159
|
-
# print more information if we would want to.
|
|
160
|
-
original_error = error.__cause__ if error.__cause__ else error
|
|
161
|
-
console.error(
|
|
162
|
-
f"Worker failed with exception: {type(original_error).__name__}: {original_error}",
|
|
163
|
-
exc_info=(type(original_error), original_error, original_error.__traceback__),
|
|
164
|
-
stack_info=False,
|
|
165
|
-
)
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
# For non-worker exceptions, use the default Textual behavior
|
|
169
|
-
super()._handle_exception(error)
|
|
130
|
+
self._on_ready()
|
|
170
131
|
|
|
171
132
|
def compose(self) -> ComposeResult:
|
|
172
133
|
"""Create child widgets for the app."""
|
|
@@ -290,24 +251,12 @@ class Plain2CodeTUI(App):
|
|
|
290
251
|
def on_render_failed(self, event: RenderFailed):
|
|
291
252
|
"""Handle render failure."""
|
|
292
253
|
self._render_error_handler.handle(event.error_message)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
try:
|
|
300
|
-
if hasattr(self, "_driver") and self._driver is not None:
|
|
301
|
-
# suspend_application_mode() calls stop_application_mode() and close()
|
|
302
|
-
# It ensures terminal reset sequences are flushed before exiting.
|
|
303
|
-
self._driver.suspend_application_mode()
|
|
304
|
-
except Exception as e:
|
|
305
|
-
log_to_widget(self, "WARNING", f"Error suspending application mode: {type(e).__name__}: {e}")
|
|
306
|
-
finally:
|
|
307
|
-
os._exit(0) # Kill process immediately, no cleanup - terminates all threads
|
|
308
|
-
|
|
309
|
-
# daemon=True ensures this thread dies with the process if it exits before the timer fires
|
|
310
|
-
threading.Thread(target=ensure_exit, daemon=True).start()
|
|
254
|
+
self._render_finished = True
|
|
255
|
+
try:
|
|
256
|
+
footer = self.screen.query_one(CustomFooter)
|
|
257
|
+
footer.show_render_finished()
|
|
258
|
+
except NoMatches:
|
|
259
|
+
pass
|
|
311
260
|
|
|
312
261
|
async def action_copy_selection(self) -> None:
|
|
313
262
|
"""Handle ctrl+c: copy selected text if any.
|
|
@@ -332,5 +281,4 @@ class Plain2CodeTUI(App):
|
|
|
332
281
|
Note: Force exit may leave files partially written if interrupted during file I/O operations.
|
|
333
282
|
This is acceptable since the folders in which we are writing are git versioned and are reset in the next render.
|
|
334
283
|
"""
|
|
335
|
-
self.render_worker.cancel()
|
|
336
284
|
self.exit()
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 Codeplain.ai
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
File without changes
|
|
File without changes
|