Skip to content

Commit 91f29e8

Browse files
committed
- support for standard template library
- '--full-plain' command line flag that returns full plain text source with all templates applied.
1 parent de1bd79 commit 91f29e8

7 files changed

+198
-30
lines changed

file_utils.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
import difflib
44

5-
from liquid import Environment, FileSystemLoader, StrictUndefined
6-
from liquid.exceptions import UndefinedError
5+
from liquid2 import Environment, FileSystemLoader, StrictUndefined
6+
from liquid2.exceptions import UndefinedError
7+
8+
import plain_spec
79

810
BINARY_FILE_EXTENSIONS = ['.pyc']
911

@@ -198,8 +200,8 @@ def __init__(self, *args, **kwargs):
198200
super().__init__(*args, **kwargs)
199201
self.loaded_templates = {}
200202

201-
def get_source(self, environment, template_name):
202-
source = super().get_source(environment, template_name)
203+
def get_source(self, environment, template_name, **kwargs):
204+
source = super().get_source(environment, template_name, **kwargs)
203205
self.loaded_templates[template_name] = source.source
204206
return source
205207

@@ -212,10 +214,13 @@ def get_loaded_templates(source_path, plain_source):
212214
loader=liquid_loader,
213215
undefined=StrictUndefined
214216
)
217+
218+
liquid_env.filters["code_variable"] = plain_spec.code_variable_liquid_filter
219+
215220
plain_source_template = liquid_env.from_string(plain_source)
216221
try:
217-
plain_source_template.render()
222+
plain_source = plain_source_template.render()
218223
except UndefinedError as e:
219224
raise Exception(f"Undefined liquid variable: {str(e)}")
220225

221-
return liquid_loader.loaded_templates
226+
return plain_source, liquid_loader.loaded_templates

plain2code.py

+38-13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
DEFAULT_BUILD_FOLDER = 'build'
1717
DEFAULT_CONFORMANCE_TESTS_FOLDER = "conformance_tests"
1818
CONFORMANCE_TESTS_DEFINITION_FILE_NAME = "conformance_tests.json"
19+
DEFAULT_TEMPLATE_DIRS = "standard_template_library"
1920

