Skip to content

Commit cdd613a

Browse files
author
Tjaž Eržen
authored
Merge pull request #32 from Codeplain-ai/feat/10-07-release
Feat/10-07-release
2 parents 733d5fe + dc06426 commit cdd613a

12 files changed

Lines changed: 335 additions & 57 deletions

codeplain_REST_api.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,3 +346,27 @@ def render_acceptance_tests(
346346
}
347347

348348
return self.post_request(endpoint_url, headers, payload, run_state)
349+
350+
def analyze_rendering(
351+
self,
352+
frid,
353+
plain_source_tree,
354+
linked_resources,
355+
existing_files_content,
356+
implementation_code_diff,
357+
fixed_implementation_code_diff,
358+
run_state: RunState,
359+
):
360+
endpoint_url = f"{self.api_url}/analyze_rendering"
361+
headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
362+
363+
payload = {
364+
"frid": frid,
365+
"plain_source_tree": plain_source_tree,
366+
"linked_resources": linked_resources,
367+
"existing_files_content": existing_files_content,
368+
"implementation_code_diff": implementation_code_diff,
369+
"fixed_implementation_code_diff": fixed_implementation_code_diff,
370+
}
371+
372+
return self.post_request(endpoint_url, headers, payload, run_state)

file_utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from liquid2.exceptions import UndefinedError
77

88
import plain_spec
9+
from plain2code_nodes import Plain2CodeIncludeTag, Plain2CodeLoaderMixin
910

1011
BINARY_FILE_EXTENSIONS = [".pyc"]
1112

@@ -212,7 +213,7 @@ def load_linked_resources(template_dirs: list[str], resources_list):
212213
return linked_resources
213214

214215

215-
class TrackingFileSystemLoader(FileSystemLoader):
216+
class TrackingFileSystemLoader(Plain2CodeLoaderMixin, FileSystemLoader):
216217
def __init__(self, *args, **kwargs):
217218
super().__init__(*args, **kwargs)
218219
self.loaded_templates = {}
@@ -226,8 +227,10 @@ def get_source(self, environment, template_name, **kwargs):
226227
def get_loaded_templates(source_path, plain_source):
227228
# Render the plain source with Liquid templating engine
228229
# to identify the templates that are being loaded
230+
229231
liquid_loader = TrackingFileSystemLoader(source_path)
230232
liquid_env = Environment(loader=liquid_loader, undefined=StrictUndefined)
233+
liquid_env.tags["include"] = Plain2CodeIncludeTag(liquid_env)
231234

232235
liquid_env.filters["code_variable"] = plain_spec.code_variable_liquid_filter
233236
liquid_env.filters["prohibited_chars"] = plain_spec.prohibited_chars_liquid_filter

git_utils.py

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -109,33 +109,43 @@ def revert_to_commit_with_frid(repo_path: Union[str, os.PathLike], frid: Optiona
109109
return repo
110110

111111

112-
def diff(repo_path: Union[str, os.PathLike], previous_frid: str = None) -> dict:
112+
def checkout_commit_with_frid(repo_path: Union[str, os.PathLike], frid: Optional[str] = None) -> Repo:
113113
"""
114-
Get the git diff between the current code state and the previous frid using git's native diff command.
115-
If previous_frid is not provided, we try to find the commit related to the copy of the base folder.
116-
Removes the 'diff --git' and 'index' lines to get clean unified diff format.
114+
Finds commit with given frid mentioned in the commit message and checks out that commit.
115+
116+
If frid argument is not provided (None), repo is checked out to the initial state. In case the base folder doesn't exist,
117+
code is checked out to the initial repo commit. Otherwise, the repo is checked out to the base folder commit.
117118
119+
It is expected that the repo has at least one commit related to provided frid if frid is not None.
120+
In case the frid related commit is not found, an exception is raised.
121+
"""
122+
repo = Repo(repo_path)
123+
124+
commit = _get_commit(repo, frid)
125+
126+
if not commit:
127+
raise InvalidGitRepositoryError("Git repository is in an invalid state. Relevant commit could not be found.")
128+
129+
repo.git.checkout(commit)
130+
return repo
131+
132+
133+
def checkout_previous_branch(repo_path: Union[str, os.PathLike]) -> Repo:
134+
"""
135+
Checks out the previous branch using 'git checkout -'.
118136
119137
Args:
120138
repo_path (str | os.PathLike): Path to the git repository
121-
previous_frid (str): Functional requirement ID (FRID) of the previous commit
122139
123140
Returns:
124-
dict: Dictionary with file names as keys and their clean diff strings as values
141+
Repo: The git repository object
125142
"""
126143
repo = Repo(repo_path)
144+
repo.git.checkout("-")
145+
return repo
127146

128-
commit = _get_commit(repo, previous_frid)
129-
130-
# Add all files to the index to get a clean diff
131-
repo.git.add("-N", ".")
132-
133-
# Get the raw git diff output, excluding .pyc files
134-
diff_output = repo.git.diff(commit, "--text", ":!*.pyc")
135-
136-
if not diff_output:
137-
return {}
138147

148+
def _get_diff_dict(diff_output: str) -> dict:
139149
diff_dict = {}
140150
current_file = None
141151
current_diff_lines = []
@@ -184,6 +194,36 @@ def diff(repo_path: Union[str, os.PathLike], previous_frid: str = None) -> dict:
184194
return diff_dict
185195

186196

197+
def diff(repo_path: Union[str, os.PathLike], previous_frid: str = None) -> dict:
198+
"""
199+
Get the git diff between the current code state and the previous frid using git's native diff command.
200+
If previous_frid is not provided, we try to find the commit related to the copy of the base folder.
201+
Removes the 'diff --git' and 'index' lines to get clean unified diff format.
202+
203+
204+
Args:
205+
repo_path (str | os.PathLike): Path to the git repository
206+
previous_frid (str): Functional requirement ID (FRID) of the previous commit
207+
208+
Returns:
209+
dict: Dictionary with file names as keys and their clean diff strings as values
210+
"""
211+
repo = Repo(repo_path)
212+
213+
commit = _get_commit(repo, previous_frid)
214+
215+
# Add all files to the index to get a clean diff
216+
repo.git.add("-N", ".")
217+
218+
# Get the raw git diff output, excluding .pyc files
219+
diff_output = repo.git.diff(commit, "--text", ":!*.pyc")
220+
221+
if not diff_output:
222+
return {}
223+
224+
return _get_diff_dict(diff_output)
225+
226+
187227
def _get_commit(repo: Repo, frid: str = None) -> str:
188228
if frid:
189229
commit = _get_commit_with_frid(repo, frid)
@@ -218,3 +258,47 @@ def _get_commit_with_message(repo: Repo, message: str) -> str:
218258
escaped_message = message.replace("[", "\\[").replace("]", "\\]")
219259

220260
return repo.git.rev_list(repo.active_branch.name, "--grep", escaped_message, "-n", "1")
261+
262+
263+
def get_implementation_code_diff(repo_path: Union[str, os.PathLike], frid: str, previous_frid: str) -> dict:
264+
repo = Repo(repo_path)
265+
266+
implementation_commit = _get_commit_with_message(repo, REFACTORED_CODE_COMMIT_MESSAGE.format(frid))
267+
if not implementation_commit:
268+
implementation_commit = _get_commit_with_message(
269+
repo, FUNCTIONAL_REQUIREMENT_IMPLEMENTED_COMMIT_MESSAGE.format(frid)
270+
)
271+
272+
previous_frid_commit = _get_commit(repo, previous_frid)
273+
274+
# Get the raw git diff output, excluding .pyc files
275+
diff_output = repo.git.diff(previous_frid_commit, implementation_commit, "--text", ":!*.pyc")
276+
277+
if not diff_output:
278+
return {}
279+
280+
return _get_diff_dict(diff_output)
281+
282+
283+
def get_fixed_implementation_code_diff(repo_path: Union[str, os.PathLike], frid: str) -> dict:
284+
repo = Repo(repo_path)
285+
286+
implementation_commit = _get_commit_with_message(repo, REFACTORED_CODE_COMMIT_MESSAGE.format(frid))
287+
if not implementation_commit:
288+
implementation_commit = _get_commit_with_message(
289+
repo, FUNCTIONAL_REQUIREMENT_IMPLEMENTED_COMMIT_MESSAGE.format(frid)
290+
)
291+
292+
conformance_tests_passed_commit = _get_commit_with_message(
293+
repo, CONFORMANCE_TESTS_PASSED_COMMIT_MESSAGE.format(frid)
294+
)
295+
if not conformance_tests_passed_commit:
296+
return None
297+
298+
# Get the raw git diff output, excluding .pyc files
299+
diff_output = repo.git.diff(implementation_commit, conformance_tests_passed_commit, "--text", ":!*.pyc")
300+
301+
if not diff_output:
302+
return {}
303+
304+
return _get_diff_dict(diff_output)

plain2code.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,13 @@ def get_render_range_from(start, plain_source_tree):
5656

5757
def _get_frids_range(plain_source_tree, start, end=None):
5858
frids = list(plain_spec.get_frids(plain_source_tree))
59-
60-
# Convert start to string to handle numeric inputs
59+
6160
start = str(start)
6261

6362
if start not in frids:
6463
raise InvalidFridArgument(f"Invalid start functional requirement ID: {start}. Valid IDs are: {frids}.")
6564

6665
if end is not None:
67-
# Convert end to string to handle numeric inputs
6866
end = str(end)
6967
if end not in frids:
7068
raise InvalidFridArgument(f"Invalid end functional requirement ID: {end}. Valid IDs are: {frids}.")
@@ -166,9 +164,7 @@ def run_unittests(
166164
)
167165

168166
if len(unittests_issue) > MAX_ISSUE_LENGTH:
169-
console.warning(
170-
f"Unit tests issue text is too long and will be smartly truncated to {MAX_ISSUE_LENGTH} characters."
171-
)
167+
console.warning("Unit tests issue text is too long and will be summarized.")
172168

173169
existing_files_content = file_utils.get_existing_files_content(args.build_folder, existing_files)
174170

@@ -361,9 +357,7 @@ def run_conformance_tests( # noqa: C901
361357
return [False, False, existing_files, True]
362358

363359
if len(conformance_tests_issue) > MAX_ISSUE_LENGTH:
364-
console.warning(
365-
f"Conformance tests issue text is too long and will be smartly truncated to {MAX_ISSUE_LENGTH} characters."
366-
)
360+
console.warning("Conformance tests issue text is too long and will be summarized.")
367361

368362
conformance_tests_files_content = file_utils.get_existing_files_content(
369363
conformance_tests_folder_name, conformance_tests_files
@@ -996,10 +990,9 @@ def render_functional_requirement( # noqa: C901
996990
)
997991

998992
# Phase 4: Conformance test the code.
999-
if (
1000-
args.render_conformance_tests
1001-
and (plain_spec.TEST_REQUIREMENTS in specifications or plain_spec.ACCEPTANCE_TESTS in specifications)
1002-
and (specifications[plain_spec.TEST_REQUIREMENTS] or specifications[plain_spec.ACCEPTANCE_TESTS])
993+
if args.render_conformance_tests and (
994+
(len(specifications.get(plain_spec.TEST_REQUIREMENTS, [])) > 0)
995+
or (len(specifications.get(plain_spec.ACCEPTANCE_TESTS, [])) > 0)
1003996
):
1004997
console.info("\n[b]Implementing conformance tests...[/b]\n")
1005998

plain2code_arguments.py

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ def frid_string(s):
5757
if not s:
5858
raise argparse.ArgumentTypeError("The functional requirement ID cannot be empty.")
5959

60-
6160
if not re.match(r"^\d+(\.\d+)*$", s):
6261
raise argparse.ArgumentTypeError(
6362
"Functional requirement ID string must contain only numbers optionally separated by dots (e.g. '1', '1.2.3')"
@@ -139,23 +138,23 @@ def create_parser():
139138

140139
render_range_group = parser.add_mutually_exclusive_group()
141140
render_range_group.add_argument(
142-
"--render-range",
143-
type=frid_range_string,
141+
"--render-range",
142+
type=frid_range_string,
144143
help="Specify a range of functional requirements to render (e.g. '1.1,2.3'). "
145-
"Use comma to separate start and end IDs. If only one ID is provided, only that requirement is rendered. "
146-
"Range is inclusive of both start and end IDs."
144+
"Use comma to separate start and end IDs. If only one ID is provided, only that requirement is rendered. "
145+
"Range is inclusive of both start and end IDs.",
147146
)
148147
render_range_group.add_argument(
149-
"--render-from",
150-
type=frid_string,
148+
"--render-from",
149+
type=frid_string,
151150
help="Continue generation starting from this specific functional requirement (e.g. '2.1'). "
152-
"The requirement with this ID will be included in the output. The ID must match one of the functional requirements in your plain file."
151+
"The requirement with this ID will be included in the output. The ID must match one of the functional requirements in your plain file.",
153152
)
154153

155154
parser.add_argument(
156-
"--unittests-script",
157-
type=str,
158-
help="Shell script to run unit tests on generated code. Receives the build folder path as its first argument (default: 'build')."
155+
"--unittests-script",
156+
type=str,
157+
help="Shell script to run unit tests on generated code. Receives the build folder path as its first argument (default: 'build').",
159158
)
160159
parser.add_argument(
161160
"--conformance-tests-folder",
@@ -164,14 +163,18 @@ def create_parser():
164163
help="Folder for conformance test files",
165164
)
166165
parser.add_argument(
167-
"--conformance-tests-script",
168-
type=str,
166+
"--conformance-tests-script",
167+
type=str,
169168
help="Path to conformance tests shell script. The script should accept two arguments: "
170-
"1) First argument: path to a folder (e.g. 'build') containing generated source code, "
171-
"2) Second argument: path to a subfolder of the conformance tests folder (e.g. 'conformance_tests/subfoldername') containing test files."
169+
"1) First argument: path to a folder (e.g. 'build') containing generated source code, "
170+
"2) Second argument: path to a subfolder of the conformance tests folder (e.g. 'conformance_tests/subfoldername') containing test files.",
172171
)
173172
parser.add_argument(
174-
"--api", type=str, nargs="?", const="https://api.codeplain.ai", help="Alternative base URL for the API. Default: `https://api.codeplain.ai`"
173+
"--api",
174+
type=str,
175+
nargs="?",
176+
const="https://api.codeplain.ai",
177+
help="Alternative base URL for the API. Default: `https://api.codeplain.ai`",
175178
)
176179
parser.add_argument(
177180
"--api-key",
@@ -180,11 +183,11 @@ def create_parser():
180183
help="API key used to access the API. If not provided, the CLAUDE_API_KEY environment variable is used.",
181184
)
182185
parser.add_argument(
183-
"--full-plain",
184-
action="store_true",
186+
"--full-plain",
187+
action="store_true",
185188
help="Display the complete plain specification before code generation. "
186-
"This shows your plain file with "
187-
"any included template content expanded. Useful for understanding what content is being processed."
189+
"This shows your plain file with "
190+
"any included template content expanded. Useful for understanding what content is being processed.",
188191
)
189192
parser.add_argument(
190193
"--dry-run", action="store_true", help="Preview of what Codeplain would do without actually making any changes."

0 commit comments

Comments
 (0)