Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better checks for Changelog entries #77

Merged
merged 15 commits into from
Feb 20, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 0.7.5dev
* [Feature] Added `--version` option to `pkgmt setup` to specify Python version
* [Feature] Better checks for new entries added to CHANGELOG.md

## 0.7.4 (2023-09-08)

Expand Down
4 changes: 4 additions & 0 deletions src/pkgmt/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ def __init__(self, text, project_root=".") -> None:
def from_path(cls, path, project_root="."):
return cls(text=Path(path).read_text(), project_root=project_root)

def extract_text_from_entry(self):
"""Extract text from a single Changelog entry"""
return _extract_text_from_items(self.tree[0])

def sort_last_section(self):
"""Sorts last section depending on the prefix"""
self.check_latest_changelog_entries()
Expand Down
100 changes: 100 additions & 0 deletions src/pkgmt/fail_if_invalid_changelog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import subprocess
import argparse
import sys
from pathlib import Path

from pkgmt import changelog


def latest_changelog_header(base_branch):
changelog_contents = (
subprocess.check_output(f"git show {base_branch}:CHANGELOG.md", shell=True)
.decode("utf-8")
.split()
)
for line in changelog_contents:
if "dev" in line:
return line.strip()


def check_modified(base_branch, debug=False):
latest_section_main = latest_changelog_header(base_branch)
text = Path("CHANGELOG.md").read_text()
changelog_parser = changelog.CHANGELOG(text)
latest_section_current = changelog_parser.get_first_subheading()[1].strip()
if latest_section_main != latest_section_current:
print(
f"Latest section in CHANGELOG.md "
f"not up-to-date. Latest section in "
f"{base_branch}: {latest_section_main}"
)
return 1
cmd = f"git diff -U0 {base_branch}... -- CHANGELOG.md"
try:
out = subprocess.check_output(cmd, shell=True).decode("utf-8")
all_diff = []
for line in out.split("\n"):
if not (line.startswith("+++") or line.startswith("---")):
all_diff.append(line.strip())
git_removals = [
line[1:].strip()
for line in all_diff
if line.startswith("-")
if line[1:].strip() != ""
]
git_additions = [
line[1:].strip()
for line in all_diff
if line.startswith("+")
if line[1:].strip() != ""
]

if len(git_additions) == 0 or out == "" and debug:
print(f"CHANGELOG.md has not been modified with respect to '{base_branch}'")
return 1

if git_removals:
print(
f"These entries have been removed: "
f"{'; '.join(git_removals)}. Please revert the changes."
)
return 1

if git_additions:
latest_entries = changelog_parser.get_latest_changelog_section()
for line in git_additions:
if line:
try:
extracted_text = changelog.CHANGELOG(
line
).extract_text_from_entry()
except KeyError:
continue
if extracted_text[0] not in latest_entries:
print(
f"Entry '{line}' should be added "
f"to section {latest_section_main}"
)
return 1

elif extracted_text[0].strip() == "":
print(f"You have added an empty entry: {line}")
return 1

except subprocess.CalledProcessError:
pass
return 0


if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Check if proper modifications has been done to CHANGELOG.md"
)
parser.add_argument(
"-b", "--base-branch", default="main", help="Base branch to compare against"
)
parser.add_argument("--debug", action="store_true", help="Print debug info")

args = parser.parse_args()
return_code = check_modified(args.base_branch, debug=args.debug)
sys.exit(return_code)
24 changes: 24 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,27 @@ def tmp_package_modi_2(root, tmp_empty):
yield tmp_empty

os.chdir(old)


@pytest.fixture
def tmp_package_changelog(root, tmp_empty):
old = Path.cwd()
path_to_templates = root / "tests" / "assets" / "package_name"
shutil.copytree(str(path_to_templates), "copy")
os.chdir("copy")