2021
MAX_UNITTEST_FIX_ATTEMPTS = 10
2122
MAX_CONFORMANCE_TEST_FIX_ATTEMPTS = 10
@@ -130,8 +131,8 @@ def run_unittests(args, codeplainAPI, frid, plain_source_tree, linked_resources,
130131
return existing_files, changed_files
131132

132133

133-
def generate_conformance_tests(args, codeplainAPI, frid, functional_requirement_id, plain_source_tree, linked_resources, existing_files, conformance_tests_folder_name):
134-
specifications = plain_spec.get_specifications_for_frid(plain_source_tree, functional_requirement_id)
134+
def generate_conformance_tests(args, codeplainAPI, frid, functional_requirement_id, plain_source_tree, linked_resources, existing_files_content, conformance_tests_folder_name):
135+
specifications, _ = plain_spec.get_specifications_for_frid(plain_source_tree, functional_requirement_id)
135136
if args.verbose:
136137
# TODO: Print the definitions.
137138
print(f"\nImplementing test requirements:")
@@ -156,8 +157,6 @@ def generate_conformance_tests(args, codeplainAPI, frid, functional_requirement_
156157

157158
file_utils.delete_files_and_subfolders(conformance_tests_folder_name, args.verbose)
158159

159-
existing_files_content = file_utils.get_existing_files_content(args.build_folder, existing_files)
160-
161160
response_files = codeplainAPI.render_conformance_tests(frid, functional_requirement_id, plain_source_tree, linked_resources, existing_files_content)
162161

163162
file_utils.store_response_files(conformance_tests_folder_name, response_files, [])
@@ -195,7 +194,7 @@ def run_conformance_tests(args, codeplainAPI, frid, functional_requirement_id, p
195194

196195
print("Recreating conformance tests.")
197196

198-
generate_conformance_tests(args, codeplainAPI, frid, functional_requirement_id, plain_source_tree, linked_resources, existing_files, conformance_tests_folder_name)
197+
generate_conformance_tests(args, codeplainAPI, frid, functional_requirement_id, plain_source_tree, linked_resources, existing_files_content, conformance_tests_folder_name)
199198

200199
recreated_conformance_tests = True
201200
conformance_test_fix_count = 0
@@ -240,7 +239,7 @@ def run_conformance_tests(args, codeplainAPI, frid, functional_requirement_id, p
240239

241240
def conformance_testing(args, codeplainAPI, frid, plain_source_tree, linked_resources, existing_files, conformance_tests):
242241
conformance_tests_run_count = 0
243-
specifications = plain_spec.get_specifications_for_frid(plain_source_tree, frid)
242+
specifications, _ = plain_spec.get_specifications_for_frid(plain_source_tree, frid)
244243
while conformance_tests_run_count < MAX_CONFORMANCE_TEST_RUNS:
245244
conformance_tests_run_count += 1
246245
implementation_code_has_changed = False
@@ -269,7 +268,7 @@ def conformance_testing(args, codeplainAPI, frid, plain_source_tree, linked_reso
269268
else:
270269
conformance_tests_folder_name = None
271270

272-
conformance_tests[frid] = generate_conformance_tests(args, codeplainAPI, frid, frid, plain_source_tree, linked_resources, existing_files, conformance_tests_folder_name)
271+
conformance_tests[frid] = generate_conformance_tests(args, codeplainAPI, frid, frid, plain_source_tree, linked_resources, existing_files_content, conformance_tests_folder_name)
273272

274273
conformance_tests_folder_name = conformance_tests[functional_requirement_id]['folder_name']
275274

@@ -315,7 +314,7 @@ def render_functional_requirement(args, codeplainAPI, plain_source_tree, frid, a
315314

316315
return
317316

318-
specifications = plain_spec.get_specifications_for_frid(plain_source_tree, frid)
317+
specifications, _ = plain_spec.get_specifications_for_frid(plain_source_tree, frid)
319318

320319
if args.verbose:
321320
print(f"\n-------------------------------------")
@@ -407,8 +406,13 @@ def render_functional_requirement(args, codeplainAPI, plain_source_tree, frid, a
407406
print('\n'.join(["- " + file_name for file_name in response_files.keys()]))
408407

409408
[existing_files, tmp_changed_files] = run_unittests(args, codeplainAPI, frid, plain_source_tree, linked_resources, existing_files)
410-
411-
changed_files.update(tmp_changed_files)
409+
410+
for file_name in tmp_changed_files:
411+
if file_name not in existing_files:
412+
if file_name in changed_files:
413+
changed_files.remove(file_name)
414+
else:
415+
changed_files.add(file_name)
412416

413417
num_refactoring_iterations = 0
414418
while num_refactoring_iterations < MAX_REFACTORING_ITERATIONS:
@@ -439,7 +443,13 @@ def render_functional_requirement(args, codeplainAPI, plain_source_tree, frid, a
439443
print('\n'.join(response_files.keys()))
440444

441445
[existing_files, tmp_changed_files] = run_unittests(args, codeplainAPI, frid, plain_source_tree, linked_resources, existing_files)
442-
changed_files.update(tmp_changed_files)
446+
447+
for file_name in tmp_changed_files:
448+
if file_name not in existing_files:
449+
if file_name in changed_files:
450+
changed_files.remove(file_name)
451+
else:
452+
changed_files.add(file_name)
443453

444454
if args.conformance_tests_script and plain_spec.TEST_REQUIREMENTS in specifications and specifications[plain_spec.TEST_REQUIREMENTS]:
445455
conformance_tests = conformance_testing(args, codeplainAPI, frid, plain_source_tree, linked_resources, existing_files, conformance_tests)
@@ -490,7 +500,19 @@ def render(args):
490500
with open(args.filename, "r") as fin:
491501
plain_source = fin.read()
492502

493-
loaded_templates = file_utils.get_loaded_templates(os.path.dirname(args.filename), plain_source)
503+
template_dirs = [
504+
os.path.dirname(args.filename),
505+
os.path.join(os.path.dirname( os.path.abspath(__file__)), DEFAULT_TEMPLATE_DIRS)
506+
]
507+
508+
[full_plain_source, loaded_templates] = file_utils.get_loaded_templates(template_dirs, plain_source)
509+
510+
if args.full_plain:
511+
if args.verbose:
512+
print("Full plain text:\n")
513+
514+
print(full_plain_source)
515+
return
494516

495517
codeplainAPI = codeplain_api.CodeplainAPI(args.api_key)
496518
codeplainAPI.debug = args.debug
@@ -531,6 +553,7 @@ def render(args):
531553
parser.add_argument('--conformance-tests-script', type=str, help='a script to run conformance tests')
532554
parser.add_argument('--api', type=str, nargs='?', const="https://api.codeplain.ai", help='force using the API (for internal use)')
533555
parser.add_argument('--api-key', type=str, default=CLAUDE_API_KEY, help='API key used to access the API. If not provided, the CLAUDE_API_KEY environment variable is used.')
556+
parser.add_argument('--full-plain', action='store_true', help='full plain text to render')
534557

535558
args = parser.parse_args()
536559

@@ -543,7 +566,9 @@ def render(args):
543566
print(f"Running plain2code using REST API at {args.api }\n")
544567
import codeplain_REST_api as codeplain_api
545568
else:
546-
print(f"Running plain2code using local API.\n")
569+
if args.verbose or not args.full_plain:
570+
print(f"Running plain2code using local API.\n")
571+
547572
codeplain_api = importlib.import_module(codeplain_api_module_name)
548573

549574
try:

plain_spec.py

+64-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import copy
2+
import uuid
3+
import hashlib
4+
import json
5+
from liquid2.filter import with_context
26

37

48
DEFINITIONS = 'Definitions:'
@@ -118,7 +122,23 @@ def get_previous_frid(plain_source_tree, frid):
118122
raise Exception(f"Functional requirement {frid} does not exist.")
119123

120124

121-
def get_specifications_from_plain_source_tree(frid, plain_source_tree, definitions, non_functional_requirements, test_requirements, functional_requirements, section_id=None):
125+
def get_specification_item_markdown(specification_item, code_variables, replace_code_variables):
126+
markdown = specification_item['markdown']
127+
if 'code_variables' in specification_item:
128+
for code_variable in specification_item['code_variables']:
129+
if code_variable['name'] in code_variables:
130+
if code_variables[code_variable['name']] != code_variable['value']:
131+
raise Exception(f"Code variable {code_variable['name']} has multiple values: {code_variables[code_variable['name']]} and {code_variable['value']}")
132+
else:
133+
code_variables[code_variable['name']] = code_variable['value']
134+
135+
if replace_code_variables:
136+
markdown = markdown.replace(f"{{{{ {code_variable['name']} }}}}", code_variable['value'])
137+
138+
return markdown
139+
140+
141+
def get_specifications_from_plain_source_tree(frid, plain_source_tree, definitions, non_functional_requirements, test_requirements, functional_requirements, code_variables, replace_code_variables, section_id=None):
122142
return_frid = None
123143
if FUNCTIONAL_REQUIREMENTS in plain_source_tree and len(plain_source_tree[FUNCTIONAL_REQUIREMENTS]) > 0:
124144
functional_requirement_count = 0
@@ -129,43 +149,77 @@ def get_specifications_from_plain_source_tree(frid, plain_source_tree, definitio
129149
else:
130150
current_frid = section_id + "." + str(functional_requirement_count)
131151

132-
functional_requirements.append(functional_requirement['markdown'])
152+
functional_requirements.append(get_specification_item_markdown(functional_requirement, code_variables, replace_code_variables))
133153

134154
if current_frid == frid:
135155
return_frid = current_frid
136156
break
137157

138158
if 'sections' in plain_source_tree:
139159
for section in plain_source_tree['sections']:
140-
sub_frid = get_specifications_from_plain_source_tree(frid, section, definitions, non_functional_requirements, test_requirements, functional_requirements, section['ID'])
160+
sub_frid = get_specifications_from_plain_source_tree(frid, section, definitions, non_functional_requirements, test_requirements, functional_requirements, code_variables, replace_code_variables, section['ID'])
141161
if sub_frid is not None:
142162
return_frid = sub_frid
143163
break
144164

145165
if return_frid is not None:
146166
if DEFINITIONS in plain_source_tree and plain_source_tree[DEFINITIONS] is not None:
147-
definitions[0:0] = [specification['markdown'] for specification in plain_source_tree[DEFINITIONS]]
167+
definitions[0:0] = [get_specification_item_markdown(specification, code_variables, replace_code_variables) for specification in plain_source_tree[DEFINITIONS]]
148168
if NON_FUNCTIONAL_REQUIREMENTS in plain_source_tree and plain_source_tree[NON_FUNCTIONAL_REQUIREMENTS] is not None:
149-
non_functional_requirements[0:0] = [specification['markdown'] for specification in plain_source_tree[NON_FUNCTIONAL_REQUIREMENTS]]
169+
non_functional_requirements[0:0] = [get_specification_item_markdown(specification, code_variables, replace_code_variables) for specification in plain_source_tree[NON_FUNCTIONAL_REQUIREMENTS]]
150170
if TEST_REQUIREMENTS in plain_source_tree and plain_source_tree[TEST_REQUIREMENTS] is not None:
151-
test_requirements[0:0] = [specification['markdown'] for specification in plain_source_tree[TEST_REQUIREMENTS]]
171+
test_requirements[0:0] = [get_specification_item_markdown(specification, code_variables, replace_code_variables) for specification in plain_source_tree[TEST_REQUIREMENTS]]
152172

153173
return return_frid
154174

155175

156-
def get_specifications_for_frid(plain_source_tree, frid):
176+
def get_specifications_for_frid(plain_source_tree, frid, replace_code_variables=True):
157177
definitions = []
158178
non_functional_requirements = []
159179
test_requirements = []
160180
functional_requirements = []
161181

162-
result = get_specifications_from_plain_source_tree(frid, plain_source_tree, definitions, non_functional_requirements, test_requirements, functional_requirements)
182+
code_variables = {}
183+
184+
result = get_specifications_from_plain_source_tree(frid, plain_source_tree, definitions, non_functional_requirements, test_requirements, functional_requirements, code_variables, replace_code_variables)
163185
if result is None:
164186
raise Exception(f"Functional requirement {frid} does not exist.")
165187

166-
return {
188+
specifications = {
167189
DEFINITIONS: definitions,
168190
NON_FUNCTIONAL_REQUIREMENTS: non_functional_requirements,
169191
TEST_REQUIREMENTS: test_requirements,
170192
FUNCTIONAL_REQUIREMENTS: functional_requirements
171-
}
193+
}
194+
195+
if code_variables:
196+
return specifications, code_variables
197+
else:
198+
return specifications, None
199+
200+
201+
@with_context
202+
def code_variable_liquid_filter(value, *, context):
203+
if len(context.scope) == 0:
204+
raise Exception("Invalid use of code_variable filter!")
205+
206+
if 'code_variables' in context.globals:
207+
code_variables = context.globals['code_variables']
208+
209+
variable = next(iter(context.scope.items()))
210+
211+
unique_str = uuid.uuid4().hex
212+
213+
code_variables[unique_str] = {variable[0]: value}
214+
215+
return unique_str
216+
else:
217+
return value
218+
219+
220+
def hash_text(text):
221+
return hashlib.sha256(text.encode()).hexdigest()
222+
223+
224+
def get_hash_value(specifications):
225+
return hash_text(json.dumps(specifications, indent=4))

requirements.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
requests
2-
python-liquid
2+
python-liquid2==0.3.0
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
***Definitions:***
2+
3+
- The App is a console application.
4+
5+
6+
***Non-Functional Requirements:***
7+
8+
- Implementation should be in Go lang.
9+
10+
- The Implementation should include unit tests using Go's built-in testing package.
11+
12+
- The main executable code file of The App should be called "{{ main_executable_file_name | code_variable }}". The main executable code file does not have any semantic meaning and it should be completely disregarded when generating the code (except for the file name).
13+
14+
15+
***Test Requirements:***
16+
17+
- The Conformance Tests of The App should be implemented in Go lang using Go's built-in testing package.
18+
19+
- The main executable code file of The App is "{{ main_executable_file_name | code_variable }}".
20+
21+
- The current working directory contains the file "{{ main_executable_file_name | code_variable }}".
22+
23+
- The App can be executed using the command "go run {{ main_executable_file_name | code_variable }}".
24+
25+
26+
# The Go lang console application boilerplate
27+
28+
***Functional Requirements:***
29+
30+
- Implement the entry point for The App.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
***Definitions:***
2+
3+
- The App is a console application.
4+
5+
6+
***Non-Functional Requirements:***
7+
8+
- The App should be implemented in Python (The Implementation).
9+
10+
- The Implementation should include unit tests using Unittest framework (The Unittests)! If the unittests are put in the subfolder, make sure to include __init__.py to make them discoverable.
11+
12+
- The main executable code file of The App should be called "{{ main_executable_file_name | code_variable }}". The main executable code file does not have any semantic meaning and it should be completely disregarded when generating the code (except for the file name).
13+
14+
15+
***Test Requirements:***
16+
17+
- The Conformance Tests of The App should be implemented in Python using Unittest framework. The Conformance Tests will be run using "python -m unittest discover" command. Therefore, if the unittests are put in the subfolder, make sure to include __init__.py to make them discoverable.
18+
19+
- The Conformance Tests must be implemented and executed - do not use unittest.skip() or “any other test skipping functionality.
20+
21+
- The main executable code file of The App is "{{ main_executable_file_name | code_variable }}".
22+
23+
- The current working directory contains the file "{{ main_executable_file_name | code_variable }}".
24+
25+
- The App can be executed using the command "python {{ main_executable_file_name | code_variable }}".
26+
27+
28+
# The Python console application boilerplate
29+
30+
***Functional Requirements:***
31+
32+
- Implement the entry point for The App.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
***Definitions:***
2+
3+
- The App is a web application.
4+
5+
6+
***Non-Functional Requirements:***
7+
8+
- The App should be implemented in TypeScript, using React as a web framework.
9+
10+
- Please note that The App will be executed using npm.
11+
12+
13+
***Test Requirements:***
14+
15+
- The Conformance Tests of The App should be written in TypeScript, using Cypress as the framework for The Conformance Tests.
16+
17+
18+
# The React boilerplate
19+
20+
***Functional Requirements:***
21+
22+
- Implement the entry point for The App.

0 commit comments

Comments
 (0)