codeplain 0.3.0__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 +1 -1
- cli_output/__init__.py +7 -0
- cli_output/dry_run.py +35 -0
- cli_output/render_summary.py +32 -0
- cli_output/status.py +160 -0
- {codeplain-0.3.0.dist-info → codeplain-0.3.2.dist-info}/METADATA +45 -24
- codeplain-0.3.2.dist-info/RECORD +94 -0
- {codeplain-0.3.0.dist-info → codeplain-0.3.2.dist-info}/WHEEL +1 -1
- codeplain_REST_api.py +9 -1
- concept_utils.py +0 -1
- file_utils.py +18 -62
- memory_management.py +2 -2
- module_renderer.py +2 -5
- partial_rendering.py +10 -6
- plain2code.py +63 -44
- plain2code_arguments.py +32 -2
- plain2code_console.py +2 -0
- plain2code_exceptions.py +6 -0
- plain2code_logger.py +37 -3
- plain2code_state.py +4 -7
- plain2code_utils.py +0 -34
- plain_file.py +6 -0
- plain_modules.py +11 -8
- render_machine/actions/fix_conformance_test.py +27 -29
- render_machine/actions/fix_unit_tests.py +2 -6
- render_machine/actions/prepare_repositories.py +3 -6
- render_machine/actions/prepare_testing_environment.py +3 -5
- render_machine/actions/refactor_code.py +11 -15
- render_machine/actions/render_conformance_tests.py +34 -39
- render_machine/actions/render_functional_requirement.py +15 -20
- render_machine/actions/run_conformance_tests.py +11 -11
- render_machine/actions/run_unit_tests.py +5 -7
- render_machine/code_renderer.py +1 -1
- render_machine/conformance_tests.py +9 -13
- render_machine/render_context.py +12 -15
- render_machine/render_types.py +0 -10
- render_machine/render_utils.py +42 -47
- render_machine/state_machine_config.py +39 -33
- system_config.py +0 -6
- tests/e2e/conftest.py +145 -0
- tests/e2e/test_hello_world_python.py +37 -0
- tests/e2e/test_hello_world_python_windows.py +64 -0
- tests/test_cli_output.py +407 -0
- tests/test_plainfileparser.py +4 -4
- tui/components.py +0 -6
- tui/plain2code_tui.py +15 -0
- tui/plain_module_render_choice_tui.py +36 -9
- tui/styles.css +1 -1
- tui/widget_helpers.py +0 -18
- codeplain-0.3.0.dist-info/RECORD +0 -88
- hash_key.py +0 -29
- render_machine/conformance_test_helpers.py +0 -68
- {codeplain-0.3.0.dist-info → codeplain-0.3.2.dist-info}/entry_points.txt +0 -0
- {codeplain-0.3.0.dist-info → codeplain-0.3.2.dist-info}/licenses/LICENSE +0 -0
_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.3.
|
|
1
|
+
__version__ = "0.3.2"
|
cli_output/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""CLI output formatting for non-interactive display."""
|
|
2
|
+
|
|
3
|
+
from cli_output.dry_run import print_dry_run_output
|
|
4
|
+
from cli_output.render_summary import print_exit_summary
|
|
5
|
+
from cli_output.status import print_status
|
|
6
|
+
|
|
7
|
+
__all__ = ["print_dry_run_output", "print_exit_summary", "print_status"]
|
cli_output/dry_run.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Dry run output display for --dry-run flag."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import plain_spec
|
|
6
|
+
from plain2code_console import console
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def print_dry_run_output(plain_source_tree: dict, render_range: Optional[list[str]]):
|
|
10
|
+
"""Print dry run output showing what would be rendered."""
|
|
11
|
+
frid = plain_spec.get_first_frid(plain_source_tree)
|
|
12
|
+
|
|
13
|
+
while frid is not None:
|
|
14
|
+
is_inside_range = render_range is None or frid in render_range
|
|
15
|
+
|
|
16
|
+
if is_inside_range:
|
|
17
|
+
specifications, _ = plain_spec.get_specifications_for_frid(plain_source_tree, frid)
|
|
18
|
+
functional_requirement_text = specifications[plain_spec.FUNCTIONAL_REQUIREMENTS][-1]
|
|
19
|
+
console.info(
|
|
20
|
+
"-------------------------------------\n"
|
|
21
|
+
f"Rendering functionality {frid}:\n"
|
|
22
|
+
f"{functional_requirement_text}\n"
|
|
23
|
+
"-------------------------------------\n"
|
|
24
|
+
)
|
|
25
|
+
if plain_spec.ACCEPTANCE_TESTS in specifications:
|
|
26
|
+
for i, acceptance_test in enumerate(specifications[plain_spec.ACCEPTANCE_TESTS], 1):
|
|
27
|
+
console.info(f"Generating acceptance test #{i}:\n\n{acceptance_test}\n")
|
|
28
|
+
else:
|
|
29
|
+
console.info(
|
|
30
|
+
"-------------------------------------\n"
|
|
31
|
+
f"Skipping rendering iteration: {frid}\n"
|
|
32
|
+
"-------------------------------------"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
frid = plain_spec.get_next_frid(plain_source_tree, frid)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Render completion summary display."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from plain2code_console import console
|
|
6
|
+
from plain2code_state import RunState
|
|
7
|
+
from plain2code_utils import format_duration_hms
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def print_exit_summary(
|
|
11
|
+
run_state: RunState,
|
|
12
|
+
spec_filename: str,
|
|
13
|
+
error_message: Optional[str] = None,
|
|
14
|
+
) -> None:
|
|
15
|
+
"""Print render outcome after the TUI exits (terminal restored)."""
|
|
16
|
+
console.quiet = False
|
|
17
|
+
|
|
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"
|
|
24
|
+
msg += f" [#8E8F91]render id:\t\t\t[#FFFFFF]{run_state.render_id}\n"
|
|
25
|
+
msg += f" [#8E8F91]input file:\t\t\t[#FFFFFF]{spec_filename}\n"
|
|
26
|
+
msg += f" [#8E8F91]generated code folder:\t[#FFFFFF]{run_state.render_generated_code_path or '-'}\n\n"
|
|
27
|
+
msg += f"[#8E8F91]functionalities [#FFFFFF]{run_state.rendered_functionalities} [#8E8F91]used credits [#FFFFFF]{run_state.rendered_functionalities} [#8E8F91]render time [#FFFFFF]{format_duration_hms(run_state.render_time_accumulated)}\n"
|
|
28
|
+
console.print(msg)
|
|
29
|
+
|
|
30
|
+
if not run_state.render_succeeded and error_message:
|
|
31
|
+
console.error(error_message)
|
|
32
|
+
console.quiet = True
|
cli_output/status.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Status display for --status flag."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import codeplain_REST_api as codeplain_api
|
|
7
|
+
from plain2code_console import console
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _create_progress_bar(remaining: int, total: int, width: int = 30) -> str:
|
|
11
|
+
"""Create a Unicode progress bar."""
|
|
12
|
+
if total == 0:
|
|
13
|
+
filled = 0
|
|
14
|
+
else:
|
|
15
|
+
filled = int((remaining / total) * width)
|
|
16
|
+
|
|
17
|
+
empty = width - filled
|
|
18
|
+
return "█" * filled + "░" * empty
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _display_credit_line(plan_credits: dict) -> None:
|
|
22
|
+
"""Display a plan credit line with progress bar."""
|
|
23
|
+
plan_type = plan_credits["type"]
|
|
24
|
+
remaining = plan_credits["remaining"]
|
|
25
|
+
total = plan_credits["total"]
|
|
26
|
+
period_end = plan_credits["period_end"]
|
|
27
|
+
|
|
28
|
+
# Parse ISO-8601 timestamp and format as "Jun 1, 2026"
|
|
29
|
+
# Handle both with and without timezone info
|
|
30
|
+
dt_str = period_end.replace("Z", "+00:00")
|
|
31
|
+
dt = datetime.fromisoformat(dt_str)
|
|
32
|
+
# If naive datetime, assume UTC
|
|
33
|
+
if dt.tzinfo is None:
|
|
34
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
35
|
+
formatted_date = dt.strftime("%b %-d, %Y")
|
|
36
|
+
|
|
37
|
+
# Check if expired
|
|
38
|
+
now = datetime.now(timezone.utc)
|
|
39
|
+
is_expired = dt < now
|
|
40
|
+
|
|
41
|
+
# Create progress bar (30 chars wide)
|
|
42
|
+
bar = _create_progress_bar(remaining, total, width=30)
|
|
43
|
+
|
|
44
|
+
# Format plan type label
|
|
45
|
+
plan_label = "Free trial" if plan_type == "free" else plan_type.upper() + " plan"
|
|
46
|
+
|
|
47
|
+
if is_expired:
|
|
48
|
+
console.print(f" {plan_label:10} {bar} expired {formatted_date}")
|
|
49
|
+
else:
|
|
50
|
+
console.print(f" {plan_label:10} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _display_purchased_credit_line(bucket: dict) -> None:
|
|
54
|
+
"""Display a purchased credit line with progress bar."""
|
|
55
|
+
remaining = bucket["remaining"]
|
|
56
|
+
total = bucket["total"]
|
|
57
|
+
expiry_date = bucket["expiry_date"]
|
|
58
|
+
|
|
59
|
+
# Parse ISO-8601 timestamp and handle both with and without timezone info
|
|
60
|
+
dt_str = expiry_date.replace("Z", "+00:00")
|
|
61
|
+
dt = datetime.fromisoformat(dt_str)
|
|
62
|
+
# If naive datetime, assume UTC
|
|
63
|
+
if dt.tzinfo is None:
|
|
64
|
+
dt = dt.replace(tzinfo=timezone.utc)
|
|
65
|
+
formatted_date = dt.strftime("%b %-d, %Y")
|
|
66
|
+
|
|
67
|
+
# Check if expired
|
|
68
|
+
now = datetime.now(timezone.utc)
|
|
69
|
+
is_expired = dt < now
|
|
70
|
+
|
|
71
|
+
bar = _create_progress_bar(remaining, total, width=30)
|
|
72
|
+
|
|
73
|
+
if is_expired:
|
|
74
|
+
console.print(f" Purchased {bar} expired {formatted_date}")
|
|
75
|
+
else:
|
|
76
|
+
console.print(f" Purchased {bar} {remaining:2} of {total} remaining expires {formatted_date}")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _display_status_message(plan_credits: Optional[dict], purchased_credits: list) -> None:
|
|
80
|
+
"""Display appropriate status message based on credit state."""
|
|
81
|
+
has_remaining = False
|
|
82
|
+
|
|
83
|
+
# Check if plan credits have remaining balance and are not expired
|
|
84
|
+
if plan_credits:
|
|
85
|
+
remaining = plan_credits["remaining"]
|
|
86
|
+
dt_str = plan_credits["period_end"].replace("Z", "+00:00")
|
|
87
|
+
period_end = datetime.fromisoformat(dt_str)
|
|
88
|
+
# If naive datetime, assume UTC
|
|
89
|
+
if period_end.tzinfo is None:
|
|
90
|
+
period_end = period_end.replace(tzinfo=timezone.utc)
|
|
91
|
+
now = datetime.now(timezone.utc)
|
|
92
|
+
|
|
93
|
+
if remaining > 0 and period_end > now:
|
|
94
|
+
has_remaining = True
|
|
95
|
+
|
|
96
|
+
# Check if any purchased credits have remaining balance and not expired
|
|
97
|
+
for bucket in purchased_credits:
|
|
98
|
+
dt_str = bucket["expiry_date"].replace("Z", "+00:00")
|
|
99
|
+
expiry_date = datetime.fromisoformat(dt_str)
|
|
100
|
+
# If naive datetime, assume UTC
|
|
101
|
+
if expiry_date.tzinfo is None:
|
|
102
|
+
expiry_date = expiry_date.replace(tzinfo=timezone.utc)
|
|
103
|
+
now = datetime.now(timezone.utc)
|
|
104
|
+
|
|
105
|
+
if bucket["remaining"] > 0 and expiry_date > now:
|
|
106
|
+
has_remaining = True
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
if not has_remaining:
|
|
110
|
+
console.print("\nNo rendering credits remaining. Upgrade to continue rendering.")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def print_status(api_key: str, api_url: str, client_version: str) -> None:
|
|
114
|
+
"""Display account status including user info and rendering credits."""
|
|
115
|
+
codeplainAPI = codeplain_api.CodeplainAPI(api_key, console)
|
|
116
|
+
codeplainAPI.api_url = api_url
|
|
117
|
+
|
|
118
|
+
# First check client version
|
|
119
|
+
version_check = codeplainAPI.connection_check(client_version)
|
|
120
|
+
client_version_valid = version_check.get("client_version_valid", False)
|
|
121
|
+
min_version = version_check.get("min_client_version", "unknown")
|
|
122
|
+
|
|
123
|
+
response = codeplainAPI.status()
|
|
124
|
+
|
|
125
|
+
user = response["user"]
|
|
126
|
+
api_key_label = response["api_key_label"]
|
|
127
|
+
org_owner = response.get("organization_owner_email")
|
|
128
|
+
plan_credits = response.get("plan_credits")
|
|
129
|
+
purchased_credits = response.get("purchased_credits", [])
|
|
130
|
+
|
|
131
|
+
# Display header information
|
|
132
|
+
if client_version_valid:
|
|
133
|
+
console.print(f"Version: {client_version}")
|
|
134
|
+
else:
|
|
135
|
+
from plain2code_console import Plain2CodeConsole
|
|
136
|
+
|
|
137
|
+
console.print(
|
|
138
|
+
f"Version: {client_version} (outdated — minimum required: {min_version})",
|
|
139
|
+
style=Plain2CodeConsole.ERROR_STYLE,
|
|
140
|
+
)
|
|
141
|
+
console.print("To update, run: uv tool upgrade codeplain\n", style=Plain2CodeConsole.ERROR_STYLE)
|
|
142
|
+
console.print(f"Name: {user['first_name']} {user['last_name']}")
|
|
143
|
+
console.print(f"User email: {user['email']}")
|
|
144
|
+
console.print(f"Organization owner: {org_owner or 'N/A'}")
|
|
145
|
+
console.print(f"API key label: {api_key_label}\n")
|
|
146
|
+
|
|
147
|
+
# Display rendering credits section
|
|
148
|
+
console.print("Rendering credits:")
|
|
149
|
+
|
|
150
|
+
# Display plan credits (free trial or subscription)
|
|
151
|
+
if plan_credits:
|
|
152
|
+
_display_credit_line(plan_credits)
|
|
153
|
+
|
|
154
|
+
# Display purchased credits
|
|
155
|
+
for bucket in purchased_credits:
|
|
156
|
+
_display_purchased_credit_line(bucket)
|
|
157
|
+
|
|
158
|
+
# Display status messages and management link
|
|
159
|
+
_display_status_message(plan_credits, purchased_credits)
|
|
160
|
+
console.print("\nTo manage your plan navigate to https://platform.codeplain.ai/plans")
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeplain
|
|
3
|
-
Version: 0.3.
|
|
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:
|
|
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
|
|
14
|
-
Requires-Dist: python-frontmatter==1.
|
|
14
|
+
Requires-Dist: python-frontmatter==1.3.0
|
|
15
15
|
Requires-Dist: python-liquid2==0.3.0
|
|
16
16
|
Requires-Dist: pyyaml==6.0.3
|
|
17
17
|
Requires-Dist: requests==2.34.0
|
|
18
|
-
Requires-Dist: rich==
|
|
18
|
+
Requires-Dist: rich==15.0.0
|
|
19
19
|
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
23
|
Requires-Dist: black==26.3.1; extra == 'dev'
|
|
24
|
-
Requires-Dist: flake8==7.
|
|
24
|
+
Requires-Dist: flake8==7.3.0; extra == 'dev'
|
|
25
25
|
Requires-Dist: isort==5.13.2; extra == 'dev'
|
|
26
26
|
Requires-Dist: mypy==1.11.2; extra == 'dev'
|
|
27
27
|
Requires-Dist: pytest==8.3.5; extra == 'dev'
|
|
@@ -29,31 +29,28 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
|
|
30
30
|
# Codeplain plain2code renderer
|
|
31
31
|
|
|
32
|
-
Render
|
|
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
|
|
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
|
|
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
|
-
|
|
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,31 +89,55 @@ 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
|
-
|
|
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:
|
|
101
|
+
```
|
|
102
|
+
cd build
|
|
103
|
+
python hello_world.py
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Releasing
|
|
107
|
+
|
|
108
|
+
Releases are built and published with [uv](https://docs.astral.sh/uv/). The version is read from `_version.py`.
|
|
109
|
+
|
|
110
|
+
1. Bump the version in `_version.py` and commit/tag (`git tag v<version>`).
|
|
111
|
+
2. Build the distributions:
|
|
112
|
+
```bash
|
|
113
|
+
uv build --python 3.11
|
|
114
|
+
```
|
|
115
|
+
This produces `dist/codeplain-<version>-py3-none-any.whl` and `dist/codeplain-<version>.tar.gz`.
|
|
116
|
+
3. (Optional) Smoke-test the wheel:
|
|
117
|
+
```bash
|
|
118
|
+
uv run --with dist/codeplain-<version>-py3-none-any.whl --no-project -- codeplain --status
|
|
119
|
+
```
|
|
120
|
+
4. (Optional) Publish to TestPyPI first:
|
|
121
|
+
```bash
|
|
122
|
+
uv publish --publish-url https://test.pypi.org/legacy/ dist/codeplain-<version>*
|
|
103
123
|
```
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
5. Publish to PyPI:
|
|
125
|
+
```bash
|
|
126
|
+
uv publish dist/codeplain-<version>*
|
|
106
127
|
```
|
|
128
|
+
Authenticate with `UV_PUBLISH_TOKEN=pypi-...` (recommended), `--token pypi-...`, or `--username __token__ --password pypi-...`.
|
|
129
|
+
6. Push the tag: `git push origin v<version>`.
|
|
107
130
|
|
|
108
131
|
## Additional Resources
|
|
109
132
|
|
|
110
133
|
### Examples and Sample Projects
|
|
111
134
|
|
|
112
135
|
- See the [examples](examples) folder for sample projects in Golang, Python, and React.
|
|
113
|
-
- For example application how to implement task manager in
|
|
114
|
-
- For example application how to implement SaaS connectors in
|
|
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.
|
|
115
138
|
|
|
116
139
|
### Documentation
|
|
117
140
|
|
|
118
141
|
- For more details on the ***plain format, see the [***plain language specification](https://www.plainlang.org/docs/).
|
|
119
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).
|
|
120
143
|
- For complete CLI documentation and usage examples, see [plain2code CLI documentation](docs/plain2code_cli.md).
|
|
121
|
-
|
|
122
|
-
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
diff_utils.py,sha256=AjiQlqo5pRos_8hVXZo5yBurl5BzSrTMGrQv4dCtRCg,1198
|
|
5
|
+
event_bus.py,sha256=g5XLwRfjrvTIcB6urjREuCJzZj5TA93hy8lKXLGXuCE,650
|
|
6
|
+
file_utils.py,sha256=grPx2Rl6-q2ijSRoaAOZcsrpOjTqb4i_mtF-jzwhKcQ,10953
|
|
7
|
+
git_utils.py,sha256=LQQnL5DAFpH5fDBQgUZX2WfjWY4YC5OEGBPIXryTmnY,16896
|
|
8
|
+
memory_management.py,sha256=nHNudgaOqp5UGkIMi62TRCc2nUeFOM0w-I1FfxBFaQE,6120
|
|
9
|
+
module_renderer.py,sha256=pXMXKKGMhUwLLlSxppE_hpZlwpJ3WSHSfR9FQxt6rSI,6860
|
|
10
|
+
partial_rendering.py,sha256=prWEJo9m1TbF7SKzpNInbcefyTF0o3oXE7Qf1l4SmBs,8409
|
|
11
|
+
plain2code.py,sha256=gyPeZtLe7VkGChWwGVNNZRXvkqpH8uC27szHbrZgmeY,14010
|
|
12
|
+
plain2code_arguments.py,sha256=J05NwaY-O04rIWVF2yIXysNB_ofZS0wJFn0u1WaCCSA,15399
|
|
13
|
+
plain2code_console.py,sha256=YdE-Kz9qRAzIIAg7aYA1bnXRcwy_aTpbFtkR2zHbd-I,4918
|
|
14
|
+
plain2code_events.py,sha256=AM8KJS68uiVXRMLt861KaGhs2H4yzujlMB_xiYPfHQc,1478
|
|
15
|
+
plain2code_exceptions.py,sha256=Fj1pwCffn06aivEplsTJJGdg6lAOR8gmWd4ctLl9cj4,1745
|
|
16
|
+
plain2code_logger.py,sha256=kNKIfkS6Ydtut7Ptw5jmv5bFPgXlxB-_Ds5nd-j4d9o,4609
|
|
17
|
+
plain2code_nodes.py,sha256=IwjQqJxRSJKgP6eEnkYGy3VQHIyoazgkFxAEkxkjfJ8,4426
|
|
18
|
+
plain2code_read_config.py,sha256=D5CxnmTRjddupecWvstcUm0kIJWWzq5-Zmk6sU0wbIg,2235
|
|
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
|
|
23
|
+
plain_spec.py,sha256=E2Yp2eXMExE7kx7GPQ7v9boHN8XhFUcqprn1DGMaFMs,15669
|
|
24
|
+
system_config.py,sha256=trgytfXKXoCn0Pgy9u00x2D0ZRHlh_tADlbkwZnF8QE,1038
|
|
25
|
+
cli_output/__init__.py,sha256=MBKl0YbVpWE-tBa3V5_aVbB7vbaqlIK2NsHTjRUZqis,284
|
|
26
|
+
cli_output/dry_run.py,sha256=6NKhNWPtD0CBK_QHSsSqIKLTzibTwxS9d8ltP9d1bgU,1473
|
|
27
|
+
cli_output/render_summary.py,sha256=QrW3Iq15xUoWXuVXpqClAKncAvHdQR0lWG1_OMUBsWk,1307
|
|
28
|
+
cli_output/status.py,sha256=5jr9XFU1MWD3pN404qw7SNYCGdvqfA9j4xv9z1tZG4s,5890
|
|
29
|
+
config/__init__.py,sha256=beYSsJWmBNHDP5rYmVDouqgEeP3t1lkkepbXJ-oq0F8,37
|
|
30
|
+
config/system_config.yaml,sha256=of5R9vukSohU9AlkGJOcAVQA1UJgxYmDy6islU6iFuE,508
|
|
31
|
+
docs/generate_cli.py,sha256=0FHVhICbM8g7ahdInTNih3hNKpty1wbV5rdGECMJj48,656
|
|
32
|
+
examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py,sha256=dwTowrHiVKKbrDv21v8xJC30Q57AXZkQasdGOO5JsBE,470
|
|
33
|
+
render_machine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
render_machine/code_renderer.py,sha256=bLdoZcWaLGvuR0GygnWY9y9kycGySD-IEWY5DFGcGsg,3781
|
|
35
|
+
render_machine/conformance_tests.py,sha256=MeEveEGJTClLGxWw04P4TR6U9kX9Qgz9qtxNrB9a8QI,7085
|
|
36
|
+
render_machine/implementation_code_helpers.py,sha256=LdHNLoKgVtrDaGH2APX8C9vjNjLBUhu2V7OAaJvm4rM,2009
|
|
37
|
+
render_machine/render_context.py,sha256=NHDtJkhQP8tW0iyRH6PR7oSk-PO_pEGMKkX38x52YMc,29128
|
|
38
|
+
render_machine/render_types.py,sha256=cT3D0lmxQ3enIo0jGp3LEGCY2t3X1Q1_tqwcadzSAv0,8761
|
|
39
|
+
render_machine/render_utils.py,sha256=ao0e64VSOGYfb8hwJXIZlbqer5HjNMYJWHlLDoHnJyc,9123
|
|
40
|
+
render_machine/state_machine_config.py,sha256=uqrU9Pu4cAKxu_-e1eiuUQK-tZWxZko0U0Bfvl67v_c,28813
|
|
41
|
+
render_machine/states.py,sha256=niFXqayyA_rezCrlzo0hYeHAruErcPzQVF-BeIBGnL8,1968
|
|
42
|
+
render_machine/triggers.py,sha256=o1N5c9xC7fsKY6ZaWcWBtALT8i1acSSharAXKAqBu8E,1285
|
|
43
|
+
render_machine/actions/analyze_specification_ambiguity.py,sha256=SykZAozGvr1jySxbpKCgt11nMpZhOK0sN-J66M42T5Y,2847
|
|
44
|
+
render_machine/actions/base_action.py,sha256=vVzMp-LanrZyq-jamHyWaQxpRl-pLqtTdtiEzUBhfyE,534
|
|
45
|
+
render_machine/actions/commit_conformance_tests_changes.py,sha256=08yxCPCpf5TlrzLPsEh5SJXVVvnLfwdNKSr7BlzlYxo,2589
|
|
46
|
+
render_machine/actions/commit_implementation_code_changes.py,sha256=yi51oAArLCgWcmFbynes-BK2IGnZwA7wygrTJGoL8dY,826
|
|
47
|
+
render_machine/actions/create_dist.py,sha256=xMPPyws7swVkaUSQg3dwMGScCi1BuII9y2C_XSP0N80,1067
|
|
48
|
+
render_machine/actions/exit_with_error.py,sha256=-LkQsFhmV-Nr741APTx38dqu473b6OSiIvKayFFsXxI,1270
|
|
49
|
+
render_machine/actions/finish_functional_requirement.py,sha256=tj9CzuxuHcbR-2-jMLcAM0gOAcbWpMLg9HIiyngI6gU,739
|
|
50
|
+
render_machine/actions/fix_conformance_test.py,sha256=ePfxz9Xm0IXht0ch7H4tLPdRzoKnVdlbcDC9EgeDAAQ,8786
|
|
51
|
+
render_machine/actions/fix_unit_tests.py,sha256=hvwvCJDk_-8fHMpkNFyeBSG1IshIcufDpymF00ulPXY,2301
|
|
52
|
+
render_machine/actions/prepare_repositories.py,sha256=eE-uU6H_K5IQMraVWSRi4TKk_XPrnA878T_JwtI9cWo,3410
|
|
53
|
+
render_machine/actions/prepare_testing_environment.py,sha256=GQNVzc_Ap5n74OAmeQuAhXXHK2FVVY374Z0lutrN2UQ,1867
|
|
54
|
+
render_machine/actions/refactor_code.py,sha256=_1C16bQbpxdzJkEwl8jDhEKVyXcyGTd-5YYDNTq2vA8,1782
|
|
55
|
+
render_machine/actions/render_conformance_tests.py,sha256=5gOQn-G1T9UWEI8sDfALcVdArk0ln9DgUprOPjccS-U,9268
|
|
56
|
+
render_machine/actions/render_functional_requirement.py,sha256=2SbPGsLbrrQx3kasXpSpphzaMsq9GD8ASnSzvIdodT4,3748
|
|
57
|
+
render_machine/actions/run_conformance_tests.py,sha256=4sJTORl6YLFXQIdLMBQHDeJd2hztGyt3tsVvrCviBko,4008
|
|
58
|
+
render_machine/actions/run_unit_tests.py,sha256=A4WXtSzpCLDHYR8fQAbfxUf_E19GYflZSHZaSreoFPk,2101
|
|
59
|
+
render_machine/actions/summarize_conformance_tests.py,sha256=Vy1kRK8vyazK_BbsKuz1MLDN_TQWVxxzkwEEm-50byc,1639
|
|
60
|
+
standard_template_library/__init__.py,sha256=LtsHl0c3VV46DUInIqyOw4KKVoxKEMOx8dAZpMXtAXc,43
|
|
61
|
+
standard_template_library/golang-console-app-template.plain,sha256=JOusEjCj7jagnXUzaI-SdIr3hj1wTo2uShK017aNtVM,949
|
|
62
|
+
standard_template_library/python-console-app-template.plain,sha256=HW-REeF8Crk0r_w4-MxnleVHeW6PdOB2C8gO8FDG61g,931
|
|
63
|
+
standard_template_library/typescript-react-app-boilerplate.plain,sha256=6LFxhEOzWnDHdVpFj-XBTq6s4_2dy7LKV3lVkCzv8i4,114
|
|
64
|
+
standard_template_library/typescript-react-app-template.plain,sha256=DN5wEJcT8ZqQTf0vFKi9Z1isi9cin2UVNOZb3r28d3M,715
|
|
65
|
+
tests/__init__.py,sha256=Wk73Io62J15BtlLVIzxmASDWaaJkQLevS4BLK5LDAQg,16
|
|
66
|
+
tests/conftest.py,sha256=QZcp08htUlJGgmHDlRWFgsWXZ8o8IBWTD5QqJaUMlU8,790
|
|
67
|
+
tests/test_cli_output.py,sha256=YdIQtplncTeMf82E0nWNrGYoYAFI8Man4zneYkWmbxk,14425
|
|
68
|
+
tests/test_file_utils.py,sha256=-O5zImiFVz9JJ4xi8TbsUwNgPFQoQUOl9Frls4BHCBI,1358
|
|
69
|
+
tests/test_git_utils.py,sha256=h3rWhhs4XVRYqzVzdmj51siF_AYD-GkbZiefK2iGfWo,15614
|
|
70
|
+
tests/test_imports.py,sha256=EWYLUxGunUUdsG9iCiY_tts6T7DIVdhdB_MZlB1LsmU,2081
|
|
71
|
+
tests/test_partial_rendering.py,sha256=4xXZhy9yStn0awSw8B-yrJ3kVYyzJrlR-dKwg9ODXMY,11188
|
|
72
|
+
tests/test_plain_modules.py,sha256=9bWsketDnjpCWkCbMZzYvLqT1ojPhLyxxKljPUq42oY,11393
|
|
73
|
+
tests/test_plainfile.py,sha256=wZ_PN4GULPWIpl_AK_r_6jkgtX0Ch8EHmN5IhX7huOE,11027
|
|
74
|
+
tests/test_plainfileparser.py,sha256=7AassxH7AKX4lfW6E_58FYxRe-IzIiA16BMVWPjWqCM,18750
|
|
75
|
+
tests/test_plainspec.py,sha256=JrzjJ4VFYDmWTZDq-8WFyt2oMx76IEuO1CAUPQeGOsw,2404
|
|
76
|
+
tests/test_requires.py,sha256=6tnHYaKudxN-RiQjSFoi9nHe_x0ze3QF-f-j67zAE74,1250
|
|
77
|
+
tests/test_resolve_config_file.py,sha256=b87tDOvyzbsnE-QRWbZ97ou7NuyaAgskv-dbYBum31c,2830
|
|
78
|
+
tests/e2e/conftest.py,sha256=5Dm67UTOE-M6hMb-mqZvwa6Ek_wmZgsR7fUv9ZEqCwY,4164
|
|
79
|
+
tests/e2e/test_hello_world_python.py,sha256=PQy4elzB7WvpiWLKa6_HFO0K8HA6_6hGLaAqRi0J3OU,1627
|
|
80
|
+
tests/e2e/test_hello_world_python_windows.py,sha256=kJQZIuzj8OEEtgBVxnzFvyQJ_wvP2oBu9L-E9geg8sE,2010
|
|
81
|
+
tui/__init__.py,sha256=_fy7FowY0RkWdn4kC7XV2jgvQPCj-2gE8fNUb-Mny0A,60
|
|
82
|
+
tui/components.py,sha256=DoMDhwNVVpv--OY6o8YQYPzOQPG9Dyh9kgMOqUYtNiE,25412
|
|
83
|
+
tui/models.py,sha256=KMjlWubmPD41GA-BjgiqkaV9v7tLJ0fuWKBDPmZj4JA,1493
|
|
84
|
+
tui/plain2code_tui.py,sha256=t-0DonjrK6_x6jPUMYW0w6tvjZZlk89E-DEor7GzjG4,13785
|
|
85
|
+
tui/plain_module_render_choice_tui.py,sha256=MLc-Ps3O2DMBKRCPJAxUY059SdDIYq2ha7-sWxuguj0,9611
|
|
86
|
+
tui/spinner.py,sha256=Ro6Gd9Przf-whuHqPRY6HwI0T57yJjyNPbhDbigZKZE,2471
|
|
87
|
+
tui/state_handlers.py,sha256=4r_6m5YMcprQtgiTP0De3XFfGAF8VzgaWXHxPweYPbQ,16325
|
|
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,,
|
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
|
-
|
|
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)
|
|
@@ -161,6 +161,14 @@ class CodeplainAPI:
|
|
|
161
161
|
}
|
|
162
162
|
return self.post_request(endpoint_url, headers, payload, None, num_retries=0, silent=True)
|
|
163
163
|
|
|
164
|
+
def status(self):
|
|
165
|
+
endpoint_url = f"{self.api_url}/status"
|
|
166
|
+
headers = {"Content-Type": "application/json"}
|
|
167
|
+
payload = {
|
|
168
|
+
"api_key": self.api_key,
|
|
169
|
+
}
|
|
170
|
+
return self.post_request(endpoint_url, headers, payload, None, num_retries=0, silent=True)
|
|
171
|
+
|
|
164
172
|
def render_functional_requirement(
|
|
165
173
|
self,
|
|
166
174
|
frid: str,
|
concept_utils.py
CHANGED
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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):
|
memory_management.py
CHANGED
|
@@ -24,9 +24,9 @@ class MemoryManager:
|
|
|
24
24
|
console.debug(f"Loaded {len(memory_files_content)} memory files.")
|
|
25
25
|
return memory_files, memory_files_content
|
|
26
26
|
|
|
27
|
-
def __init__(self, codeplain_api,
|
|
27
|
+
def __init__(self, codeplain_api, module_name: str, conformance_tests_folder: str):
|
|
28
28
|
self.codeplain_api = codeplain_api
|
|
29
|
-
self.memory_folder = os.path.join(
|
|
29
|
+
self.memory_folder = os.path.join(conformance_tests_folder, module_name, CODEPLAIN_MEMORY_SUBFOLDER)
|
|
30
30
|
|
|
31
31
|
def create_conformance_tests_memory(
|
|
32
32
|
self, render_context: RenderContext, exit_code: int, conformance_tests_issue: str
|
module_renderer.py
CHANGED
|
@@ -60,7 +60,6 @@ class ModuleRenderer:
|
|
|
60
60
|
render_range=render_range,
|
|
61
61
|
render_conformance_tests=self.args.render_conformance_tests,
|
|
62
62
|
base_folder=self.args.base_folder,
|
|
63
|
-
verbose=self.args.verbose,
|
|
64
63
|
run_state=self.run_state,
|
|
65
64
|
event_bus=self.event_bus,
|
|
66
65
|
test_script_timeout=self.args.test_script_timeout,
|
|
@@ -116,10 +115,8 @@ class ModuleRenderer:
|
|
|
116
115
|
|
|
117
116
|
memory_manager = MemoryManager(
|
|
118
117
|
self.codeplainAPI,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
plain_module.module_name,
|
|
122
|
-
),
|
|
118
|
+
plain_module.module_name,
|
|
119
|
+
self.args.conformance_tests_folder,
|
|
123
120
|
)
|
|
124
121
|
render_context = self._build_render_context_for_module(
|
|
125
122
|
plain_module,
|