subprocess.run(["git", "init"])
subprocess.check_call(["git", "config", "commit.gpgsign", "false"])
subprocess.check_call(["git", "config", "user.email", "ci@ploomberio"])
subprocess.check_call(["git", "config", "user.name", "Ploomber"])
subprocess.run(["git", "checkout", "-b", "main"])
with open("CHANGELOG.md", "a") as f:
f.write("\n")
subprocess.run(["git", "add", "--all"])
subprocess.run(["git", "commit", "-m", "init-commit-message"])

subprocess.run(["git", "checkout", "-b", "test_modified_changelog"])

yield tmp_empty

os.chdir(old)
6 changes: 0 additions & 6 deletions tests/test_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,6 @@ def test_check_if_broken(url, code, broken):
assert response.broken == broken


def test_check_if_broken_doesnt_accept_head_request():
response = links.LinkChecker().check_if_broken("https://binder.ploomber.io")
assert response.code == 405
assert not response.broken


@pytest.mark.parametrize(
"extensions, expected",
[
Expand Down
116 changes: 115 additions & 1 deletion tests/test_modified.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import pytest
from pkgmt import fail_if_modified, fail_if_not_modified
import subprocess

from pkgmt import fail_if_modified, fail_if_not_modified, fail_if_invalid_changelog
from pathlib import Path


Expand Down Expand Up @@ -81,3 +83,115 @@ def test_check_modified_in_2(tmp_package_modi_2, base_branch, include_path, retu
fail_if_not_modified.check_modified(base_branch, include_path, debug=True)
== returncode
)


# @pytest.mark.parametrize("contents", ["""
# # CHANGELOG
#
# ## 0.1dev
#
# * [Fix] Fixes #1
# * [Feature] Added feature 1
# """
# ])
def test_check_modified_changelog(tmp_package_changelog):
Path("CHANGELOG.md").write_text(
"""
# CHANGELOG

## 0.1dev

* [Fix] Fixes #1
* [Feature] Added feature 1
"""
)
subprocess.run(["git", "add", "CHANGELOG.md"])
subprocess.run(["git", "commit", "-m", "changelog_modified"])
assert fail_if_invalid_changelog.check_modified("main", debug=True) == 0


@pytest.mark.parametrize(
"contents, message",
[
(
"""
# CHANGELOG

## 0.1dev

* [Fix] Fixes #1


""",
"CHANGELOG.md has not been modified with respect to 'main'",
),
(
"""
# CHANGELOG

## 0.1dev

* [Fix] Fixes #1

""",
"CHANGELOG.md has not been modified with respect to 'main'",
),
(
"""
# CHANGELOG

## 0.0.1dev

* [Fix] Fixes #1

""",
"Latest section in CHANGELOG.md not up-to-date. "
"Latest section in main: 0.1dev",
),
(
"""
# CHANGELOG

## 0.1dev

* [Fix] Fixes #1

## 0.0.1

* [Feature] Some feature

""",
"Entry '* [Feature] Some feature' should be added to section 0.1dev",
),
(
"""
# CHANGELOG

## 0.1dev

* [Fix] Fixes #2

""",
"These entries have been removed: * [Fix] Fixes #1. "
"Please revert the changes.",
),
],
ids=["blanks", "no-change", "outdated-header", "incorrect-section", "removal"],
)
def test_check_modified_changelog_error(
tmp_package_changelog, contents, message, capsys
):
Path("CHANGELOG.md").write_text(contents)
subprocess.run(["git", "add", "CHANGELOG.md"])
subprocess.run(["git", "commit", "-m", "changelog_modified"])
assert fail_if_invalid_changelog.check_modified("main", debug=True) == 1
assert message in capsys.readouterr().out


def test_check_modified_changelog_error_file_added(tmp_package_changelog, capsys):
Path("some_file.txt").write_text("content")
subprocess.run(["git", "add", "some_file.txt"])
subprocess.run(["git", "commit", "-m", "added file"])
assert fail_if_invalid_changelog.check_modified("main", debug=True) == 1
out = capsys.readouterr().out
assert "CHANGELOG.md has not been modified with respect to 'main'" in out
Loading