We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[SonarCloud] Suppress warning `Python:S5852` or replace the affected RegEx strings with non-* equivalent
From [SonarCloud check](https://sonarcloud.io/project/security_hotspots?id=vacanza_python-holidays&branch=beta&resolved=false&inNewCodePeriod=true), for `dev` branch

> Make sure the regex used here, which is vulnerable to polynomial runtime due to backtracking, cannot lead to denial of service.

> Using slow regular expressions is security-sensitive[python:S5852](https://sonarcloud.io/organizations/vacanza/rules?open=python%3AS5852&rule_key=python%3AS5852)

All 3 affected instances are:
-  `pr = re.findall(r"^(.*) \(#\d+ .*\)$", pr)[0]` in [scripts/generate_release_notes.py](https://github.com/vacanza/python-holidays/blob/beta/scripts/generate_release_notes.py)
- `subdivision_group_re = re.compile(".*: (.*)")` and `subdivision_aliases_re = re.compile(r"(.*)\s\((.*?)\)")` in [test/test_docs.py](https://github.com/vacanza/python-holidays/blob/beta/tests/test_docs.py)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `scripts/generate_release_notes.py`
Content:
```
1 #!/usr/bin/env python3
2 
3 #  python-holidays
4 #  ---------------
5 #  A fast, efficient Python library for generating country, province and state
6 #  specific sets of holidays on the fly. It aims to make determining whether a
7 #  specific date is a holiday as fast and flexible as possible.
8 #
9 #  Authors: dr-prodigy <dr.prodigy.github@gmail.com> (c) 2017-2023
10 #           ryanss <ryanssdev@icloud.com> (c) 2014-2017
11 #  Website: https://github.com/dr-prodigy/python-holidays
12 #  License: MIT (see LICENSE file)
13 
14 import argparse
15 import re
16 import sys
17 from datetime import date
18 from pathlib import Path
19 from typing import Dict, Set
20 
21 from git import Repo
22 from github import Github
23 from github.GithubException import UnknownObjectException
24 
25 sys.path.append(f"{Path.cwd()}")
26 import holidays  # noqa: E402
27 
28 BRANCH_NAME = "dev"
29 HEADER_TEMPLATE = """
30 Version {version}
31 ============
32 
33 Released {month} {day}, {year}
34 """
35 IGNORED_CONTRIBUTORS = {"dependabot[bot]", "github-actions[bot]"}
36 REPOSITORY_NAME = "vacanza/python-holidays"
37 
38 
39 class ReleaseNotesGenerator:
40     """
41     Generates release notes based on local git commits and GitHub PRs metadata.
42 
43     Usage example: scripts/generate_release_notes.py
44     """
45 
46     def __init__(self) -> None:
47         arg_parser = argparse.ArgumentParser()
48         arg_parser.add_argument(
49             "-a",
50             "--author-only",
51             action="extend",
52             default=[],
53             help="Add only author as a contributor for this PR",
54             nargs="+",
55             type=int,
56         )
57         arg_parser.add_argument(
58             "-c",
59             "--cut-off-at",
60             help="Cut off at PR",
61             required=False,
62             type=int,
63         )
64         arg_parser.add_argument(
65             "-e",
66             "--exclude",
67             action="extend",
68             default=[],
69             help="Exclude this PR from the release notes",
70             nargs="+",
71             type=int,
72         )
73         arg_parser.add_argument(
74             "-v",
75             "--verbose",
76             action="store_true",
77             default=False,
78             help="Verbose output",
79         )
80         self.args = arg_parser.parse_args()
81 
82         self.local_repo = Repo(Path.cwd())
83         self.remote_repo = Github(self.github_token).get_repo(REPOSITORY_NAME)
84 
85         self.previous_commits: Set[str] = set()
86         self.pull_requests: Dict[int, str] = {}
87 
88         self.tag = holidays.__version__
89 
90         try:
91             latest_tag = self.remote_repo.get_tags()[0]
92             self.latest_tag_name = latest_tag.name
93             self.previous_commits.add(latest_tag.commit.sha)
94         except IndexError:
95             self.latest_tag_name = None
96 
97     @property
98     def github_token(self, path=Path(".github_token")):
99         """Return GitHub access token."""
100         return path.read_text(encoding="UTF-8").strip()
101 
102     @property
103     def is_ready(self):
104         """Perform environment checks and input validation."""
105         current_branch = str(self.local_repo.active_branch)
106         if current_branch != BRANCH_NAME:
107             exit(
108                 f"Switch to '{BRANCH_NAME}' first (currently in "
109                 f"'{current_branch}'). Use 'git switch {BRANCH_NAME}'."
110             )
111 
112         return True
113 
114     @property
115     def sorted_pull_requests(self):
116         def custom_order(pr):
117             pr = re.findall(r"^(.*) \(#\d+ .*\)$", pr)[0]
118 
119             if re.findall(r"^(Introduce|Refactor)", pr) or re.findall(r"Add .* support", pr):
120                 weight = 10
121             elif re.findall(r"^Add .* holidays$", pr):
122                 weight = 20
123             elif re.findall(r"(^Localize|localization$)", pr):
124                 weight = 30
125             elif re.findall(r"^Fix", pr):
126                 weight = 40
127             elif re.findall(r"^(Change|Improve|Optimize|Update|Upgrade)", pr):
128                 weight = 50
129             else:
130                 weight = 100
131 
132             return (weight, pr)
133 
134         return sorted(self.pull_requests.values(), key=custom_order)
135 
136     def add_pull_request(self, pull_request):
137         """Add pull request information to the release notes dict."""
138         author = pull_request.user.login if pull_request.user else None
139         if author in IGNORED_CONTRIBUTORS:
140             print((f"Skipping #{pull_request.number} {pull_request.title}" f" by {author}"))
141             return None
142 
143         # Skip failed release attempt PRs, version upgrades.
144         pr_title = pull_request.title
145         skip_titles = (f"v.{self.tag}", "Bump", "Revert")
146         for skip_title in skip_titles:
147             if pr_title.startswith(skip_title):
148                 return None
149 
150         # Get contributors (expand from commits by default).
151         contributors = set()
152         if pull_request.number not in self.args.author_only:
153             for commit in pull_request.get_commits():
154                 if commit.author:
155                     contributors.add(commit.author.login)
156 
157         if author in contributors:
158             contributors.remove(author)
159         contributors = (f"@{c}" for c in [author] + sorted(contributors, key=str.lower))
160         self.pull_requests[pull_request.number] = (
161             f"{pull_request.title} (#{pull_request.number} by " f"{', '.join(contributors)})"
162         )
163 
164     def generate_release_notes(self):
165         """Generate release notes contents."""
166         print("Processing pull requests...")
167         self.get_new_pull_requests()
168         self.get_old_pull_requests()
169         print("Done!")
170 
171     def get_new_pull_requests(self):
172         """Get PRs created after the latest release.
173 
174         This operation also populates a set of previous release commits.
175         """
176         cut_off_at = self.args.cut_off_at
177         excluded_pr_numbers = set(self.args.exclude)
178         for pull_request in self.remote_repo.get_pulls(state="closed"):
179             # Stop getting pull requests after previous release tag or specific PR number.
180             cut_off = cut_off_at and pull_request.number == cut_off_at
181             if cut_off or pull_request.title == self.latest_tag_name:
182                 # Get previous release commits SHAs.
183                 for commit in pull_request.get_commits():
184                     self.previous_commits.add(commit.sha)
185                 break
186 
187             # Skip closed unmerged PRs.
188             if not pull_request.merged:
189                 continue
190 
191             if pull_request.number in excluded_pr_numbers:
192                 if self.args.verbose:
193                     print(f"Excluding PR #{pull_request.number} as requested")
194                 continue
195 
196             if self.args.verbose:
197                 messages = [f"Fetching PR #{pull_request.number}"]
198                 if pull_request.number in self.args.author_only:
199                     messages.append("(keeping PR author as a sole contributor)")
200                 print(" ".join(messages))
201 
202             self.add_pull_request(pull_request)
203 
204     def get_old_pull_requests(self):
205         """Get PRs created before the latest release."""
206         pull_request_numbers = set()
207         for commit in self.local_repo.iter_commits():
208             if commit.hexsha in self.previous_commits:
209                 break
210 
211             try:
212                 pull_request_number = re.findall(
213                     r"#(\d{3,})",
214                     commit.message,
215                 )[0]
216                 pull_request_numbers.add(int(pull_request_number))
217             except IndexError:
218                 continue
219 
220         # Fetch old PRs metadata only. Skip all known PRs.
221         pull_request_numbers -= set(self.pull_requests.keys())
222         pull_request_numbers -= set(self.args.exclude)
223         for pull_request_number in pull_request_numbers:
224             if self.args.verbose:
225                 messages = [f"Fetching PR #{pull_request_number}"]
226                 if pull_request_number in self.args.author_only:
227                     messages.append("(keeping PR author as a sole contributor)")
228                 print(" ".join(messages))
229 
230             try:
231                 self.add_pull_request(self.remote_repo.get_pull(pull_request_number))
232             # 3rd party contributions to forks.
233             except UnknownObjectException:
234                 pass
235 
236     def print_release_notes(self):
237         """Print generated release notes."""
238         print("")
239         if self.pull_requests:
240             today = date.today()
241             print(
242                 HEADER_TEMPLATE.format(
243                     day=today.day,
244                     month=today.strftime("%B"),
245                     version=self.tag,
246                     year=today.year,
247                 )
248             )
249             print("\n".join((f"- {pr}" for pr in self.sorted_pull_requests)))
250 
251         else:
252             print(f"No changes since {self.latest_tag_name} release.")
253 
254 
255 if __name__ == "__main__":
256     rng = ReleaseNotesGenerator()
257     if rng.is_ready:
258         rng.generate_release_notes()
259         rng.print_release_notes()
260
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/scripts/generate_release_notes.py b/scripts/generate_release_notes.py
--- a/scripts/generate_release_notes.py
+++ b/scripts/generate_release_notes.py
@@ -114,22 +114,20 @@
     @property
     def sorted_pull_requests(self):
         def custom_order(pr):
-            pr = re.findall(r"^(.*) \(#\d+ .*\)$", pr)[0]
-
-            if re.findall(r"^(Introduce|Refactor)", pr) or re.findall(r"Add .* support", pr):
+            if re.findall(r"^(Introduce|Refactor)", pr[0]) or re.findall(r"Add .* support", pr[0]):
                 weight = 10
-            elif re.findall(r"^Add .* holidays$", pr):
+            elif re.findall(r"^Add .* holidays$", pr[0]):
                 weight = 20
-            elif re.findall(r"(^Localize|localization$)", pr):
+            elif re.findall(r"(^Localize)|(localization$)", pr[0]):
                 weight = 30
-            elif re.findall(r"^Fix", pr):
+            elif re.findall(r"^Fix", pr[0]):
                 weight = 40
-            elif re.findall(r"^(Change|Improve|Optimize|Update|Upgrade)", pr):
+            elif re.findall(r"^(Change|Improve|Optimize|Update|Upgrade)", pr[0]):
                 weight = 50
             else:
                 weight = 100
 
-            return (weight, pr)
+            return weight, pr
 
         return sorted(self.pull_requests.values(), key=custom_order)
 
@@ -158,7 +156,8 @@
             contributors.remove(author)
         contributors = (f"@{c}" for c in [author] + sorted(contributors, key=str.lower))
         self.pull_requests[pull_request.number] = (
-            f"{pull_request.title} (#{pull_request.number} by " f"{', '.join(contributors)})"
+            pull_request.title,
+            f"#{pull_request.number} by {', '.join(contributors)}",
         )
 
     def generate_release_notes(self):
@@ -246,7 +245,7 @@
                     year=today.year,
                 )
             )
-            print("\n".join((f"- {pr}" for pr in self.sorted_pull_requests)))
+            print("\n".join((f"- {pr[0]} ({pr[1]})" for pr in self.sorted_pull_requests)))
 
         else:
             print(f"No changes since {self.latest_tag_name} release.")

{"golden_diff": "diff --git a/scripts/generate_release_notes.py b/scripts/generate_release_notes.py\n--- a/scripts/generate_release_notes.py\n+++ b/scripts/generate_release_notes.py\n@@ -114,22 +114,20 @@\n     @property\n     def sorted_pull_requests(self):\n         def custom_order(pr):\n-            pr = re.findall(r\"^(.*) \\(#\\d+ .*\\)$\", pr)[0]\n-\n-            if re.findall(r\"^(Introduce|Refactor)\", pr) or re.findall(r\"Add .* support\", pr):\n+            if re.findall(r\"^(Introduce|Refactor)\", pr[0]) or re.findall(r\"Add .* support\", pr[0]):\n                 weight = 10\n-            elif re.findall(r\"^Add .* holidays$\", pr):\n+            elif re.findall(r\"^Add .* holidays$\", pr[0]):\n                 weight = 20\n-            elif re.findall(r\"(^Localize|localization$)\", pr):\n+            elif re.findall(r\"(^Localize)|(localization$)\", pr[0]):\n                 weight = 30\n-            elif re.findall(r\"^Fix\", pr):\n+            elif re.findall(r\"^Fix\", pr[0]):\n                 weight = 40\n-            elif re.findall(r\"^(Change|Improve|Optimize|Update|Upgrade)\", pr):\n+            elif re.findall(r\"^(Change|Improve|Optimize|Update|Upgrade)\", pr[0]):\n                 weight = 50\n             else:\n                 weight = 100\n \n-            return (weight, pr)\n+            return weight, pr\n \n         return sorted(self.pull_requests.values(), key=custom_order)\n \n@@ -158,7 +156,8 @@\n             contributors.remove(author)\n         contributors = (f\"@{c}\" for c in [author] + sorted(contributors, key=str.lower))\n         self.pull_requests[pull_request.number] = (\n-            f\"{pull_request.title} (#{pull_request.number} by \" f\"{', '.join(contributors)})\"\n+            pull_request.title,\n+            f\"#{pull_request.number} by {', '.join(contributors)}\",\n         )\n \n     def generate_release_notes(self):\n@@ -246,7 +245,7 @@\n                     year=today.year,\n                 )\n             )\n-            print(\"\\n\".join((f\"- {pr}\" for pr in self.sorted_pull_requests)))\n+            print(\"\\n\".join((f\"- {pr[0]} ({pr[1]})\" for pr in self.sorted_pull_requests)))\n \n         else:\n             print(f\"No changes since {self.latest_tag_name} release.\")\n", "issue": "[SonarCloud] Suppress warning `Python:S5852` or replace the affected RegEx strings with non-* equivalent\nFrom [SonarCloud check](https://sonarcloud.io/project/security_hotspots?id=vacanza_python-holidays&branch=beta&resolved=false&inNewCodePeriod=true), for `dev` branch\r\n\r\n> Make sure the regex used here, which is vulnerable to polynomial runtime due to backtracking, cannot lead to denial of service.\r\n\r\n> Using slow regular expressions is security-sensitive[python:S5852](https://sonarcloud.io/organizations/vacanza/rules?open=python%3AS5852&rule_key=python%3AS5852)\r\n\r\nAll 3 affected instances are:\r\n-  `pr = re.findall(r\"^(.*) \\(#\\d+ .*\\)$\", pr)[0]` in [scripts/generate_release_notes.py](https://github.com/vacanza/python-holidays/blob/beta/scripts/generate_release_notes.py)\r\n- `subdivision_group_re = re.compile(\".*: (.*)\")` and `subdivision_aliases_re = re.compile(r\"(.*)\\s\\((.*?)\\)\")` in [test/test_docs.py](https://github.com/vacanza/python-holidays/blob/beta/tests/test_docs.py)\n", "before_files": [{"content": "#!/usr/bin/env python3\n\n#  python-holidays\n#  ---------------\n#  A fast, efficient Python library for generating country, province and state\n#  specific sets of holidays on the fly. It aims to make determining whether a\n#  specific date is a holiday as fast and flexible as possible.\n#\n#  Authors: dr-prodigy <dr.prodigy.github@gmail.com> (c) 2017-2023\n#           ryanss <ryanssdev@icloud.com> (c) 2014-2017\n#  Website: https://github.com/dr-prodigy/python-holidays\n#  License: MIT (see LICENSE file)\n\nimport argparse\nimport re\nimport sys\nfrom datetime import date\nfrom pathlib import Path\nfrom typing import Dict, Set\n\nfrom git import Repo\nfrom github import Github\nfrom github.GithubException import UnknownObjectException\n\nsys.path.append(f\"{Path.cwd()}\")\nimport holidays  # noqa: E402\n\nBRANCH_NAME = \"dev\"\nHEADER_TEMPLATE = \"\"\"\nVersion {version}\n============\n\nReleased {month} {day}, {year}\n\"\"\"\nIGNORED_CONTRIBUTORS = {\"dependabot[bot]\", \"github-actions[bot]\"}\nREPOSITORY_NAME = \"vacanza/python-holidays\"\n\n\nclass ReleaseNotesGenerator:\n    \"\"\"\n    Generates release notes based on local git commits and GitHub PRs metadata.\n\n    Usage example: scripts/generate_release_notes.py\n    \"\"\"\n\n    def __init__(self) -> None:\n        arg_parser = argparse.ArgumentParser()\n        arg_parser.add_argument(\n            \"-a\",\n            \"--author-only\",\n            action=\"extend\",\n            default=[],\n            help=\"Add only author as a contributor for this PR\",\n            nargs=\"+\",\n            type=int,\n        )\n        arg_parser.add_argument(\n            \"-c\",\n            \"--cut-off-at\",\n            help=\"Cut off at PR\",\n            required=False,\n            type=int,\n        )\n        arg_parser.add_argument(\n            \"-e\",\n            \"--exclude\",\n            action=\"extend\",\n            default=[],\n            help=\"Exclude this PR from the release notes\",\n            nargs=\"+\",\n            type=int,\n        )\n        arg_parser.add_argument(\n            \"-v\",\n            \"--verbose\",\n            action=\"store_true\",\n            default=False,\n            help=\"Verbose output\",\n        )\n        self.args = arg_parser.parse_args()\n\n        self.local_repo = Repo(Path.cwd())\n        self.remote_repo = Github(self.github_token).get_repo(REPOSITORY_NAME)\n\n        self.previous_commits: Set[str] = set()\n        self.pull_requests: Dict[int, str] = {}\n\n        self.tag = holidays.__version__\n\n        try:\n            latest_tag = self.remote_repo.get_tags()[0]\n            self.latest_tag_name = latest_tag.name\n            self.previous_commits.add(latest_tag.commit.sha)\n        except IndexError:\n            self.latest_tag_name = None\n\n    @property\n    def github_token(self, path=Path(\".github_token\")):\n        \"\"\"Return GitHub access token.\"\"\"\n        return path.read_text(encoding=\"UTF-8\").strip()\n\n    @property\n    def is_ready(self):\n        \"\"\"Perform environment checks and input validation.\"\"\"\n        current_branch = str(self.local_repo.active_branch)\n        if current_branch != BRANCH_NAME:\n            exit(\n                f\"Switch to '{BRANCH_NAME}' first (currently in \"\n                f\"'{current_branch}'). Use 'git switch {BRANCH_NAME}'.\"\n            )\n\n        return True\n\n    @property\n    def sorted_pull_requests(self):\n        def custom_order(pr):\n            pr = re.findall(r\"^(.*) \\(#\\d+ .*\\)$\", pr)[0]\n\n            if re.findall(r\"^(Introduce|Refactor)\", pr) or re.findall(r\"Add .* support\", pr):\n                weight = 10\n            elif re.findall(r\"^Add .* holidays$\", pr):\n                weight = 20\n            elif re.findall(r\"(^Localize|localization$)\", pr):\n                weight = 30\n            elif re.findall(r\"^Fix\", pr):\n                weight = 40\n            elif re.findall(r\"^(Change|Improve|Optimize|Update|Upgrade)\", pr):\n                weight = 50\n            else:\n                weight = 100\n\n            return (weight, pr)\n\n        return sorted(self.pull_requests.values(), key=custom_order)\n\n    def add_pull_request(self, pull_request):\n        \"\"\"Add pull request information to the release notes dict.\"\"\"\n        author = pull_request.user.login if pull_request.user else None\n        if author in IGNORED_CONTRIBUTORS:\n            print((f\"Skipping #{pull_request.number} {pull_request.title}\" f\" by {author}\"))\n            return None\n\n        # Skip failed release attempt PRs, version upgrades.\n        pr_title = pull_request.title\n        skip_titles = (f\"v.{self.tag}\", \"Bump\", \"Revert\")\n        for skip_title in skip_titles:\n            if pr_title.startswith(skip_title):\n                return None\n\n        # Get contributors (expand from commits by default).\n        contributors = set()\n        if pull_request.number not in self.args.author_only:\n            for commit in pull_request.get_commits():\n                if commit.author:\n                    contributors.add(commit.author.login)\n\n        if author in contributors:\n            contributors.remove(author)\n        contributors = (f\"@{c}\" for c in [author] + sorted(contributors, key=str.lower))\n        self.pull_requests[pull_request.number] = (\n            f\"{pull_request.title} (#{pull_request.number} by \" f\"{', '.join(contributors)})\"\n        )\n\n    def generate_release_notes(self):\n        \"\"\"Generate release notes contents.\"\"\"\n        print(\"Processing pull requests...\")\n        self.get_new_pull_requests()\n        self.get_old_pull_requests()\n        print(\"Done!\")\n\n    def get_new_pull_requests(self):\n        \"\"\"Get PRs created after the latest release.\n\n        This operation also populates a set of previous release commits.\n        \"\"\"\n        cut_off_at = self.args.cut_off_at\n        excluded_pr_numbers = set(self.args.exclude)\n        for pull_request in self.remote_repo.get_pulls(state=\"closed\"):\n            # Stop getting pull requests after previous release tag or specific PR number.\n            cut_off = cut_off_at and pull_request.number == cut_off_at\n            if cut_off or pull_request.title == self.latest_tag_name:\n                # Get previous release commits SHAs.\n                for commit in pull_request.get_commits():\n                    self.previous_commits.add(commit.sha)\n                break\n\n            # Skip closed unmerged PRs.\n            if not pull_request.merged:\n                continue\n\n            if pull_request.number in excluded_pr_numbers:\n                if self.args.verbose:\n                    print(f\"Excluding PR #{pull_request.number} as requested\")\n                continue\n\n            if self.args.verbose:\n                messages = [f\"Fetching PR #{pull_request.number}\"]\n                if pull_request.number in self.args.author_only:\n                    messages.append(\"(keeping PR author as a sole contributor)\")\n                print(\" \".join(messages))\n\n            self.add_pull_request(pull_request)\n\n    def get_old_pull_requests(self):\n        \"\"\"Get PRs created before the latest release.\"\"\"\n        pull_request_numbers = set()\n        for commit in self.local_repo.iter_commits():\n            if commit.hexsha in self.previous_commits:\n                break\n\n            try:\n                pull_request_number = re.findall(\n                    r\"#(\\d{3,})\",\n                    commit.message,\n                )[0]\n                pull_request_numbers.add(int(pull_request_number))\n            except IndexError:\n                continue\n\n        # Fetch old PRs metadata only. Skip all known PRs.\n        pull_request_numbers -= set(self.pull_requests.keys())\n        pull_request_numbers -= set(self.args.exclude)\n        for pull_request_number in pull_request_numbers:\n            if self.args.verbose:\n                messages = [f\"Fetching PR #{pull_request_number}\"]\n                if pull_request_number in self.args.author_only:\n                    messages.append(\"(keeping PR author as a sole contributor)\")\n                print(\" \".join(messages))\n\n            try:\n                self.add_pull_request(self.remote_repo.get_pull(pull_request_number))\n            # 3rd party contributions to forks.\n            except UnknownObjectException:\n                pass\n\n    def print_release_notes(self):\n        \"\"\"Print generated release notes.\"\"\"\n        print(\"\")\n        if self.pull_requests:\n            today = date.today()\n            print(\n                HEADER_TEMPLATE.format(\n                    day=today.day,\n                    month=today.strftime(\"%B\"),\n                    version=self.tag,\n                    year=today.year,\n                )\n            )\n            print(\"\\n\".join((f\"- {pr}\" for pr in self.sorted_pull_requests)))\n\n        else:\n            print(f\"No changes since {self.latest_tag_name} release.\")\n\n\nif __name__ == \"__main__\":\n    rng = ReleaseNotesGenerator()\n    if rng.is_ready:\n        rng.generate_release_notes()\n        rng.print_release_notes()\n", "path": "scripts/generate_release_notes.py"}], "after_files": [{"content": "#!/usr/bin/env python3\n\n#  python-holidays\n#  ---------------\n#  A fast, efficient Python library for generating country, province and state\n#  specific sets of holidays on the fly. It aims to make determining whether a\n#  specific date is a holiday as fast and flexible as possible.\n#\n#  Authors: dr-prodigy <dr.prodigy.github@gmail.com> (c) 2017-2023\n#           ryanss <ryanssdev@icloud.com> (c) 2014-2017\n#  Website: https://github.com/dr-prodigy/python-holidays\n#  License: MIT (see LICENSE file)\n\nimport argparse\nimport re\nimport sys\nfrom datetime import date\nfrom pathlib import Path\nfrom typing import Dict, Set\n\nfrom git import Repo\nfrom github import Github\nfrom github.GithubException import UnknownObjectException\n\nsys.path.append(f\"{Path.cwd()}\")\nimport holidays  # noqa: E402\n\nBRANCH_NAME = \"dev\"\nHEADER_TEMPLATE = \"\"\"\nVersion {version}\n============\n\nReleased {month} {day}, {year}\n\"\"\"\nIGNORED_CONTRIBUTORS = {\"dependabot[bot]\", \"github-actions[bot]\"}\nREPOSITORY_NAME = \"vacanza/python-holidays\"\n\n\nclass ReleaseNotesGenerator:\n    \"\"\"\n    Generates release notes based on local git commits and GitHub PRs metadata.\n\n    Usage example: scripts/generate_release_notes.py\n    \"\"\"\n\n    def __init__(self) -> None:\n        arg_parser = argparse.ArgumentParser()\n        arg_parser.add_argument(\n            \"-a\",\n            \"--author-only\",\n            action=\"extend\",\n            default=[],\n            help=\"Add only author as a contributor for this PR\",\n            nargs=\"+\",\n            type=int,\n        )\n        arg_parser.add_argument(\n            \"-c\",\n            \"--cut-off-at\",\n            help=\"Cut off at PR\",\n            required=False,\n            type=int,\n        )\n        arg_parser.add_argument(\n            \"-e\",\n            \"--exclude\",\n            action=\"extend\",\n            default=[],\n            help=\"Exclude this PR from the release notes\",\n            nargs=\"+\",\n            type=int,\n        )\n        arg_parser.add_argument(\n            \"-v\",\n            \"--verbose\",\n            action=\"store_true\",\n            default=False,\n            help=\"Verbose output\",\n        )\n        self.args = arg_parser.parse_args()\n\n        self.local_repo = Repo(Path.cwd())\n        self.remote_repo = Github(self.github_token).get_repo(REPOSITORY_NAME)\n\n        self.previous_commits: Set[str] = set()\n        self.pull_requests: Dict[int, str] = {}\n\n        self.tag = holidays.__version__\n\n        try:\n            latest_tag = self.remote_repo.get_tags()[0]\n            self.latest_tag_name = latest_tag.name\n            self.previous_commits.add(latest_tag.commit.sha)\n        except IndexError:\n            self.latest_tag_name = None\n\n    @property\n    def github_token(self, path=Path(\".github_token\")):\n        \"\"\"Return GitHub access token.\"\"\"\n        return path.read_text(encoding=\"UTF-8\").strip()\n\n    @property\n    def is_ready(self):\n        \"\"\"Perform environment checks and input validation.\"\"\"\n        current_branch = str(self.local_repo.active_branch)\n        if current_branch != BRANCH_NAME:\n            exit(\n                f\"Switch to '{BRANCH_NAME}' first (currently in \"\n                f\"'{current_branch}'). Use 'git switch {BRANCH_NAME}'.\"\n            )\n\n        return True\n\n    @property\n    def sorted_pull_requests(self):\n        def custom_order(pr):\n            if re.findall(r\"^(Introduce|Refactor)\", pr[0]) or re.findall(r\"Add .* support\", pr[0]):\n                weight = 10\n            elif re.findall(r\"^Add .* holidays$\", pr[0]):\n                weight = 20\n            elif re.findall(r\"(^Localize)|(localization$)\", pr[0]):\n                weight = 30\n            elif re.findall(r\"^Fix\", pr[0]):\n                weight = 40\n            elif re.findall(r\"^(Change|Improve|Optimize|Update|Upgrade)\", pr[0]):\n                weight = 50\n            else:\n                weight = 100\n\n            return weight, pr\n\n        return sorted(self.pull_requests.values(), key=custom_order)\n\n    def add_pull_request(self, pull_request):\n        \"\"\"Add pull request information to the release notes dict.\"\"\"\n        author = pull_request.user.login if pull_request.user else None\n        if author in IGNORED_CONTRIBUTORS:\n            print((f\"Skipping #{pull_request.number} {pull_request.title}\" f\" by {author}\"))\n            return None\n\n        # Skip failed release attempt PRs, version upgrades.\n        pr_title = pull_request.title\n        skip_titles = (f\"v.{self.tag}\", \"Bump\", \"Revert\")\n        for skip_title in skip_titles:\n            if pr_title.startswith(skip_title):\n                return None\n\n        # Get contributors (expand from commits by default).\n        contributors = set()\n        if pull_request.number not in self.args.author_only:\n            for commit in pull_request.get_commits():\n                if commit.author:\n                    contributors.add(commit.author.login)\n\n        if author in contributors:\n            contributors.remove(author)\n        contributors = (f\"@{c}\" for c in [author] + sorted(contributors, key=str.lower))\n        self.pull_requests[pull_request.number] = (\n            pull_request.title,\n            f\"#{pull_request.number} by {', '.join(contributors)}\",\n        )\n\n    def generate_release_notes(self):\n        \"\"\"Generate release notes contents.\"\"\"\n        print(\"Processing pull requests...\")\n        self.get_new_pull_requests()\n        self.get_old_pull_requests()\n        print(\"Done!\")\n\n    def get_new_pull_requests(self):\n        \"\"\"Get PRs created after the latest release.\n\n        This operation also populates a set of previous release commits.\n        \"\"\"\n        cut_off_at = self.args.cut_off_at\n        excluded_pr_numbers = set(self.args.exclude)\n        for pull_request in self.remote_repo.get_pulls(state=\"closed\"):\n            # Stop getting pull requests after previous release tag or specific PR number.\n            cut_off = cut_off_at and pull_request.number == cut_off_at\n            if cut_off or pull_request.title == self.latest_tag_name:\n                # Get previous release commits SHAs.\n                for commit in pull_request.get_commits():\n                    self.previous_commits.add(commit.sha)\n                break\n\n            # Skip closed unmerged PRs.\n            if not pull_request.merged:\n                continue\n\n            if pull_request.number in excluded_pr_numbers:\n                if self.args.verbose:\n                    print(f\"Excluding PR #{pull_request.number} as requested\")\n                continue\n\n            if self.args.verbose:\n                messages = [f\"Fetching PR #{pull_request.number}\"]\n                if pull_request.number in self.args.author_only:\n                    messages.append(\"(keeping PR author as a sole contributor)\")\n                print(\" \".join(messages))\n\n            self.add_pull_request(pull_request)\n\n    def get_old_pull_requests(self):\n        \"\"\"Get PRs created before the latest release.\"\"\"\n        pull_request_numbers = set()\n        for commit in self.local_repo.iter_commits():\n            if commit.hexsha in self.previous_commits:\n                break\n\n            try:\n                pull_request_number = re.findall(\n                    r\"#(\\d{3,})\",\n                    commit.message,\n                )[0]\n                pull_request_numbers.add(int(pull_request_number))\n            except IndexError:\n                continue\n\n        # Fetch old PRs metadata only. Skip all known PRs.\n        pull_request_numbers -= set(self.pull_requests.keys())\n        pull_request_numbers -= set(self.args.exclude)\n        for pull_request_number in pull_request_numbers:\n            if self.args.verbose:\n                messages = [f\"Fetching PR #{pull_request_number}\"]\n                if pull_request_number in self.args.author_only:\n                    messages.append(\"(keeping PR author as a sole contributor)\")\n                print(\" \".join(messages))\n\n            try:\n                self.add_pull_request(self.remote_repo.get_pull(pull_request_number))\n            # 3rd party contributions to forks.\n            except UnknownObjectException:\n                pass\n\n    def print_release_notes(self):\n        \"\"\"Print generated release notes.\"\"\"\n        print(\"\")\n        if self.pull_requests:\n            today = date.today()\n            print(\n                HEADER_TEMPLATE.format(\n                    day=today.day,\n                    month=today.strftime(\"%B\"),\n                    version=self.tag,\n                    year=today.year,\n                )\n            )\n            print(\"\\n\".join((f\"- {pr[0]} ({pr[1]})\" for pr in self.sorted_pull_requests)))\n\n        else:\n            print(f\"No changes since {self.latest_tag_name} release.\")\n\n\nif __name__ == \"__main__\":\n    rng = ReleaseNotesGenerator()\n    if rng.is_ready:\n        rng.generate_release_notes()\n        rng.print_release_notes()\n", "path": "scripts/generate_release_notes.py"}]}