Skip to content

Commit 14f5ecc

Browse files
authored
refactor: use jinja for comments instead of inline string formatting (#84)
1 parent e2732ca commit 14f5ecc

File tree

11 files changed

+258
-143
lines changed

11 files changed

+258
-143
lines changed

poetry.lock

Lines changed: 4 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ dependencies = [
3737
"python-gitlab>=5.1.0,<6.0.0",
3838
"rich>=13.9.4,<14.0.0",
3939
"pygithub>=2.6.1,<3.0.0",
40-
"httpx (>=0.28.1,<0.29.0)"
40+
"httpx (>=0.28.1,<0.29.0)",
41+
"jinja2 (>=3.1.6,<4.0.0)"
4142
]
4243

4344
[tool.poetry.group.dev.dependencies]

src/lgtm_ai/formatters/markdown.py

Lines changed: 68 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,92 @@
1-
import textwrap
1+
import pathlib
2+
from typing import ClassVar
23

3-
from lgtm_ai.ai.schemas import PublishMetadata, Review, ReviewComment, ReviewGuide, ReviewScore
4+
from jinja2 import Environment, FileSystemLoader
5+
from lgtm_ai.ai.schemas import PublishMetadata, Review, ReviewComment, ReviewGuide
46
from lgtm_ai.formatters.base import Formatter
57
from lgtm_ai.formatters.constants import CATEGORY_MAP, SCORE_MAP, SEVERITY_MAP
68
from pydantic_ai.usage import Usage
79

810

911
class MarkDownFormatter(Formatter[str]):
10-
def format_review_summary_section(self, review: Review, comments: list[ReviewComment] | None = None) -> str:
11-
header = textwrap.dedent(f"""
12-
## 🦉 lgtm Review
13-
14-
> **Score:** {self._format_score(review.review_response.score)}
15-
16-
### 🔍 Summary
17-
18-
""")
19-
summary = header + review.review_response.summary
20-
if comments:
21-
summary += f"\n\n{self.format_review_comments_section(comments)}"
12+
REVIEW_SUMMARY_TEMPLATE: ClassVar[str] = "review_summary.md.j2"
13+
REVIEW_COMMENTS_SECTION_TEMPLATE: ClassVar[str] = "review_comments_section.md.j2"
14+
REVIEW_COMMENT_TEMPLATE: ClassVar[str] = "review_comment.md.j2"
15+
REVIEW_GUIDE_TEMPLATE: ClassVar[str] = "review_guide.md.j2"
16+
SNIPPET_TEMPLATE: ClassVar[str] = "snippet.md.j2"
17+
USAGES_SUMMARY_TEMPLATE: ClassVar[str] = "usages_summary.md.j2"
18+
METADATA_TEMPLATE: ClassVar[str] = "metadata.md.j2"
19+
20+
def __init__(self) -> None:
21+
template_dir = pathlib.Path(__file__).parent / "templates"
22+
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
2223

23-
summary += self._format_metadata(review.metadata)
24-
return summary
24+
def format_review_summary_section(self, review: Review, comments: list[ReviewComment] | None = None) -> str:
25+
template = self.env.get_template(self.REVIEW_SUMMARY_TEMPLATE)
26+
comments_section = self.format_review_comments_section(comments or [])
27+
metadata = self._format_metadata(review.metadata)
28+
return template.render(
29+
score=review.review_response.score,
30+
score_icon=SCORE_MAP[review.review_response.score],
31+
summary=review.review_response.summary,
32+
comments_section=comments_section,
33+
metadata=metadata,
34+
)
2535

2636
def format_review_comments_section(self, comments: list[ReviewComment]) -> str:
2737
if not comments:
2838
return ""
29-
lines = ["**Specific Comments:**"]
30-
for comment in comments:
31-
lines.append(f"- {self.format_review_comment(comment, with_footer=False)}")
32-
return "\n\n".join(lines)
39+
template = self.env.get_template(self.REVIEW_COMMENTS_SECTION_TEMPLATE)
40+
rendered_comments = [self.format_review_comment(comment, with_footer=False) for comment in comments]
41+
return template.render(comments=rendered_comments)
3342

3443
def format_review_comment(self, comment: ReviewComment, *, with_footer: bool = True) -> str:
35-
header_section = "\n\n".join(
36-
[
37-
f"#### 🦉 {CATEGORY_MAP[comment.category]} {comment.category}",
38-
f"> **Severity:** {comment.severity} {SEVERITY_MAP[comment.severity]}",
39-
]
44+
template = self.env.get_template(self.REVIEW_COMMENT_TEMPLATE)
45+
header_category = CATEGORY_MAP[comment.category]
46+
severity_icon = SEVERITY_MAP[comment.severity]
47+
snippet = self._format_snippet(comment) if comment.quote_snippet else None
48+
return template.render(
49+
category=header_category,
50+
category_key=comment.category,
51+
severity=comment.severity,
52+
severity_icon=severity_icon,
53+
snippet=snippet,
54+
comment=comment.comment,
55+
with_footer=with_footer,
56+
new_path=comment.new_path,
57+
line_number=comment.line_number,
58+
relative_line_number=comment.relative_line_number,
4059
)
41-
comment_section = (
42-
f"\n{self._format_snippet(comment)}\n{comment.comment}" if comment.quote_snippet else comment.comment
43-
)
44-
45-
footer_section = (
46-
textwrap.dedent(f"""
47-
48-
<details><summary>More information about this comment</summary>
49-
50-
- **File**: `{comment.new_path}`
51-
- **Line**: `{comment.line_number}`
52-
- **Relative line**: `{comment.relative_line_number}`
53-
54-
</details>
55-
""")
56-
if with_footer
57-
else ""
58-
)
59-
60-
return f"{header_section}\n\n{comment_section}\n\n{footer_section}"
6160

6261
def format_guide(self, guide: ReviewGuide) -> str:
63-
header = textwrap.dedent("""
64-
## 🦉 lgtm Reviewer Guide
65-
66-
""")
67-
68-
summary = guide.guide_response.summary
69-
# Format key changes as a markdown table
70-
key_changes = ["| File Name | Description |", "| ---- | ---- |"] + [
71-
f"| {change.file_name} | {change.description} |" for change in guide.guide_response.key_changes
72-
]
73-
74-
# Format checklist items as a checklist
75-
checklist = [f"- [ ] {item.description}" for item in guide.guide_response.checklist]
76-
77-
# Format references as a list
78-
if guide.guide_response.references:
79-
references = [f"- [{item.title}]({item.url})" for item in guide.guide_response.references]
80-
else:
81-
references = []
82-
83-
# Combine all sections
84-
85-
summary = (
86-
header
87-
+ "### 🔍 Summary\n\n"
88-
+ summary
89-
+ "\n\n### 🔑 Key Changes\n\n"
90-
+ "\n".join(key_changes)
91-
+ "\n\n### ✅ Reviewer Checklist\n\n"
92-
+ "\n".join(checklist)
62+
template = self.env.get_template(self.REVIEW_GUIDE_TEMPLATE)
63+
key_changes = guide.guide_response.key_changes
64+
checklist = guide.guide_response.checklist
65+
references = guide.guide_response.references
66+
metadata = self._format_metadata(guide.metadata)
67+
return template.render(
68+
summary=guide.guide_response.summary,
69+
key_changes=key_changes,
70+
checklist=checklist,
71+
references=references,
72+
metadata=metadata,
9373
)
94-
if references:
95-
summary += "\n\n### 📚 References\n\n" + "\n".join(references)
96-
97-
summary += self._format_metadata(guide.metadata)
98-
return summary
99-
100-
def _format_score(self, score: ReviewScore) -> str:
101-
return f"{score} {SCORE_MAP[score]}"
10274

10375
def _format_snippet(self, comment: ReviewComment) -> str:
104-
return f"\n\n```{comment.programming_language.lower()}\n{comment.quote_snippet}\n```\n\n"
76+
template = self.env.get_template(self.SNIPPET_TEMPLATE)
77+
return template.render(language=comment.programming_language.lower(), snippet=comment.quote_snippet)
10578

10679
def _format_usages_summary(self, usages: list[Usage]) -> str:
107-
formatted_usage_calls = []
108-
for i, usage in enumerate(usages):
109-
formatted_usage_calls += [self._format_usage_call_collapsible(usage, i)]
110-
111-
return f"""
112-
<details><summary>Usage summary</summary>
113-
{"\n".join(formatted_usage_calls)}
114-
**Total tokens**: `{sum([usage.total_tokens or 0 for usage in usages])}`
115-
</details>
116-
"""
117-
118-
def _format_usage_call_collapsible(self, usage: Usage, index: int) -> str:
119-
return f"""
120-
<details><summary>Call {index + 1}</summary>
121-
122-
- **Request count**: `{usage.requests}`
123-
- **Request tokens**: `{usage.request_tokens}`
124-
- **Response tokens**: `{usage.response_tokens}`
125-
- **Total tokens**: `{usage.total_tokens}`
126-
</details>
127-
"""
80+
template = self.env.get_template(self.USAGES_SUMMARY_TEMPLATE)
81+
total_tokens = sum(usage.total_tokens or 0 for usage in usages)
82+
return template.render(usages=usages, total_tokens=total_tokens)
12883

12984
def _format_metadata(self, metadata: PublishMetadata) -> str:
130-
return textwrap.dedent(f"""
131-
132-
<details><summary>More information</summary>
133-
134-
- **Id**: `{metadata.uuid}`
135-
- **Model**: `{metadata.model_name}`
136-
- **Created at**: `{metadata.created_at}`
137-
138-
{self._format_usages_summary(metadata.usages)}
139-
140-
> See the [📚 lgtm-ai repository](https://github.com/elementsinteractive/lgtm-ai) for more information about lgtm.
141-
142-
</details>
143-
""")
85+
template = self.env.get_template(self.METADATA_TEMPLATE)
86+
usages_summary = self._format_usages_summary(metadata.usages)
87+
return template.render(
88+
uuid=metadata.uuid,
89+
model_name=metadata.model_name,
90+
created_at=metadata.created_at,
91+
usages_summary=usages_summary,
92+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<details><summary>More information</summary>
2+
3+
- **Id**: `{{ uuid }}`
4+
- **Model**: `{{ model_name }}`
5+
- **Created at**: `{{ created_at }}`
6+
7+
8+
{{ usages_summary | safe }}
9+
10+
11+
> See the [📚 lgtm-ai repository](https://github.com/elementsinteractive/lgtm-ai) for more information about lgtm.
12+
13+
</details>
14+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#### 🦉 {{ category }} {{ category_key }}
2+
3+
> **Severity:** {{ severity }} {{ severity_icon }}
4+
5+
{% if snippet %}
6+
{{ snippet | safe }}
7+
8+
{% endif %}
9+
{{ comment | safe }}
10+
11+
{% if with_footer %}
12+
<details><summary>More information about this comment</summary>
13+
14+
- **File**: `{{ new_path }}`
15+
- **Line**: `{{ line_number }}`
16+
- **Relative line**: `{{ relative_line_number }}`
17+
</details>
18+
{% endif %}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{% if comments %}**Specific Comments:**
2+
3+
{% for comment in comments %}- {{ comment | safe }}
4+
{% endfor %}
5+
{% endif %}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
## 🦉 lgtm Reviewer Guide
3+
4+
### 🔍 Summary
5+
6+
{{ summary | safe }}
7+
8+
### 🔑 Key Changes
9+
10+
| File Name | Description |
11+
| ---- | ---- |
12+
{% for change in key_changes -%}
13+
| {{ change.file_name }} | {{ change.description }} |
14+
{% endfor %}
15+
16+
### ✅ Reviewer Checklist
17+
18+
{% for item in checklist %}
19+
- [ ] {{ item.description }}
20+
{% endfor %}
21+
22+
{% if references %}
23+
### 📚 References
24+
{% for ref in references %}
25+
- [{{ ref.title }}]({{ ref.url }})
26+
{% endfor %}
27+
{% endif %}
28+
29+
{{ metadata | safe }}
30+
31+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
## 🦉 lgtm Review
3+
4+
> **Score:** {{ score }} {{ score_icon }}
5+
6+
### 🔍 Summary
7+
8+
{{ summary | safe }}
9+
10+
{% if comments_section %}
11+
{{ comments_section | safe}}
12+
{% endif %}
13+
14+
{{ metadata | safe }}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```{{ language }}
2+
{{ snippet | safe }}
3+
```
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<details><summary>Usage summary</summary>
2+
{% for usage in usages %}
3+
4+
<details><summary>Call {{ loop.index }}</summary>
5+
6+
- **Request count**: `{{ usage.requests }}`
7+
- **Request tokens**: `{{ usage.request_tokens }}`
8+
- **Response tokens**: `{{ usage.response_tokens }}`
9+
- **Total tokens**: `{{ usage.total_tokens }}`
10+
</details>
11+
12+
{% endfor %}
13+
**Total tokens**: `{{ total_tokens }}`
14+
</details>

0 commit comments

Comments
 (0)