Skip to content

Commit 54e54ad

Browse files
committed
Add pre-commit hook to verify format of news fragment
1 parent 15858cb commit 54e54ad

File tree

2 files changed

+91
-0
lines changed

2 files changed

+91
-0
lines changed

.pre-commit-config.yaml

+7
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ repos:
8585
language: system
8686
types: [text]
8787
files: ^(doc/whatsnew/\d+\.\d+\.rst)
88+
- id: check-newsfragments
89+
name: Check newsfragments
90+
entry: python3 -m script.check_newsfragments
91+
language: system
92+
types: [text]
93+
files: ^(doc/whatsnew/fragments)
94+
exclude: doc/whatsnew/fragments/_.*.rst
8895
- repo: https://github.com/rstcheck/rstcheck
8996
rev: "v6.0.0.post1"
9097
hooks:

script/check_newsfragments.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
2+
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
3+
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
4+
5+
"""Small script to check the formatting of news fragments for towncrier.
6+
Used by pre-commit.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import argparse
12+
import re
13+
import sys
14+
from pathlib import Path
15+
from re import Pattern
16+
17+
VALID_ISSUES_KEYWORDS = [
18+
"Refs",
19+
"Closes",
20+
"Follow-up in",
21+
"Fixes part of",
22+
]
23+
ISSUES_KEYWORDS = "|".join(VALID_ISSUES_KEYWORDS)
24+
VALID_CHANGELOG_PATTERN = rf"(?P<description>(.*\n)*(.*\.\n))\n(?P<ref>({ISSUES_KEYWORDS}) (PyCQA/astroid)?#(?P<issue>\d+))"
25+
VALID_CHANGELOG_COMPILED_PATTERN: Pattern[str] = re.compile(
26+
VALID_CHANGELOG_PATTERN, flags=re.MULTILINE
27+
)
28+
29+
30+
def main(argv: list[str] | None = None) -> int:
31+
parser = argparse.ArgumentParser()
32+
parser.add_argument(
33+
"filenames",
34+
nargs="*",
35+
metavar="FILES",
36+
help="File names to check",
37+
)
38+
parser.add_argument("--verbose", "-v", action="count", default=0)
39+
args = parser.parse_args(argv)
40+
is_valid = True
41+
for filename in args.filenames:
42+
is_valid &= check_file(Path(filename), args.verbose)
43+
return 0 if is_valid else 1
44+
45+
46+
def check_file(file: Path, verbose: bool) -> bool:
47+
"""Check that a file contains a valid changelog entry."""
48+
with open(file, encoding="utf8") as f:
49+
content = f.read()
50+
match = VALID_CHANGELOG_COMPILED_PATTERN.match(content)
51+
if match:
52+
issue = match.group("issue")
53+
if file.stem != issue:
54+
print(
55+
f"{file} must be named '{issue}.<fragmenttype>', after the issue it references."
56+
)
57+
return False
58+
if verbose:
59+
print(f"Checked '{file}': LGTM 🤖👍")
60+
return True
61+
print(
62+
f"""\
63+
{file}: does not respect the standard format 🤖👎
64+
65+
The standard format is:
66+
67+
<one or more line of text>
68+
<one blank line>
69+
<issue reference> #<issuenumber>
70+
71+
Where <issue reference> can be one of: {', '.join(VALID_ISSUES_KEYWORDS)}
72+
73+
For example:
74+
75+
``pylint.x.y`` is now a private API.
76+
77+
Refs #1234
78+
"""
79+
)
80+
return False
81+
82+
83+
if __name__ == "__main__":
84+
sys.exit(main())

0 commit comments

Comments
 (0)