codeplain 0.3.10.dev5__py3-none-any.whl → 0.3.11__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.
- cli_output/status.py +13 -8
- {codeplain-0.3.10.dev5.dist-info → codeplain-0.3.11.dist-info}/METADATA +4 -4
- {codeplain-0.3.10.dev5.dist-info → codeplain-0.3.11.dist-info}/RECORD +26 -21
- {codeplain-0.3.10.dev5.dist-info → codeplain-0.3.11.dist-info}/WHEEL +1 -1
- codeplain_REST_api.py +32 -1
- file_utils.py +14 -0
- git_preflight.py +41 -0
- plain2code.py +61 -5
- plain2code_console.py +5 -4
- plain_file.py +51 -18
- plain_modules.py +6 -6
- render_machine/actions/fix_conformance_test.py +11 -0
- render_machine/actions/fix_unit_tests.py +25 -0
- render_machine/render_types.py +5 -0
- tests/test_cli_output.py +15 -8
- tests/test_console_log_styles.py +61 -5
- tests/test_fix_unit_tests_conformance_context.py +230 -0
- tests/test_git_preflight.py +98 -0
- tests/test_plainfileparser.py +82 -22
- tests/test_render_finished_report.py +200 -0
- tests/test_tui_components.py +147 -0
- tui/components.py +37 -10
- tui/plain2code_tui.py +9 -0
- tui/widget_helpers.py +11 -4
- {codeplain-0.3.10.dev5.dist-info → codeplain-0.3.11.dist-info}/entry_points.txt +0 -0
- {codeplain-0.3.10.dev5.dist-info → codeplain-0.3.11.dist-info}/licenses/LICENSE +0 -0
cli_output/status.py
CHANGED
|
@@ -6,6 +6,8 @@ from typing import Optional
|
|
|
6
6
|
import codeplain_REST_api as codeplain_api
|
|
7
7
|
from plain2code_console import console
|
|
8
8
|
|
|
9
|
+
PLANS_URL = "https://platform.codeplain.ai/plans"
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
def _create_progress_bar(remaining: int, total: int, width: int = 30) -> str:
|
|
11
13
|
"""Create a Unicode progress bar."""
|
|
@@ -42,12 +44,12 @@ def _display_credit_line(plan_credits: dict) -> None:
|
|
|
42
44
|
bar = _create_progress_bar(remaining, total, width=30)
|
|
43
45
|
|
|
44
46
|
# Format plan type label
|
|
45
|
-
plan_label = "Free
|
|
47
|
+
plan_label = "Free credits" if plan_type == "free" else plan_type.upper() + " plan"
|
|
46
48
|
|
|
47
49
|
if is_expired:
|
|
48
|
-
console.print(f" {plan_label:
|
|
50
|
+
console.print(f" {plan_label:12} {bar} expired {formatted_date}")
|
|
49
51
|
else:
|
|
50
|
-
console.print(f" {plan_label:
|
|
52
|
+
console.print(f" {plan_label:12} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
def _display_bucket_credit_line(bucket: dict, label: str) -> None:
|
|
@@ -71,9 +73,9 @@ def _display_bucket_credit_line(bucket: dict, label: str) -> None:
|
|
|
71
73
|
bar = _create_progress_bar(remaining, total, width=30)
|
|
72
74
|
|
|
73
75
|
if is_expired:
|
|
74
|
-
console.print(f" {label:
|
|
76
|
+
console.print(f" {label:12} {bar} expired {formatted_date}")
|
|
75
77
|
else:
|
|
76
|
-
console.print(f" {label:
|
|
78
|
+
console.print(f" {label:12} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
|
|
77
79
|
|
|
78
80
|
|
|
79
81
|
def _display_status_message(plan_credits: Optional[dict], purchased_credits: list, promo_credits: list) -> None:
|
|
@@ -107,7 +109,11 @@ def _display_status_message(plan_credits: Optional[dict], purchased_credits: lis
|
|
|
107
109
|
break
|
|
108
110
|
|
|
109
111
|
if not has_remaining:
|
|
110
|
-
console.print(
|
|
112
|
+
console.print(
|
|
113
|
+
f"\nNo rendering credits remaining. Upgrade to the PRO plan at {PLANS_URL} to continue rendering."
|
|
114
|
+
)
|
|
115
|
+
else:
|
|
116
|
+
console.print(f"\nTo manage your plan navigate to {PLANS_URL}")
|
|
111
117
|
|
|
112
118
|
|
|
113
119
|
def print_status(api_key: str, api_url: str, client_version: str) -> None:
|
|
@@ -148,7 +154,7 @@ def print_status(api_key: str, api_url: str, client_version: str) -> None:
|
|
|
148
154
|
# Display rendering credits section
|
|
149
155
|
console.print("Rendering credits:")
|
|
150
156
|
|
|
151
|
-
# Display plan credits (free
|
|
157
|
+
# Display plan credits (free credits or subscription)
|
|
152
158
|
if plan_credits:
|
|
153
159
|
_display_credit_line(plan_credits)
|
|
154
160
|
|
|
@@ -162,4 +168,3 @@ def print_status(api_key: str, api_url: str, client_version: str) -> None:
|
|
|
162
168
|
|
|
163
169
|
# Display status messages and management link
|
|
164
170
|
_display_status_message(plan_credits, purchased_credits, promo_credits)
|
|
165
|
-
console.print("\nTo manage your plan navigate to https://platform.codeplain.ai/plans")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: codeplain
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.11
|
|
4
4
|
Summary: Transform plain language specifications into working code
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Classifier: Environment :: Console
|
|
@@ -8,7 +8,7 @@ Classifier: Intended Audience :: Developers
|
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Topic :: Software Development :: Code Generators
|
|
10
10
|
Requires-Python: ==3.11.*
|
|
11
|
-
Requires-Dist: gitpython==3.1.
|
|
11
|
+
Requires-Dist: gitpython==3.1.59
|
|
12
12
|
Requires-Dist: mistletoe==1.6.0
|
|
13
13
|
Requires-Dist: networkx==3.6.1
|
|
14
14
|
Requires-Dist: python-frontmatter==1.3.0
|
|
@@ -16,9 +16,9 @@ Requires-Dist: python-liquid2==0.4.0
|
|
|
16
16
|
Requires-Dist: pyyaml==6.0.3
|
|
17
17
|
Requires-Dist: requests==2.34.2
|
|
18
18
|
Requires-Dist: rich==15.0.0
|
|
19
|
-
Requires-Dist: sentry-sdk==2.
|
|
19
|
+
Requires-Dist: sentry-sdk==2.68.1
|
|
20
20
|
Requires-Dist: textual>=7.5.0
|
|
21
|
-
Requires-Dist: tiktoken==0.
|
|
21
|
+
Requires-Dist: tiktoken==0.14.0
|
|
22
22
|
Requires-Dist: transitions==0.9.3
|
|
23
23
|
Provides-Extra: dev
|
|
24
24
|
Requires-Dist: black==26.5.1; extra == 'dev'
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
change_detection.py,sha256=CazORZrezeZSw2I6VBvieN-c2SEjieZPo6pNrKwmMVA,6915
|
|
2
|
-
codeplain_REST_api.py,sha256=
|
|
2
|
+
codeplain_REST_api.py,sha256=ptb2u0mC5FJqMNeUbcc-bNXNhy9NRJZa5vI80uSFSwQ,22715
|
|
3
3
|
concept_utils.py,sha256=TpyiQ4DfMtO231j-lPj65i0suRTPr8t9j3QNf9KiM4A,8080
|
|
4
4
|
conformance_fix_journal.py,sha256=7wL3UOc3BWaUo5MCmiFbb3kIwOQ6iEpdQSyEmedo8Vk,9488
|
|
5
5
|
diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
|
|
6
6
|
event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
|
|
7
|
-
file_utils.py,sha256=
|
|
7
|
+
file_utils.py,sha256=IshKvld9LJLwXSB3lxZ1HQyVHa93I626BsTScBu95ss,12118
|
|
8
|
+
git_preflight.py,sha256=vB2A8GyYz_iWr8jOE4POFLBHr3qihmedWuidspGdsrw,1509
|
|
8
9
|
git_utils.py,sha256=gCUA6_9zg19f6k_0aVcRnIVlAJSBD0Vt6takx-0pKME,16547
|
|
9
10
|
memory_management.py,sha256=CwIOQqF7Xo1sNfvP69zQRK_CpjinQD9dbolhRPl6lTk,2446
|
|
10
11
|
metadata_utils.py,sha256=YO17o7IAma5TT1ZN0pTwrLVr_KxFQ8APv02KqKPpI5k,1641
|
|
11
12
|
module_renderer.py,sha256=IJBle8mbBARn_xrekjSpsVID7719yy2rNPkM15-ONoI,6982
|
|
12
13
|
partial_rendering.py,sha256=uzZc2j4d3pAXzfiHw_y1TK4yLRsLOb1Mm21570AGxJ8,13837
|
|
13
14
|
path_resolution.py,sha256=0tMqU0a1g4DYhTOp7QjLVSEkv_cgasb4NI1mK809dUM,2233
|
|
14
|
-
plain2code.py,sha256=
|
|
15
|
+
plain2code.py,sha256=FV13ItuD9FQN4FhS4H7RXHh7MoQaAlGSh1ctkjeMSoo,20368
|
|
15
16
|
plain2code_arguments.py,sha256=xMK-R13Vu005bHz3MUDY8F0Cf3ED9hCPqNsC6jGZmkk,17620
|
|
16
|
-
plain2code_console.py,sha256=
|
|
17
|
+
plain2code_console.py,sha256=YGBONkQtwZUkrjxTKhqcBoRzW0EmSfYHPH8P4_FM60I,6040
|
|
17
18
|
plain2code_events.py,sha256=rgDF5PDtNr_lEPvJmd1tIQXZrzwgzZW5b-_pykugqe4,1587
|
|
18
19
|
plain2code_exceptions.py,sha256=rtFEQbs1ruzSlGXIGJZoZry9sjYzmhPKHiUQIECcbL0,2567
|
|
19
20
|
plain2code_logger.py,sha256=YQ-ENYBHKXROVv6Q52xh7eXrFkj_3Lex__N9zfxlw9Y,5729
|
|
@@ -22,15 +23,15 @@ plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,223
|
|
|
22
23
|
plain2code_state.py,sha256=uePQlC29Pbhm1yXfdCFebONYdy6jo13gTTmXSwC11cQ,3168
|
|
23
24
|
plain2code_telemetry.py,sha256=e4UCAoFklJMDWGQsL50i7nTLiOBy8gs86A0XO4WLgPo,4597
|
|
24
25
|
plain2code_utils.py,sha256=unOxuqSQRpwG8YF6uoQhnZ3ZSTJRIUCPLdJ-1lRZR5M,1534
|
|
25
|
-
plain_file.py,sha256=
|
|
26
|
-
plain_modules.py,sha256=
|
|
26
|
+
plain_file.py,sha256=yzdHYBZye1UjI6K-vcod5u9hPqQ_zBQS050yypdmB3M,37614
|
|
27
|
+
plain_modules.py,sha256=nXlmw0J2rKkuU1Y3A68LVtZ1Ffdc-D5iMjxaXSaSHTQ,25963
|
|
27
28
|
plain_spec.py,sha256=7nb5lLJKHZ3uu9THkSXyJ5brIKkbGW0ooVfRclLdlzA,16062
|
|
28
29
|
system_config.py,sha256=sA8wiPmBMtRsLwSQ40U5pVdLazAafbV-1Z19Yp0HiSQ,5314
|
|
29
30
|
usage_summary.py,sha256=MA4WcVaAwz6IWCN4k_rgxVcwO4I157wN7BZlFi3h0R4,1282
|
|
30
31
|
cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
|
|
31
32
|
cli_output/dry_run.py,sha256=6NKhNWPtD0CBK_QHSsSqIKLTzibTwxS9d8ltP9d1bgU,1473
|
|
32
33
|
cli_output/render_summary.py,sha256=-yRoI7ZZTS9IHyp1vd-sFleGXTVbxlrCkvuM5TX0Mp4,1168
|
|
33
|
-
cli_output/status.py,sha256=
|
|
34
|
+
cli_output/status.py,sha256=TrRo-BYUJmzjVzPlMUDpE76FUSpSsN_JdsKigKMrdmg,6294
|
|
34
35
|
config/__init__.py,sha256=beYSsJWmBNHDP5rYmVDouqgEeP3t1lkkepbXJ-oq0F8,37
|
|
35
36
|
config/system_config.yaml,sha256=of5R9vukSohU9AlkGJOcAVQA1UJgxYmDy6islU6iFuE,508
|
|
36
37
|
docs/generate_cli.py,sha256=0FHVhICbM8g7ahdInTNih3hNKpty1wbV5rdGECMJj48,656
|
|
@@ -40,7 +41,7 @@ render_machine/code_renderer.py,sha256=-vp_ltU7e3ssX9hbS8IY4ETSp-39JAuoJ3ZltKpdt
|
|
|
40
41
|
render_machine/conformance_tests.py,sha256=PNTCnJNd_xNIhpTPPS8vlx8qws3cTgQSTLdADRab2YM,9617
|
|
41
42
|
render_machine/implementation_code_helpers.py,sha256=Fxm-IInoZjAIZn41_-WXWj7VIYczpW4yH44Gt8CYMAQ,2378
|
|
42
43
|
render_machine/render_context.py,sha256=iaJWI_EMSn51LNF_b7KRvlir46ZgUkjbaEiVgSvMTCI,26314
|
|
43
|
-
render_machine/render_types.py,sha256=
|
|
44
|
+
render_machine/render_types.py,sha256=A5BHAkjmDHQ0pd5rR6THlRY5z6rzrdJdcgZFhN7yw9w,8860
|
|
44
45
|
render_machine/render_utils.py,sha256=yA3X15E_Lebk_-MxkSz9rM_qy-Pes8EQ9Ye-smkHCDw,9320
|
|
45
46
|
render_machine/state_machine_config.py,sha256=0pL4PyrZ5F2UumvHLMRfhyh64M4pDvf-9zEs-5OAEU8,27866
|
|
46
47
|
render_machine/states.py,sha256=Lu-7upbs9_bjJsy0P738qx5IiN0DzVcFdhF2-m6YjnQ,2067
|
|
@@ -53,8 +54,8 @@ render_machine/actions/create_dist.py,sha256=ykER7i2OvWAXjrlAZnkQHpiNl00vSMF8WDx
|
|
|
53
54
|
render_machine/actions/distill_conformance_test_memory.py,sha256=ENjfhTWqRLePHeRF4t8zNRhWKDV2rU4pLYuU_CZhhmo,2639
|
|
54
55
|
render_machine/actions/exit_with_error.py,sha256=W6TMULHJcHEaXE4WUnlELEu0_l7IZy9HWaPkXyqg7ck,1279
|
|
55
56
|
render_machine/actions/finish_functional_requirement.py,sha256=usQgPSg6PHpBOR1RhK7lPz9zjY-2VJJDqi3T0nlqdLw,841
|
|
56
|
-
render_machine/actions/fix_conformance_test.py,sha256=
|
|
57
|
-
render_machine/actions/fix_unit_tests.py,sha256=
|
|
57
|
+
render_machine/actions/fix_conformance_test.py,sha256=Yy6niLefPIyR3vSxyyX4NUi3NJ4dVc0aEAH9JBCBYX4,11572
|
|
58
|
+
render_machine/actions/fix_unit_tests.py,sha256=pqZzp7snSAe967o_kbh1yAhp9pZIMFY9uoJK1HGcCuc,3532
|
|
58
59
|
render_machine/actions/prepare_repositories.py,sha256=HyZ1R6E3H5V-G4WDSGfhxl_arskmW9FcaxNdQr6JjVs,3867
|
|
59
60
|
render_machine/actions/prepare_testing_environment.py,sha256=kIsXgV6-jOlqp-04cimPgTQoHPrVIu0zW-H_Jj-jowE,2106
|
|
60
61
|
render_machine/actions/refactor_code.py,sha256=gyPsnqgHseL2bkENu4WeGCRyV0R18_GH3JOrPdoxKvg,2609
|
|
@@ -71,13 +72,15 @@ tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
|
|
|
71
72
|
tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
|
|
72
73
|
tests/test_arg_provenance.py,sha256=d_J7aCeHMXBpctBWTt60WU-I8pEKN7XRXRTJZZGCdo0,3441
|
|
73
74
|
tests/test_change_detection.py,sha256=jH8SEhndX400DNqIKmcYLzxHqq9WFdgmuvY3XPq-ZFc,13534
|
|
74
|
-
tests/test_cli_output.py,sha256=
|
|
75
|
+
tests/test_cli_output.py,sha256=9yTk0pD2w6vkiSdSkYwneUmEtNqX4x64F6PzYShUEcw,19331
|
|
75
76
|
tests/test_conformance_fix_journal.py,sha256=olTeAuxXgFwQ8LSQyt0FqtBy9_XY_gK-Ei_pUuJ0C3M,10389
|
|
76
|
-
tests/test_console_log_styles.py,sha256=
|
|
77
|
+
tests/test_console_log_styles.py,sha256=sxlhynMAP-kmqhBn1-pKvZXdCnlKSyCGJqwYKrh6sQ0,6468
|
|
77
78
|
tests/test_distill_conformance_test_memory.py,sha256=Tftw9l-gLVwIU6QttY03h2VedvuFxpIsCmZIzvWfxMA,5481
|
|
78
79
|
tests/test_dry_run_link_validation.py,sha256=ivvYXQPqUkQ8glbFyUrdCninCxF2mg-rAsDcfjcB7MM,2140
|
|
79
80
|
tests/test_file_utils.py,sha256=R_ZIhrTtwlhHxgmbBtA8PFEU5-QB4Fi5skJ7PS2KzdU,3614
|
|
81
|
+
tests/test_fix_unit_tests_conformance_context.py,sha256=IEsXK0xMLRuHmoQGBwrY0ENWVng2n-db_YzSO2qBfqM,9136
|
|
80
82
|
tests/test_folder_path_resolution.py,sha256=K9v684xkPE0HOAsovg8uDixpZmaIk7ho-Wd3rTjKLF0,6560
|
|
83
|
+
tests/test_git_preflight.py,sha256=6aQqInDzpk03pS-6UfkGQWjErrIXTEdHs5oeq3ZYzTw,3186
|
|
81
84
|
tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
|
|
82
85
|
tests/test_headless_logging.py,sha256=soNESeotKQaU-SQyxtjpq35l-dLVsauZ_lJ-wk9i2mI,6419
|
|
83
86
|
tests/test_imports.py,sha256=1ncbqQhtQW237kIjyV-Ufl7-R-PuduvoYFj2iS-DXW4,2608
|
|
@@ -90,30 +93,32 @@ tests/test_plain2code_state.py,sha256=cowPZS9oRbv9J49CE4P3Rc47Q5oQOby4bTf2OlnRlB
|
|
|
90
93
|
tests/test_plain2code_utils.py,sha256=-W8a7FquMJQ_pTdBoBF1XprzNL2G555QhdtXCy6BDyQ,1237
|
|
91
94
|
tests/test_plain_modules.py,sha256=Fn3FeH-M4exnyFJDZzc9YWaLEfBmiqPH5TpiBT9jgAs,33033
|
|
92
95
|
tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
|
|
93
|
-
tests/test_plainfileparser.py,sha256=
|
|
96
|
+
tests/test_plainfileparser.py,sha256=OY7dCilaUEeM4vAOShQYsZXKQzzUOi7nGYBaROu8Te8,26606
|
|
94
97
|
tests/test_plainspec.py,sha256=ng55E8ajrH2-gPC1p_QDIe_zXplPvoqHJ-d57UWKFTw,3577
|
|
95
98
|
tests/test_prepare_repositories_layout.py,sha256=PhJQLnHfjfjo6CTWoicyEKgL0_GtjwB2OKYAWSBVHtk,14638
|
|
99
|
+
tests/test_render_finished_report.py,sha256=QtViWItlo8YB8bLOv3YwvfLprVZgZ9r8Q1Td7_vGi18,7445
|
|
96
100
|
tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
|
|
97
101
|
tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
|
|
98
102
|
tests/test_script_path_resolution.py,sha256=tiEqMevQRXr9YpfHN0xldOOQpLAUgnSqTzeZWkKHaUY,4156
|
|
99
103
|
tests/test_system_config.py,sha256=gH5qs9q4lh5CljAdz28HUrJzuKqCvkZherFuv7enMak,2787
|
|
100
104
|
tests/test_telemetry.py,sha256=LINvx1_Oz1OJ3ho2RcIcAA29IsNfUbkVVCbFaQcrBaA,8133
|
|
105
|
+
tests/test_tui_components.py,sha256=O_PB6z_Xt7YWebsD21b99PVgFtqq5c03lVtYytIT5Ps,6200
|
|
101
106
|
tests/test_tui_usage.py,sha256=sfiAGTpRrT3MRr0-8DH3qjuECSxjODVgwzrK_K3iPXs,7959
|
|
102
107
|
tests/test_usage_summary.py,sha256=Ps0kVzvmdZYeXrBR-cqkn3szTV5neYKs1PmmURngeIc,1079
|
|
103
108
|
tests/e2e/conftest.py,sha256=5Dm67UTOE-M6hMb-mqZvwa6Ek_wmZgsR7fUv9ZEqCwY,4164
|
|
104
109
|
tests/e2e/test_hello_world_python.py,sha256=2Cgs3-79dtQh5gVab257tFDHXTznefZeYqbTjlM4Nng,1632
|
|
105
110
|
tests/e2e/test_hello_world_python_windows.py,sha256=ggynH_Wm4011M500zoPNNgvD-s4vZ3hvlorf1yARgQQ,2019
|
|
106
111
|
tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
|
|
107
|
-
tui/components.py,sha256=
|
|
112
|
+
tui/components.py,sha256=tAgP7paI8bR6XAWOWyiUsKexRurEx1CNz_4z81Ty0G4,27195
|
|
108
113
|
tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
|
|
109
|
-
tui/plain2code_tui.py,sha256=
|
|
114
|
+
tui/plain2code_tui.py,sha256=b6FOEXt_dcVhpfOrAHvg-Rvn0bSH18moy5zmUaz3NS4,17552
|
|
110
115
|
tui/plain_module_render_choice_tui.py,sha256=XwBMMzyDat9FYqMkKdIV7kmIevddfwGA5Qk9kgy5sYY,12112
|
|
111
116
|
tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
|
|
112
117
|
tui/state_handlers.py,sha256=zbUS_D9eU8qIwVpaxPEp9zL9bpg8gXsWNoUaqg9TLrQ,16664
|
|
113
118
|
tui/styles.css,sha256=kiC7Og_2G1E7owoiZgV95-3_dSeQnsNj3qzaY04xkDg,6727
|
|
114
|
-
tui/widget_helpers.py,sha256=
|
|
115
|
-
codeplain-0.3.
|
|
116
|
-
codeplain-0.3.
|
|
117
|
-
codeplain-0.3.
|
|
118
|
-
codeplain-0.3.
|
|
119
|
-
codeplain-0.3.
|
|
119
|
+
tui/widget_helpers.py,sha256=jitm2WHiKj-NVFL-hULxWzckPreRbUTl9OIZLN31-ek,6997
|
|
120
|
+
codeplain-0.3.11.dist-info/METADATA,sha256=WtLGmKTaR0d2o383f3bvaZ6JtDdF86bBNjcRxncpLSQ,7251
|
|
121
|
+
codeplain-0.3.11.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
122
|
+
codeplain-0.3.11.dist-info/entry_points.txt,sha256=oDZkBqu9WhtZApb_K6ia8-fn9aojwmAsgnKELceX5T4,46
|
|
123
|
+
codeplain-0.3.11.dist-info/licenses/LICENSE,sha256=pCeKgQ1mXE5OmNUuKOfOZh1T1vqkeCUxghE7N8qgnzc,11345
|
|
124
|
+
codeplain-0.3.11.dist-info/RECORD,,
|
codeplain_REST_api.py
CHANGED
|
@@ -11,6 +11,10 @@ from plain2code_state import RunState
|
|
|
11
11
|
MAX_RETRIES = 4
|
|
12
12
|
RETRY_DELAY = 3
|
|
13
13
|
|
|
14
|
+
# A report the render does not consume must not be able to hold the process open
|
|
15
|
+
# at exit, and requests waits indefinitely by default.
|
|
16
|
+
REPORT_TIMEOUT_SECONDS = 5
|
|
17
|
+
|
|
14
18
|
RETRY_ERROR_CODES = [
|
|
15
19
|
"LLMInternalError",
|
|
16
20
|
]
|
|
@@ -125,6 +129,7 @@ class CodeplainAPI:
|
|
|
125
129
|
run_state: Optional[RunState],
|
|
126
130
|
num_retries: int = MAX_RETRIES,
|
|
127
131
|
silent: bool = False,
|
|
132
|
+
timeout: Optional[int] = None,
|
|
128
133
|
):
|
|
129
134
|
if run_state is not None:
|
|
130
135
|
self._extend_payload_with_run_state(payload, run_state)
|
|
@@ -134,7 +139,7 @@ class CodeplainAPI:
|
|
|
134
139
|
|
|
135
140
|
for attempt in range(num_retries + 1):
|
|
136
141
|
try:
|
|
137
|
-
response = requests.post(endpoint_url, headers=headers, json=payload)
|
|
142
|
+
response = requests.post(endpoint_url, headers=headers, json=payload, timeout=timeout)
|
|
138
143
|
|
|
139
144
|
try:
|
|
140
145
|
response_json = response.json()
|
|
@@ -173,6 +178,27 @@ class CodeplainAPI:
|
|
|
173
178
|
}
|
|
174
179
|
return self.post_request(endpoint_url, headers, payload, None, num_retries=0, silent=True)
|
|
175
180
|
|
|
181
|
+
def render_finished(self, outcome: str, run_state: RunState):
|
|
182
|
+
"""Report the terminal state a render reached.
|
|
183
|
+
|
|
184
|
+
Called once per render, whatever the outcome. The render state machine
|
|
185
|
+
lives here, so the API cannot tell a finished render from one still
|
|
186
|
+
going; it has to be told.
|
|
187
|
+
|
|
188
|
+
No retries and no console output: a report has no result the render
|
|
189
|
+
needs, and must not add minutes to a render that has already ended nor
|
|
190
|
+
print retry noise over the exit summary.
|
|
191
|
+
"""
|
|
192
|
+
endpoint_url = f"{self.api_url}/render_finished"
|
|
193
|
+
headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
|
|
194
|
+
payload = {
|
|
195
|
+
"outcome": outcome,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return self.post_request(
|
|
199
|
+
endpoint_url, headers, payload, run_state, num_retries=0, silent=True, timeout=REPORT_TIMEOUT_SECONDS
|
|
200
|
+
)
|
|
201
|
+
|
|
176
202
|
def render_functional_requirement(
|
|
177
203
|
self,
|
|
178
204
|
frid: str,
|
|
@@ -237,6 +263,7 @@ class CodeplainAPI:
|
|
|
237
263
|
required_modules,
|
|
238
264
|
unittests_issue,
|
|
239
265
|
run_state: RunState,
|
|
266
|
+
conformance_tests_fixes: list[dict] | None = None,
|
|
240
267
|
):
|
|
241
268
|
endpoint_url = f"{self.api_url}/fix_unittests_issue"
|
|
242
269
|
headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
|
|
@@ -251,6 +278,10 @@ class CodeplainAPI:
|
|
|
251
278
|
"unittests_issue": unittests_issue,
|
|
252
279
|
"unittest_batch_id": run_state.unittest_batch_id,
|
|
253
280
|
}
|
|
281
|
+
# Implementation code changes made by the conformance tests fixer right before this unit tests run;
|
|
282
|
+
# only sent when unit tests are processed inside the conformance tests phase.
|
|
283
|
+
if conformance_tests_fixes is not None:
|
|
284
|
+
payload["conformance_tests_fixes"] = conformance_tests_fixes
|
|
254
285
|
|
|
255
286
|
return self.post_request(endpoint_url, headers, payload, run_state)
|
|
256
287
|
|
file_utils.py
CHANGED
|
@@ -180,6 +180,20 @@ def open_from(dirs, file_name):
|
|
|
180
180
|
return None
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
def resolve_linked_resource(dirs, file_name):
|
|
184
|
+
"""Return the first existing path for file_name across dirs, or None if not found.
|
|
185
|
+
|
|
186
|
+
Mirrors the search order used by open_from so that validating a linked resource and
|
|
187
|
+
loading it can never disagree.
|
|
188
|
+
"""
|
|
189
|
+
for dir in dirs:
|
|
190
|
+
full_file_name = os.path.join(dir, file_name)
|
|
191
|
+
if os.path.exists(full_file_name):
|
|
192
|
+
return full_file_name
|
|
193
|
+
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
183
197
|
def load_linked_resources(template_dirs: list[str], resources_list, module_name: str):
|
|
184
198
|
linked_resources = {}
|
|
185
199
|
|
git_preflight.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Verify a working git is available before GitPython is imported.
|
|
2
|
+
|
|
3
|
+
GitPython probes for the git executable on import and raises from module scope
|
|
4
|
+
if it is missing or broken, so the CLI would crash with a traceback before
|
|
5
|
+
main() could explain it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from plain2code_console import console
|
|
13
|
+
|
|
14
|
+
GIT_MISSING_MESSAGE = "git is not installed. Please install git and try again."
|
|
15
|
+
GIT_BROKEN_MESSAGE = "git is installed but not working. Please repair your git installation and try again."
|
|
16
|
+
MACOS_BROKEN_GIT_HINT = (
|
|
17
|
+
"This usually means the Command Line Tools are missing; install them with 'xcode-select --install'."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def git_runs(git_path):
|
|
22
|
+
"""Whether `git version` succeeds. A git on PATH can still be unusable: on
|
|
23
|
+
macOS without the Command Line Tools /usr/bin/git is a stub that exits with
|
|
24
|
+
an xcrun error, and GIT_PYTHON_REFRESH=quiet does not suppress that."""
|
|
25
|
+
try:
|
|
26
|
+
return subprocess.run([git_path, "version"], capture_output=True, timeout=10).returncode == 0
|
|
27
|
+
except (OSError, subprocess.SubprocessError):
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def require_git():
|
|
32
|
+
"""Exit with a clean message unless a working git is available."""
|
|
33
|
+
git_path = shutil.which("git")
|
|
34
|
+
if git_path is None:
|
|
35
|
+
console.error(f"{GIT_MISSING_MESSAGE}\n")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
if not git_runs(git_path):
|
|
39
|
+
hint = f"\n{MACOS_BROKEN_GIT_HINT}" if sys.platform == "darwin" else ""
|
|
40
|
+
console.error(f"{GIT_BROKEN_MESSAGE}{hint}\n")
|
|
41
|
+
sys.exit(1)
|
plain2code.py
CHANGED
|
@@ -61,6 +61,11 @@ from tui.plain_module_render_choice_tui import PlainModuleRenderChoiceTUI
|
|
|
61
61
|
DEFAULT_TEMPLATE_DIRS = "standard_template_library"
|
|
62
62
|
RENDER_THREAD_SHUTDOWN_TIMEOUT = 0.7
|
|
63
63
|
|
|
64
|
+
# The terminal states a render can reach, as the API names them.
|
|
65
|
+
RENDER_OUTCOME_COMPLETED = "completed"
|
|
66
|
+
RENDER_OUTCOME_FAILED = "failed"
|
|
67
|
+
RENDER_OUTCOME_CANCELLED = "cancelled"
|
|
68
|
+
|
|
64
69
|
# Exceptions that represent expected, user-facing error conditions. They are
|
|
65
70
|
# reported to the user directly and must never be sent to Sentry as crashes.
|
|
66
71
|
EXPECTED_EXCEPTIONS = (
|
|
@@ -177,6 +182,52 @@ def _check_connection(codeplainAPI: codeplain_api.CodeplainAPI) -> Optional[str]
|
|
|
177
182
|
return response.get("user_email")
|
|
178
183
|
|
|
179
184
|
|
|
185
|
+
def _render_outcome(run_state: RunState, interrupted: bool = False) -> str:
|
|
186
|
+
"""Name the terminal state a render reached.
|
|
187
|
+
|
|
188
|
+
Ctrl-C is the user stopping the render, so it is a cancellation. It does not
|
|
189
|
+
set render_cancelled: the exit summary reads that flag, and its wording for
|
|
190
|
+
an interrupt stays as it is.
|
|
191
|
+
|
|
192
|
+
Cancellation is tested first. A cancelled render leaves render_succeeded
|
|
193
|
+
False, so asking about success first would report it as a failure.
|
|
194
|
+
"""
|
|
195
|
+
if interrupted or run_state.render_cancelled:
|
|
196
|
+
return RENDER_OUTCOME_CANCELLED
|
|
197
|
+
if run_state.render_succeeded:
|
|
198
|
+
return RENDER_OUTCOME_COMPLETED
|
|
199
|
+
return RENDER_OUTCOME_FAILED
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def report_render_finished(
|
|
203
|
+
codeplainAPI: codeplain_api.CodeplainAPI, run_state: RunState, interrupted: bool = False
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Tell the API which terminal state this render reached.
|
|
206
|
+
|
|
207
|
+
The API cannot work it out for itself - the state machine lives here - and
|
|
208
|
+
main()'s finally is the one place that sees every ending: success, failure,
|
|
209
|
+
cancellation, an unexpected crash and a keyboard interrupt alike. The state
|
|
210
|
+
machine's own completed and failed hooks see only two of the five.
|
|
211
|
+
|
|
212
|
+
Reported only once the connection check has set a user email, which is
|
|
213
|
+
exactly the condition "the API knows who we are". A missing key, an invalid
|
|
214
|
+
key and an outdated client all leave it unset, and none of them got as far
|
|
215
|
+
as a render.
|
|
216
|
+
|
|
217
|
+
Never raises. A report must not change the exit code, and must not mask the
|
|
218
|
+
error that ended the render.
|
|
219
|
+
"""
|
|
220
|
+
if not run_state.user_email:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
codeplainAPI.render_finished(_render_outcome(run_state, interrupted), run_state)
|
|
225
|
+
except Exception:
|
|
226
|
+
# Deliberately silent: logging has stopped by now, and a failed report
|
|
227
|
+
# is not the user's problem.
|
|
228
|
+
pass
|
|
229
|
+
|
|
230
|
+
|
|
180
231
|
def warn_if_acceptance_tests_without_conformance_script(plain_module, args) -> None:
|
|
181
232
|
"""Warn when any loaded module (including required modules) defines acceptance tests
|
|
182
233
|
but no conformance tests script is configured.
|
|
@@ -214,6 +265,7 @@ def validate_linked_resources(plain_module) -> None:
|
|
|
214
265
|
|
|
215
266
|
def render( # noqa: C901
|
|
216
267
|
plain_module: plain_modules.PlainModule,
|
|
268
|
+
codeplainAPI: codeplain_api.CodeplainAPI,
|
|
217
269
|
args,
|
|
218
270
|
run_state: RunState,
|
|
219
271
|
event_bus: EventBus,
|
|
@@ -224,10 +276,6 @@ def render( # noqa: C901
|
|
|
224
276
|
if args.render_range or args.render_from:
|
|
225
277
|
render_range = plain_spec.compute_render_range(args, plain_module.plain_source)
|
|
226
278
|
|
|
227
|
-
codeplainAPI = codeplain_api.CodeplainAPI(args.api_key, console)
|
|
228
|
-
assert args.api is not None and args.api != "", "API URL is required"
|
|
229
|
-
codeplainAPI.api_url = args.api
|
|
230
|
-
|
|
231
279
|
run_state.user_email = _check_connection(codeplainAPI)
|
|
232
280
|
|
|
233
281
|
stop_event = threading.Event()
|
|
@@ -419,6 +467,10 @@ def main(): # noqa: C901
|
|
|
419
467
|
if not args.api:
|
|
420
468
|
args.api = system_config.default_api_url
|
|
421
469
|
|
|
470
|
+
assert args.api is not None and args.api != "", "API URL is required"
|
|
471
|
+
codeplainAPI = codeplain_api.CodeplainAPI(args.api_key, console)
|
|
472
|
+
codeplainAPI.api_url = args.api
|
|
473
|
+
|
|
422
474
|
run_state = RunState(spec_filename=args.filename, replay_with=args.replay_with)
|
|
423
475
|
|
|
424
476
|
if args.headless:
|
|
@@ -431,6 +483,7 @@ def main(): # noqa: C901
|
|
|
431
483
|
|
|
432
484
|
exc_info = None
|
|
433
485
|
error_message = None
|
|
486
|
+
interrupted = False
|
|
434
487
|
|
|
435
488
|
try:
|
|
436
489
|
# Validate API key is present
|
|
@@ -438,10 +491,11 @@ def main(): # noqa: C901
|
|
|
438
491
|
raise MissingAPIKey(
|
|
439
492
|
"Your API key is required. Please set the CODEPLAIN_API_KEY environment variable or provide it with the --api-key argument.\n"
|
|
440
493
|
)
|
|
441
|
-
render(plain_module, args, run_state, event_bus, default_log_level)
|
|
494
|
+
render(plain_module, codeplainAPI, args, run_state, event_bus, default_log_level)
|
|
442
495
|
except BaseException as e:
|
|
443
496
|
if isinstance(e, KeyboardInterrupt):
|
|
444
497
|
error_message = "Keyboard interrupt"
|
|
498
|
+
interrupted = True
|
|
445
499
|
else:
|
|
446
500
|
error_message = str(e) if str(e) else repr(e)
|
|
447
501
|
|
|
@@ -460,6 +514,8 @@ def main(): # noqa: C901
|
|
|
460
514
|
# Remove any scratch extractions created for archive-only ("<module>.module") modules.
|
|
461
515
|
for module in plain_module.all_required_modules + [plain_module]:
|
|
462
516
|
module.cleanup_scratch()
|
|
517
|
+
# Last, so nothing the user is waiting for sits behind a network call.
|
|
518
|
+
report_render_finished(codeplainAPI, run_state, interrupted)
|
|
463
519
|
|
|
464
520
|
if args.headless and (exc_info is not None or not run_state.render_succeeded):
|
|
465
521
|
sys.exit(1)
|
plain2code_console.py
CHANGED
|
@@ -27,7 +27,7 @@ class Plain2CodeConsole(Console):
|
|
|
27
27
|
DEBUG_STYLE = Style(color="purple")
|
|
28
28
|
|
|
29
29
|
def __init__(self):
|
|
30
|
-
super().__init__()
|
|
30
|
+
super().__init__(emoji=False)
|
|
31
31
|
try:
|
|
32
32
|
import tiktoken
|
|
33
33
|
|
|
@@ -66,9 +66,10 @@ class Plain2CodeConsole(Console):
|
|
|
66
66
|
"""
|
|
67
67
|
logger.log(level, " ".join(map(str, args)), extra={"log_color": color})
|
|
68
68
|
style = base_style + Style(color=color) if color else base_style
|
|
69
|
-
#
|
|
70
|
-
# in interpolated content (error texts, file names)
|
|
71
|
-
#
|
|
69
|
+
# Messages must render exactly as logged and not pick its own styling:
|
|
70
|
+
# -- no Rich markup (via square brackets) in interpolated content (error texts, file names)
|
|
71
|
+
# -- no repr highlighter restyling brackets and numbers inside them.
|
|
72
|
+
# -- no emoji substitution, but it's set False in __init__, otherwise it won't reach renderables like Trees
|
|
72
73
|
kwargs.setdefault("markup", False)
|
|
73
74
|
kwargs.setdefault("highlight", False)
|
|
74
75
|
super().print(*args, **kwargs, style=style)
|
plain_file.py
CHANGED
|
@@ -90,7 +90,7 @@ def remove_quotes(token):
|
|
|
90
90
|
token.children = tuple(new_children)
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
def check_section_for_linked_resources(section):
|
|
93
|
+
def check_section_for_linked_resources(section, template_dirs):
|
|
94
94
|
linked_resources = []
|
|
95
95
|
for link in traverse(section, klass=Link):
|
|
96
96
|
parsed_url = urlparse(link.node.target)
|
|
@@ -99,11 +99,19 @@ def check_section_for_linked_resources(section):
|
|
|
99
99
|
f"Plain syntax error: Only relative links are allowed (text: {link.node.children[0].content}, target: {link.node.target})."
|
|
100
100
|
)
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
resolved_target = file_utils.resolve_linked_resource(template_dirs, link.node.target)
|
|
103
|
+
if resolved_target is None:
|
|
104
|
+
searched_dirs = "\n".join(f" {position}. {dir}" for position, dir in enumerate(template_dirs, start=1))
|
|
105
|
+
raise PlainSyntaxError(
|
|
106
|
+
f"Plain syntax error: Link {link.node.target} does not exist. "
|
|
107
|
+
f"Linked resources are looked up relative to the following directories "
|
|
108
|
+
f"(highest to lowest precedence):\n{searched_dirs}"
|
|
109
|
+
)
|
|
104
110
|
|
|
105
|
-
if not os.path.isfile(
|
|
106
|
-
raise PlainSyntaxError(
|
|
111
|
+
if not os.path.isfile(resolved_target):
|
|
112
|
+
raise PlainSyntaxError(
|
|
113
|
+
f"Plain syntax error: Link {link.node.target} must be a file (resolved to {resolved_target})."
|
|
114
|
+
)
|
|
107
115
|
|
|
108
116
|
if len(link.node.children) != 1:
|
|
109
117
|
raise PlainSyntaxError(f"Plain syntax error: Link must have text specified (link: {link.node.target}).")
|
|
@@ -122,15 +130,15 @@ def check_section_for_linked_resources(section):
|
|
|
122
130
|
section.linked_resources = linked_resources
|
|
123
131
|
|
|
124
132
|
|
|
125
|
-
def check_for_linked_resources(plain_source):
|
|
133
|
+
def check_for_linked_resources(plain_source, template_dirs):
|
|
126
134
|
for specification_heading in plain_spec.ALLOWED_SPECIFICATION_HEADINGS:
|
|
127
135
|
if specification_heading in plain_source and hasattr(plain_source[specification_heading], "children"):
|
|
128
136
|
for requirement in plain_source[specification_heading].children:
|
|
129
|
-
check_section_for_linked_resources(requirement)
|
|
137
|
+
check_section_for_linked_resources(requirement, template_dirs)
|
|
130
138
|
|
|
131
139
|
if hasattr(requirement, plain_spec.ACCEPTANCE_TESTS):
|
|
132
140
|
for acceptance_test in requirement.acceptance_tests:
|
|
133
|
-
check_section_for_linked_resources(acceptance_test)
|
|
141
|
+
check_section_for_linked_resources(acceptance_test, template_dirs)
|
|
134
142
|
|
|
135
143
|
|
|
136
144
|
def process_section_code_variables(section, code_variables):
|
|
@@ -615,15 +623,45 @@ def parse_plain_source( # noqa: C901
|
|
|
615
623
|
f"Plain syntax error: Syntax error at line {nested_acceptance_test_heading.line_number}: {plain_spec.ACCEPTANCE_TEST_HEADING} heading should be nested under specific functional spec."
|
|
616
624
|
)
|
|
617
625
|
|
|
626
|
+
defined_concepts = set[str]()
|
|
618
627
|
if plain_source[plain_spec.DEFINITIONS] is not None:
|
|
619
628
|
with PlainRenderer() as renderer:
|
|
620
629
|
for token in plain_source[plain_spec.DEFINITIONS].children:
|
|
621
630
|
rendered_token = renderer.render(token)
|
|
622
631
|
new_concepts, _ = concept_utils.extract_concepts_from_definition(rendered_token)
|
|
632
|
+
defined_concepts.update(new_concepts)
|
|
623
633
|
for concept in new_concepts:
|
|
624
634
|
if concept in required_concepts:
|
|
625
635
|
required_concepts.remove(concept)
|
|
626
636
|
|
|
637
|
+
if EXPORTED_CONCEPTS_DIRECTIVE in plain_source_obj.metadata:
|
|
638
|
+
module_name = modules_trace[-1] if modules_trace else None
|
|
639
|
+
subject = f"Module '{module_name}'" if module_name else "Module"
|
|
640
|
+
|
|
641
|
+
for entry in plain_source_obj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]:
|
|
642
|
+
exported_concepts, errors = concept_utils.extract_concepts_from_definition(entry)
|
|
643
|
+
if errors:
|
|
644
|
+
raise PlainSyntaxError(
|
|
645
|
+
f"Plain syntax error: {subject} exports an invalid concept (`{entry}`). "
|
|
646
|
+
f"Expected a concept token, e.g. :ConceptName:."
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
for exported_concept in exported_concepts:
|
|
650
|
+
if exported_concept in concept_utils.DEFAULT_CONCEPTS:
|
|
651
|
+
raise PlainSyntaxError(
|
|
652
|
+
f"Plain syntax error: {subject} cannot export default concept {exported_concept}. "
|
|
653
|
+
f"Only user-defined concepts can be exported."
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
if exported_concept not in defined_concepts:
|
|
657
|
+
message = f"Plain syntax error: {subject} exports undefined concept {exported_concept}."
|
|
658
|
+
if REQUIRES_DIRECTIVE in plain_source_obj.metadata:
|
|
659
|
+
message += (
|
|
660
|
+
" An exported concept must be defined in this module's definitions or in an "
|
|
661
|
+
"imported module; a concept inherited through requires cannot be re-exported."
|
|
662
|
+
)
|
|
663
|
+
raise PlainSyntaxError(message)
|
|
664
|
+
|
|
627
665
|
required_modules = []
|
|
628
666
|
if REQUIRES_DIRECTIVE in plain_source_obj.metadata:
|
|
629
667
|
required_modules = plain_source_obj.metadata[REQUIRES_DIRECTIVE]
|
|
@@ -710,15 +748,7 @@ def process_required_modules(
|
|
|
710
748
|
if EXPORTED_CONCEPTS_DIRECTIVE in plain_file_parse_result.plain_source_obj.metadata:
|
|
711
749
|
exported_concepts = list[str]()
|
|
712
750
|
for concept in plain_file_parse_result.plain_source_obj.metadata[EXPORTED_CONCEPTS_DIRECTIVE]:
|
|
713
|
-
|
|
714
|
-
raise PlainSyntaxError(
|
|
715
|
-
f"Plain syntax error: Default concept cannot be exported: {concept}. Only user-defined concepts can be exported."
|
|
716
|
-
)
|
|
717
|
-
|
|
718
|
-
if isinstance(concept, str):
|
|
719
|
-
exported_concepts.extend(concept_utils.extract_concepts_from_definition(concept)[0])
|
|
720
|
-
else:
|
|
721
|
-
raise PlainSyntaxError(f"Plain syntax error: Invalid exported concept: {concept}.")
|
|
751
|
+
exported_concepts.extend(concept_utils.extract_concepts_from_definition(concept)[0])
|
|
722
752
|
|
|
723
753
|
with PlainRenderer() as renderer:
|
|
724
754
|
for exported_concept in exported_concepts:
|
|
@@ -737,6 +767,9 @@ def process_exported_definitions(plain_source: dict, exported_definitions: list[
|
|
|
737
767
|
if len(exported_definitions) == 0:
|
|
738
768
|
return
|
|
739
769
|
|
|
770
|
+
if plain_source[plain_spec.DEFINITIONS] is None:
|
|
771
|
+
plain_source[plain_spec.DEFINITIONS] = mistletoe.Document("")
|
|
772
|
+
|
|
740
773
|
with PlainRenderer() as renderer:
|
|
741
774
|
for exported_definition in exported_definitions:
|
|
742
775
|
add_defintion = True
|
|
@@ -820,7 +853,7 @@ def plain_file_parser( # noqa: C901
|
|
|
820
853
|
|
|
821
854
|
process_acceptance_tests(plain_file_parse_result.plain_source)
|
|
822
855
|
|
|
823
|
-
check_for_linked_resources(plain_file_parse_result.plain_source)
|
|
856
|
+
check_for_linked_resources(plain_file_parse_result.plain_source, template_dirs)
|
|
824
857
|
|
|
825
858
|
marshalled_plain_source = marshall_plain_source(plain_file_parse_result.plain_source)
|
|
826
859
|
|