Skip to content

Commit c16f8c6

Browse files
authored
Merge pull request #21 from kieferro/comment-template
Add possibility to extend blocks of the comment template
2 parents a23160b + dfc62f0 commit c16f8c6

File tree

9 files changed

+264
-58
lines changed

9 files changed

+264
-58
lines changed

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,49 @@ jobs:
229229
# You typically don't have to change this unless you're already using this name for something else.
230230
COMMENT_FILENAME: python-coverage-comment-action.txt
231231

232+
# An alternative template for the comment for pull requests. See details below.
233+
COMMENT_TEMPLATE: The coverage rate is `{{ coverage.info.percent_covered | pct }}{{ marker }}`
234+
```
235+
236+
## Overriding the template
237+
238+
By default, comments are generated from a
239+
[Jinja](https://jinja.palletsprojects.com) template that you can read
240+
[here](https://github.com/ewjoachim/python-coverage-comment-action/blob/v2/coverage_comment/default.md.j2).
241+
242+
If you want to change this template, you can set ``COMMENT_TEMPLATE``. This is
243+
an advanced usage, so you're likely to run into more road bumps.
244+
245+
You will need to follow some rules for your template to be valid:
246+
247+
- Your template needs to be syntactically correct with Jinja2 rules
248+
- You may define a new template from scratch, but in this case you are required
249+
to include ``{{ marker }}``, which includes an HTML comment (invisible on
250+
GitHub) that the action uses to identify its own comments.
251+
- If you'd rather want to change parts of the default template, you can do so
252+
by starting your comment with ``{% extends "base" %}``, and then override the
253+
blocks (``{% block foo %}``) that you wish to change. If you're unsure how it
254+
works, see [the Jinja
255+
documentation](https://jinja.palletsprojects.com/en/3.0.x/templates/#template-inheritance)
256+
- In either case, you will most likely want to get yourself familiar with the
257+
available context variables, the best is to read the code from
258+
[here](https://github.com/ewjoachim/python-coverage-comment-action/blob/v2/coverage_comment/template.py).
259+
Should those variables change, we'll do our best to bump the action's major version.
260+
261+
### Examples
262+
In the first example, we change the emoji that illustrates coverage going down from
263+
`:down_arrow:` to `:sob:`:
264+
265+
```jinja2
266+
{% extends "base" %}
267+
{% block emoji_coverage_down %}:sob:{% endblock emoji_coverage_down %}
268+
```
269+
270+
In this second example, we replace the whole comment by something much shorter with the
271+
coverage (percentage) of the whole project from the PR build:
272+
273+
```jinja2
274+
Coverage: {{ coverage.info.percent_covered | pct }}
232275
```
233276

234277
# Other topics

action.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ inputs:
1818
required: false
1919
COMMENT_TEMPLATE:
2020
description: >
21-
[Advanced] Specify a different template for the comments that will be written on the PR.
21+
[Advanced] Specify a different template for the comments that will be written on
22+
the PR. See the Action README documentation for how to use this properly.
2223
required: false
2324
BADGE_FILENAME:
2425
description: >
@@ -68,6 +69,7 @@ runs:
6869
env:
6970
GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }}
7071
GITHUB_PR_RUN_ID: ${{ inputs.GITHUB_PR_RUN_ID }}
72+
COMMENT_TEMPLATE: ${{ inputs.COMMENT_TEMPLATE }}
7173
BADGE_FILENAME: ${{ inputs.BADGE_FILENAME }}
7274
COMMENT_ARTIFACT_NAME: ${{ inputs.COMMENT_ARTIFACT_NAME }}
7375
COMMENT_FILENAME: ${{ inputs.COMMENT_FILENAME }}

coverage_comment/default.md.j2

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,56 @@
1-
## Coverage report
1+
{% block title %}## Coverage report{% endblock title %}
2+
{% block coverage_evolution -%}
23
{% if previous_coverage_rate -%}
3-
The coverage rate went from `{{ previous_coverage_rate | pct }}` to `{{ coverage.info.percent_covered | pct }}` {{
4-
":arrow_up:" if previous_coverage_rate < coverage.info.percent_covered else
5-
":arrow_down:" if previous_coverage_rate > coverage.info.percent_covered else
6-
":arrow_right:"
7-
}}
4+
{% block coverage_evolution_wording -%}
5+
The coverage rate went from `{{ previous_coverage_rate | pct }}` to `{{ coverage.info.percent_covered | pct }}`{{" "}}
6+
{%- endblock coverage_evolution_wording %}
7+
{%- block emoji_coverage -%}
8+
{%- if previous_coverage_rate < coverage.info.percent_covered -%}
9+
{%- block emoji_coverage_up -%}:arrow_up:{%- endblock emoji_coverage_up -%}
10+
{%- elif previous_coverage_rate > coverage.info.percent_covered -%}
11+
{%- block emoji_coverage_down -%}:arrow_down:{%- endblock emoji_coverage_down -%}
812
{%- else -%}
9-
The coverage rate is `{{ coverage.info.percent_covered | pct }}`
13+
{%- block emoji_coverage_constant -%}:arrow_right:{%- endblock emoji_coverage_constant -%}
1014
{%- endif %}
15+
{%- endblock emoji_coverage -%}
16+
{%- else -%}
17+
{% block coverage_value_wording -%}
18+
The coverage rate is `{{ coverage.info.percent_covered | pct }}`.
19+
{%- endblock coverage_value_wording %}
20+
{%- endif %}
21+
{%- endblock coverage_evolution %}
22+
{% block branch_coverage -%}
1123
{% if coverage.meta.branch_coverage and coverage.info.num_branches -%}
12-
The branch rate is `{{ (coverage.info.covered_branches / coverage.info.num_branches) | pct }}`
24+
{% block branch_coverage_wording -%}
25+
The branch rate is `{{ (coverage.info.covered_branches / coverage.info.num_branches) | pct }}`.
26+
{% endblock branch_coverage_wording -%}
1327
{%- endif %}
28+
{% endblock branch_coverage -%}
1429

30+
{% block diff_coverage_wording -%}
1531
`{{ diff_coverage.total_percent_covered | pct }}` of new lines are covered.
32+
{%- endblock diff_coverage_wording %}
1633

34+
{% block coverage_by_file -%}
1735
{%if diff_coverage.files -%}
1836
<details>
19-
<summary>Diff Coverage details (click to unfold)</summary>
37+
<summary>{% block coverage_by_file_summary_wording -%}Diff Coverage details (click to unfold){% endblock coverage_by_file_summary_wording -%}</summary>
2038

2139
{% for filename, diff_file_coverage in diff_coverage.files.items() -%}
22-
### {{ filename }}
23-
`{{ diff_file_coverage.percent_covered | pct }}` of new lines are covered (`{{ coverage.files[filename].info.percent_covered | pct }}` of the complete file)
24-
40+
{% block coverage_single_file scoped -%}
41+
{% block coverage_single_file_title scoped %}### {{ filename }}{% endblock coverage_single_file_title %}
42+
{% block diff_coverage_single_file_wording scoped -%}
43+
`{{ diff_file_coverage.percent_covered | pct }}` of new lines are covered (`{{ coverage.files[filename].info.percent_covered | pct }}` of the complete file).
44+
{%- endblock diff_coverage_single_file_wording %}
2545
{%- if diff_file_coverage.violation_lines %}
46+
{% block single_file_missing_lines_wording scoped -%}
2647
{% set separator = joiner(", ") %}
2748
Missing lines: {% for line in diff_file_coverage.violation_lines %}{{ separator() }}`{{ line }}`{% endfor %}
28-
{% endif %}
29-
30-
{% endfor %}
49+
{%- endblock single_file_missing_lines_wording %}
50+
{% endif -%}
51+
{%- endblock coverage_single_file -%}
52+
{%- endfor %}
3153
</details>
32-
{%- endif -%}
54+
{%- endif %}
55+
{%- endblock coverage_by_file %}
3356
{{ marker }}

coverage_comment/main.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def action(
5050
github_session: httpx.Client,
5151
http_session: httpx.Client,
5252
git: subprocess.Git,
53-
):
53+
) -> int:
5454
log.debug(f"Operating on {config.GITHUB_REF}")
5555

5656
event_name = config.GITHUB_EVENT_NAME
@@ -96,7 +96,7 @@ def generate_comment(
9696
github_session: httpx.Client,
9797
http_session: httpx.Client,
9898
git: subprocess.Git,
99-
):
99+
) -> int:
100100
log.info("Generating comment for PR")
101101

102102
diff_coverage = coverage_module.get_diff_coverage_info(
@@ -113,12 +113,30 @@ def generate_comment(
113113
if previous_coverage_data_file:
114114
previous_coverage = badge.parse_badge(contents=previous_coverage_data_file)
115115

116-
comment = template.get_markdown_comment(
117-
coverage=coverage,
118-
diff_coverage=diff_coverage,
119-
previous_coverage_rate=previous_coverage,
120-
template=template.read_template_file(),
121-
)
116+
try:
117+
comment = template.get_markdown_comment(
118+
coverage=coverage,
119+
diff_coverage=diff_coverage,
120+
previous_coverage_rate=previous_coverage,
121+
base_template=template.read_template_file(),
122+
custom_template=config.COMMENT_TEMPLATE,
123+
)
124+
except template.MissingMarker:
125+
log.error(
126+
"Marker not found. This error can happen if you defined a custom comment "
127+
"template that doesn't inherit the base template and you didn't include "
128+
"``{{ marker }}``. The marker is necessary for this action to recognize "
129+
"its own comment and avoid making new comments or overwriting someone else's "
130+
"comment."
131+
)
132+
return 1
133+
except template.TemplateError:
134+
log.exception(
135+
"There was a rendering error when computing the text of the comment to post "
136+
"on the PR. Please see the traceback, in particular if you're using a custom "
137+
"template."
138+
)
139+
return 1
122140

123141
gh = github_client.GitHub(session=github_session)
124142

@@ -152,7 +170,7 @@ def generate_comment(
152170
return 0
153171

154172

155-
def post_comment(config: settings.Config, github_session: httpx.Client):
173+
def post_comment(config: settings.Config, github_session: httpx.Client) -> int:
156174
log.info("Posting comment to PR")
157175

158176
if not config.GITHUB_PR_RUN_ID:
@@ -212,7 +230,7 @@ def save_badge(
212230
coverage: coverage_module.Coverage,
213231
github_session: httpx.Client,
214232
git: subprocess.Git,
215-
):
233+
) -> int:
216234
gh = github_client.GitHub(session=github_session)
217235
is_default_branch = github.is_default_branch(
218236
github=gh,

coverage_comment/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class Config:
2727
GITHUB_REF: str
2828
GITHUB_EVENT_NAME: str
2929
GITHUB_PR_RUN_ID: int | None
30+
COMMENT_TEMPLATE: str | None = None
3031
BADGE_FILENAME: pathlib.Path = pathlib.Path(
3132
"python-coverage-comment-action-badge.json"
3233
)

coverage_comment/template.py

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,63 @@
11
from importlib import resources
22

33
import jinja2
4+
from jinja2.sandbox import SandboxedEnvironment
45

56
from coverage_comment import coverage as coverage_module
67

78
MARKER = """<!-- This comment was produced by python-coverage-comment-action -->"""
89

910

11+
class CommentLoader(jinja2.BaseLoader):
12+
def __init__(self, base_template: str, custom_template: str | None):
13+
self.base_template = base_template
14+
self.custom_template = custom_template
15+
16+
def get_source(
17+
self, environment: jinja2.Environment, template: str
18+
) -> tuple[str, str | None, bool]:
19+
if template == "base":
20+
return self.base_template, None, True
21+
22+
if self.custom_template and template == "custom":
23+
return self.custom_template, None, True
24+
25+
raise jinja2.TemplateNotFound(template)
26+
27+
28+
class MissingMarker(Exception):
29+
pass
30+
31+
32+
class TemplateError(Exception):
33+
pass
34+
35+
1036
def get_markdown_comment(
1137
coverage: coverage_module.Coverage,
1238
diff_coverage: coverage_module.DiffCoverage,
1339
previous_coverage_rate: float | None,
14-
template: str,
40+
base_template: str,
41+
custom_template: str | None = None,
1542
):
16-
env = jinja2.Environment()
43+
loader = CommentLoader(base_template=base_template, custom_template=custom_template)
44+
env = SandboxedEnvironment(loader=loader)
1745
env.filters["pct"] = pct
1846

19-
return env.from_string(template).render(
20-
previous_coverage_rate=previous_coverage_rate,
21-
coverage=coverage,
22-
diff_coverage=diff_coverage,
23-
marker=MARKER,
24-
)
47+
try:
48+
comment = env.get_template("custom" if custom_template else "base").render(
49+
previous_coverage_rate=previous_coverage_rate,
50+
coverage=coverage,
51+
diff_coverage=diff_coverage,
52+
marker=MARKER,
53+
)
54+
except jinja2.exceptions.TemplateError as exc:
55+
raise TemplateError from exc
56+
57+
if MARKER not in comment:
58+
raise MissingMarker()
59+
60+
return comment
2561

2662

2763
def read_template_file() -> str:

tests/integration/test_main.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,44 @@ def checker(payload):
181181
assert capsys.readouterr().out.strip() == expected_stdout
182182

183183

184+
def test_action__pull_request__post_comment__no_marker(
185+
pull_request_config, session, in_integration_env, get_logs
186+
):
187+
# There is an existing badge in this test, allowing to test the coverage evolution
188+
session.register(
189+
"GET",
190+
"https://raw.githubusercontent.com/wiki/ewjoachim/foobar/python-coverage-comment-action-badge.json",
191+
)(status_code=404)
192+
193+
result = main.action(
194+
config=pull_request_config(COMMENT_TEMPLATE="""foo"""),
195+
github_session=session,
196+
http_session=session,
197+
git=None,
198+
)
199+
assert result == 1
200+
assert get_logs("ERROR", "Marker not found")
201+
202+
203+
def test_action__pull_request__post_comment__template_error(
204+
pull_request_config, session, in_integration_env, get_logs
205+
):
206+
# There is an existing badge in this test, allowing to test the coverage evolution
207+
session.register(
208+
"GET",
209+
"https://raw.githubusercontent.com/wiki/ewjoachim/foobar/python-coverage-comment-action-badge.json",
210+
)(status_code=404)
211+
212+
result = main.action(
213+
config=pull_request_config(COMMENT_TEMPLATE="""{%"""),
214+
github_session=session,
215+
http_session=session,
216+
git=None,
217+
)
218+
assert result == 1
219+
assert get_logs("ERROR", "There was a rendering error")
220+
221+
184222
def test_action__push__non_default_branch(
185223
push_config, session, in_integration_env, get_logs
186224
):

tests/unit/test_settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def test_config__from_environ__ok():
3333
"BADGE_FILENAME": "bar",
3434
"COMMENT_ARTIFACT_NAME": "baz",
3535
"COMMENT_FILENAME": "qux",
36+
"COMMENT_TEMPLATE": "footemplate",
3637
"MINIMUM_GREEN": "90",
3738
"MINIMUM_ORANGE": "50.8",
3839
"MERGE_COVERAGE_FILES": "true",
@@ -48,6 +49,7 @@ def test_config__from_environ__ok():
4849
BADGE_FILENAME=pathlib.Path("bar"),
4950
COMMENT_ARTIFACT_NAME="baz",
5051
COMMENT_FILENAME=pathlib.Path("qux"),
52+
COMMENT_TEMPLATE="footemplate",
5153
MINIMUM_GREEN=90.0,
5254
MINIMUM_ORANGE=50.8,
5355
MERGE_COVERAGE_FILES=True,

0 commit comments

Comments
 (0)