Repository: sktime/sktime

Issue:
[ENH] improvements to changelog generator
@lmmentel, THANK YOU for your excellent changelog generator script!

Collecting some improvement suggestions:

* in PR names, single-quotes should be converted to double-quotes, since the rst code environment has double-quotes, and the markdown code environment has single-qupte


Discussion:
Could you give an example where the quotes should be added?
it's hard to give an examples directly since quotes are reserved symbols within markdown.

So here is an indirect example: suppose this PR ends up in a change log:
https://github.com/alan-turing-institute/sktime/pull/2597
then we need to change the quotes in its title to double-quotes.

Resolving patch:
diff --git a/build_tools/changelog.py b/build_tools/changelog.py
--- a/build_tools/changelog.py
+++ b/build_tools/changelog.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-
 """RestructuredText changelog generator."""
 
 import os
@@ -18,24 +17,41 @@
 
 OWNER = "alan-turing-institute"
 REPO = "sktime"
+GITHUB_REPOS = "https://api.github.com/repos"
 
 
 def fetch_merged_pull_requests(page: int = 1) -> List[Dict]:  # noqa
     "Fetch a page of pull requests"
-    params = {"state": "closed", "page": page, "per_page": 50}
+    params = {
+        "state": "closed",
+        "page": page,
+        "per_page": 50,
+        "sort": "updated",
+        "direction": "desc",
+    }
     r = httpx.get(
-        f"https://api.github.com/repos/{OWNER}/{REPO}/pulls",
+        f"{GITHUB_REPOS}/{OWNER}/{REPO}/pulls",
         headers=HEADERS,
         params=params,
     )
     return [pr for pr in r.json() if pr["merged_at"]]
 
 
+def fetch_latest_release():  # noqa
+    response = httpx.get(
+        f"{GITHUB_REPOS}/{OWNER}/{REPO}/releases/latest", headers=HEADERS
+    )
+
+    if response.status_code == 200:
+        return response.json()
+    else:
+        raise ValueError(response.text, response.status_code)
+
+
 def fetch_pull_requests_since_last_release() -> List[Dict]:  # noqa
+    "Fetch pull requests and filter based on merged date"
 
-    release = httpx.get(
-        f"https://api.github.com/repos/{OWNER}/{REPO}/releases/latest", headers=HEADERS
-    ).json()
+    release = fetch_latest_release()
     published_at = parser.parse(release["published_at"])
     print(  # noqa
         f"Latest release {release['tag_name']} was published at {published_at}"
@@ -54,9 +70,20 @@ def fetch_pull_requests_since_last_release() -> List[Dict]:  # noqa
     return all_pulls
 
 
+def github_compare_tags(tag_left: str, tag_right: str = "HEAD"):  # noqa
+    "Compare commit between two tags"
+    response = httpx.get(
+        f"{GITHUB_REPOS}/{OWNER}/{REPO}/compare/{tag_left}...{tag_right}"
+    )
+    if response.status_code == 200:
+        return response.json()
+    else:
+        raise ValueError(response.text, response.status_code)
+
+
 def render_contributors(prs: List, fmt: str = "rst"):  # noqa
     "Find unique authors and print a list in  given format"
-    authors = sorted(set(pr["user"]["login"] for pr in prs), key=lambda x: x.lower())
+    authors = sorted({pr["user"]["login"] for pr in prs}, key=lambda x: x.lower())
 
     header = "Contributors"
     if fmt == "github":
@@ -68,8 +95,8 @@ def render_contributors(prs: List, fmt: str = "rst"):  # noqa
         print(",\n".join(f":user:`{user}`" for user in authors))  # noqa
 
 
-def assign_prs(prs, categs):  # noqa
-
+def assign_prs(prs, categs: List[Dict[str, List[str]]]):  # noqa
+    "Assign PR to categories based on labels"
     assigned = defaultdict(list)
 
     for i, pr in enumerate(prs):
@@ -82,19 +109,24 @@ def assign_prs(prs, categs):  # noqa
     #                 print(i, pr_labels)
 
     assigned["Other"] = list(
-        set(range(len(prs))) - set([i for _, l in assigned.items() for i in l])
+        set(range(len(prs))) - {i for _, l in assigned.items() for i in l}
     )
+
     return assigned
 
 
 def render_row(pr):  # noqa
     "Render a single row with PR in restructuredText format"
     print(  # noqa
-        "*", pr["title"], f"(:pr:`{pr['number']}`)", f":user:`{pr['user']['login']}`"
+        "*",
+        pr["title"].replace("`", "``"),
+        f"(:pr:`{pr['number']}`)",
+        f":user:`{pr['user']['login']}`",
     )
 
 
 def render_changelog(prs, assigned):  # noqa
+    # sourcery skip: use-named-expression
     "Render changelog"
     for title, _ in assigned.items():
         pr_group = [prs[i] for i in assigned[title]]
@@ -122,3 +154,12 @@ def render_changelog(prs, assigned):  # noqa
     render_changelog(pulls, assigned)
     print()  # noqa
     render_contributors(pulls)
+
+    release = fetch_latest_release()
+    diff = github_compare_tags(release["tag_name"])
+    if diff["total_commits"] != len(pulls):
+        raise ValueError(
+            "Something went wrong and not all PR were fetched. "
+            f'There is {len(pulls)} PRs but {diff["total_commits"]} in the diff'
+            "Please verify that all PRs are included in the changelog."
+        )  # noqa


Test patch: