From c645011c914e956e3516b623910bd84ce9342dc4 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Sun, 19 Nov 2023 17:54:20 -0800 Subject: [PATCH 01/75] Draft release functions --- scripts/release.py | 87 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 scripts/release.py diff --git a/scripts/release.py b/scripts/release.py new file mode 100644 index 00000000..d182c178 --- /dev/null +++ b/scripts/release.py @@ -0,0 +1,87 @@ +import subprocess +import sys +from github import Github +from getpass import getpass + +def add_git_tag_for_version(version: str) -> None: + """Add a git tag for the given version.""" + try: + subprocess.run(["git", "tag", "-a", version, "-m", version], check=True) + # TODO: Remove this print statement after testing + print(f"Version {version} tag added successfully.") + except subprocess.CalledProcessError as error: + print(f"Failed to add version tag: {error}") + sys.exit(1) + + +def clean_repo() -> None: + """Clean the repo.""" + try: + subprocess.run(["git", "clean", "-fdx"], check=True) + # TODO: Remove this print statement after testing + print("Repo cleaned successfully.") + except subprocess.CalledProcessError as error: + print(f"Failed to clean repo: {error}") + sys.exit(1) + + +def create_build() -> None: + """Create a build.""" + try: + subprocess.run(["python", "-m", "build"], check=True) + # TODO: Remove this print statement after testing + print("Build created successfully.") + except subprocess.CalledProcessError as error: + print(f"Failed to create build: {error}") + sys.exit(1) + +def verify_build() -> None: + """Verify the build.""" + try: + subprocess.run(["ls", "-l", "dist/"], check=True) + # TODO: Remove this print statement after testing + print("Build verified successfully.") + except subprocess.CalledProcessError as error: + print(f"Failed to verify build: {error}") + sys.exit(1) + +def get_github_client() -> Github: + """Get a Github client.""" + + + +def upload_build_to_pypi() -> None: + """Upload the build to PyPI.""" + # TODO: This needs to be updated to use the token and password from env variables. + try: + subprocess.run(["twine", "upload", "dist/*"], check=True) + # TODO: Remove this print statement after testing + print("Build uploaded successfully.") + except subprocess.CalledProcessError as error: + print(f"Failed to upload build: {error}") + sys.exit(1) + +def push_git_tags() -> None: + """Push all git tags to the remote.""" + try: + subprocess.run(["git", "push", "--tags", "origin", "master"], check=True) + # TODO: Remove this print statement after testing + print("Tags pushed successfully.") + except subprocess.CalledProcessError as error: + print(f"Failed to push tags: {error}") + sys.exit(1) + +""" +pyPyToken = input("Enter the PyPI token: ") +pyPyPassword = getpass("Enter the PyPI password: ") +githubToken = getpass("Enter the github token: ") +githubPassword = getpass("Enter the github password: ") +""" +version_number = input("Enter the version number: ") + +def main() -> None: + """Run the main program.""" + add_git_tag_for_version(version_number) + clean_repo() + create_build() + verify_build() From 9803abc3e421385924cd0d00c07c41dc75c0db3e Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Sun, 19 Nov 2023 18:50:53 -0800 Subject: [PATCH 02/75] Add build verification part b --- scripts/release.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index d182c178..171ab663 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -44,6 +44,14 @@ def verify_build() -> None: except subprocess.CalledProcessError as error: print(f"Failed to verify build: {error}") sys.exit(1) + try: + subprocess.run(["parallel", "-j", "1", "-t", "tar", "-tvf", ":::", "/dist*"], check=True) + # TODO: Remove this print statement after testing + print("Build verified successfully.") + except subprocess.CalledProcessError as error: + print(f"Failed to verify build: {error}") + sys.exit(1) + def get_github_client() -> Github: """Get a Github client.""" From 89bd0fad4a719dd7a57dfb3a26a997009fcc5cff Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Fri, 24 Nov 2023 13:44:08 -0800 Subject: [PATCH 03/75] Add pause for manual build verification --- scripts/release.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 171ab663..87604866 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -39,15 +39,29 @@ def verify_build() -> None: """Verify the build.""" try: subprocess.run(["ls", "-l", "dist/"], check=True) - # TODO: Remove this print statement after testing - print("Build verified successfully.") + confirmation = input("Does the build look correct? (y/n): ") + if confirmation == "y": + # TODO: Remove this print statement after testing + print("Build verified successfully.") + upload_build_to_pypi() + push_git_tags() + else: + print("Build not uploaded.") + sys.exit(1) except subprocess.CalledProcessError as error: print(f"Failed to verify build: {error}") sys.exit(1) try: subprocess.run(["parallel", "-j", "1", "-t", "tar", "-tvf", ":::", "/dist*"], check=True) - # TODO: Remove this print statement after testing - print("Build verified successfully.") + confirmation = input("Does the build look correct? (y/n): ") + if confirmation == "y": + # TODO: Remove this print statement after testing + print("Build verified successfully.") + upload_build_to_pypi() + push_git_tags() + else: + print("Build not uploaded.") + sys.exit(1) except subprocess.CalledProcessError as error: print(f"Failed to verify build: {error}") sys.exit(1) From c615013e90e44f7fb9ca0e11063f38d97127bf54 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 30 Jan 2024 18:35:39 -0500 Subject: [PATCH 04/75] Bring in tested release script from the other repo --- scripts/release.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) mode change 100644 => 100755 scripts/release.py diff --git a/scripts/release.py b/scripts/release.py old mode 100644 new mode 100755 index 87604866..19748fb1 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,25 +1,23 @@ import subprocess import sys -from github import Github +# from github import Github from getpass import getpass def add_git_tag_for_version(version: str) -> None: """Add a git tag for the given version.""" try: subprocess.run(["git", "tag", "-a", version, "-m", version], check=True) - # TODO: Remove this print statement after testing print(f"Version {version} tag added successfully.") except subprocess.CalledProcessError as error: print(f"Failed to add version tag: {error}") sys.exit(1) -def clean_repo() -> None: - """Clean the repo.""" +def remove_previous_dist() -> None: + """Check for dist folder, and if it exists, remove it.""" try: - subprocess.run(["git", "clean", "-fdx"], check=True) - # TODO: Remove this print statement after testing - print("Repo cleaned successfully.") + subprocess.run(["rm", "-rf", "dist/"], check=True) + print("Dist folder sucessfully removed.") except subprocess.CalledProcessError as error: print(f"Failed to clean repo: {error}") sys.exit(1) @@ -29,19 +27,18 @@ def create_build() -> None: """Create a build.""" try: subprocess.run(["python", "-m", "build"], check=True) - # TODO: Remove this print statement after testing print("Build created successfully.") except subprocess.CalledProcessError as error: print(f"Failed to create build: {error}") sys.exit(1) + def verify_build() -> None: """Verify the build.""" try: subprocess.run(["ls", "-l", "dist/"], check=True) confirmation = input("Does the build look correct? (y/n): ") if confirmation == "y": - # TODO: Remove this print statement after testing print("Build verified successfully.") upload_build_to_pypi() push_git_tags() @@ -55,7 +52,6 @@ def verify_build() -> None: subprocess.run(["parallel", "-j", "1", "-t", "tar", "-tvf", ":::", "/dist*"], check=True) confirmation = input("Does the build look correct? (y/n): ") if confirmation == "y": - # TODO: Remove this print statement after testing print("Build verified successfully.") upload_build_to_pypi() push_git_tags() @@ -67,17 +63,16 @@ def verify_build() -> None: sys.exit(1) -def get_github_client() -> Github: +# def get_github_client() -> Github: """Get a Github client.""" def upload_build_to_pypi() -> None: """Upload the build to PyPI.""" - # TODO: This needs to be updated to use the token and password from env variables. try: - subprocess.run(["twine", "upload", "dist/*"], check=True) - # TODO: Remove this print statement after testing + # Note current version uses the testpypi repository + subprocess.run(["twine", "upload", "--repository", "testpypi", "dist/*"], check=True) print("Build uploaded successfully.") except subprocess.CalledProcessError as error: print(f"Failed to upload build: {error}") @@ -87,7 +82,6 @@ def push_git_tags() -> None: """Push all git tags to the remote.""" try: subprocess.run(["git", "push", "--tags", "origin", "master"], check=True) - # TODO: Remove this print statement after testing print("Tags pushed successfully.") except subprocess.CalledProcessError as error: print(f"Failed to push tags: {error}") @@ -103,7 +97,11 @@ def push_git_tags() -> None: def main() -> None: """Run the main program.""" + print("Starting the upload process...") add_git_tag_for_version(version_number) - clean_repo() + remove_previous_dist() create_build() verify_build() + +if __name__ == "__main__": + main() From ea3c1f68bf8dd671a56d9f3054851fec488874e9 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Sun, 11 Feb 2024 13:34:36 -0800 Subject: [PATCH 05/75] Fix references to dist folder location --- scripts/release.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 19748fb1..1de831ae 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -17,7 +17,6 @@ def remove_previous_dist() -> None: """Check for dist folder, and if it exists, remove it.""" try: subprocess.run(["rm", "-rf", "dist/"], check=True) - print("Dist folder sucessfully removed.") except subprocess.CalledProcessError as error: print(f"Failed to clean repo: {error}") sys.exit(1) @@ -49,7 +48,7 @@ def verify_build() -> None: print(f"Failed to verify build: {error}") sys.exit(1) try: - subprocess.run(["parallel", "-j", "1", "-t", "tar", "-tvf", ":::", "/dist*"], check=True) + subprocess.run(["parallel", "-j", "1", "-t", "tar", "-tvf", ":::", "dist*"], check=True) confirmation = input("Does the build look correct? (y/n): ") if confirmation == "y": print("Build verified successfully.") From e5972cb88918a7765b732b8973f4b69caef6c502 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Fri, 16 Feb 2024 21:48:25 -0800 Subject: [PATCH 06/75] Create release notes --- scripts/release.py | 80 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 10 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 1de831ae..5351692b 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,8 +1,10 @@ import subprocess import sys -# from github import Github +import json +import re from getpass import getpass + def add_git_tag_for_version(version: str) -> None: """Add a git tag for the given version.""" try: @@ -62,9 +64,69 @@ def verify_build() -> None: sys.exit(1) -# def get_github_client() -> Github: - """Get a Github client.""" - +def generate_github_release_notes_body(version) -> str: + """Generate and grab release notes URL from Github.""" + try: + command = [ + "curl", + "-L", + "-X", + "POST", + "-H", + "Accept: application/vnd.github+json", + "-H", + f"Authorization: Bearer {GITHUB_TOKEN}", + "-H", + "X-GitHub-Api-Version: 2022-11-28", + "https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes", + "-d", + '{"tag_name":"${version_number}"}', + ] + response_json = subprocess.run(command, check=True, capture_output=True) + parsed_json = json.loads(response_json) + body = parsed_json["body"] + return body + except subprocess.CalledProcessError as error: + print(f"Failed to generate release notes: {error}") + return "" + + +def get_release_notes_url(body) -> str: + """Parse the release notes content to get the changelog URL.""" + url_pattern = re.compile(r"\*\*Full Changelog\*\*: (.*?)\n") + match = url_pattern.search(body) + if match: + return match.group(1) + else: + print("Failed to parse release notes URL from GitHub response.") + return "" + + +# TODO: Refactor to use markdown parsing library instead of regex +def get_changelog_release_notes() -> str: + """Get the changelog release notes.""" + changelog_text = None + with open("../CHANGELOG.md", "r", encoding="utf-8") as file: + changelog_text = file.read() + pattern = re.compile(rf"## {version_number}(?:\n(.*?))?##", re.DOTALL) + match = pattern.search(changelog_text) + if match: + return match.group(1) + else: + print("Failed to parse changelog release notes.") + return "" + + +def create_release_notes_body() -> str: + """Compile the release notes.""" + changelog_notes = get_changelog_release_notes() + github_release_body = generate_github_release_notes_body(version_number) + release_notes_url = get_release_notes_url(github_release_body) + full_release_notes = ( + changelog_notes + "\n\n**Full Changelog**: " + release_notes_url + ) + return full_release_notes + def upload_build_to_pypi() -> None: @@ -86,21 +148,19 @@ def push_git_tags() -> None: print(f"Failed to push tags: {error}") sys.exit(1) -""" -pyPyToken = input("Enter the PyPI token: ") -pyPyPassword = getpass("Enter the PyPI password: ") -githubToken = getpass("Enter the github token: ") -githubPassword = getpass("Enter the github password: ") -""" + version_number = input("Enter the version number: ") + def main() -> None: """Run the main program.""" print("Starting the upload process...") + add_git_tag_for_version(version_number) remove_previous_dist() create_build() verify_build() + if __name__ == "__main__": main() From 27d5d5e13b7744d6cc356dbce459fbe71ee392c6 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Fri, 16 Feb 2024 21:48:44 -0800 Subject: [PATCH 07/75] Revise regexs + minor linting --- scripts/release.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 5351692b..af5cc9eb 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -3,6 +3,9 @@ import json import re from getpass import getpass +import os + +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") def add_git_tag_for_version(version: str) -> None: @@ -50,7 +53,9 @@ def verify_build() -> None: print(f"Failed to verify build: {error}") sys.exit(1) try: - subprocess.run(["parallel", "-j", "1", "-t", "tar", "-tvf", ":::", "dist*"], check=True) + subprocess.run( + ["parallel", "-j", "1", "-t", "tar", "-tvf", ":::", "dist*"], check=True + ) confirmation = input("Does the build look correct? (y/n): ") if confirmation == "y": print("Build verified successfully.") @@ -80,10 +85,10 @@ def generate_github_release_notes_body(version) -> str: "X-GitHub-Api-Version: 2022-11-28", "https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes", "-d", - '{"tag_name":"${version_number}"}', + f'{{"tag_name":"{version_number}"}}', ] response_json = subprocess.run(command, check=True, capture_output=True) - parsed_json = json.loads(response_json) + parsed_json = json.loads(response_json.stdout) body = parsed_json["body"] return body except subprocess.CalledProcessError as error: @@ -93,7 +98,7 @@ def generate_github_release_notes_body(version) -> str: def get_release_notes_url(body) -> str: """Parse the release notes content to get the changelog URL.""" - url_pattern = re.compile(r"\*\*Full Changelog\*\*: (.*?)\n") + url_pattern = re.compile(r"\*\*Full Changelog\*\*: (.*)$") match = url_pattern.search(body) if match: return match.group(1) @@ -106,12 +111,12 @@ def get_release_notes_url(body) -> str: def get_changelog_release_notes() -> str: """Get the changelog release notes.""" changelog_text = None - with open("../CHANGELOG.md", "r", encoding="utf-8") as file: + with open("CHANGELOG.md", "r", encoding="utf-8") as file: changelog_text = file.read() - pattern = re.compile(rf"## {version_number}(?:\n(.*?))?##", re.DOTALL) + pattern = re.compile(rf"## {re.escape(version_number)}[^\n]*(.*?)##", re.DOTALL) match = pattern.search(changelog_text) if match: - return match.group(1) + return str(match.group(1)).strip() else: print("Failed to parse changelog release notes.") return "" @@ -133,12 +138,15 @@ def upload_build_to_pypi() -> None: """Upload the build to PyPI.""" try: # Note current version uses the testpypi repository - subprocess.run(["twine", "upload", "--repository", "testpypi", "dist/*"], check=True) + subprocess.run( + ["twine", "upload", "--repository", "testpypi", "dist/*"], check=True + ) print("Build uploaded successfully.") except subprocess.CalledProcessError as error: print(f"Failed to upload build: {error}") sys.exit(1) + def push_git_tags() -> None: """Push all git tags to the remote.""" try: From 9ed8ebb0e3af82ebab76831399b642270b7cca03 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Sun, 18 Feb 2024 17:24:16 -0800 Subject: [PATCH 08/75] Create github release draft --- scripts/release.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index af5cc9eb..fe03d1a4 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -133,6 +133,32 @@ def create_release_notes_body() -> str: return full_release_notes +def create_github_release_draft() -> None: + """Create a release on GitHub.""" + release_body = create_release_notes_body() + release_body = release_body.replace("\n", "\\n") + try: + command = [ + "curl", + "-L", + "-X", + "POST", + "-H", + "Accept: application/vnd.github+json", + "-H", + f"Authorization: Bearer {GITHUB_TOKEN}", + "-H", + "X-GitHub-Api-Version: 2022-11-28", + "https://api.github.com/repos/ekcorso/releasetestrepo2/releases", + "-d", + f'{{"tag_name":"{version_number}","name":"{version_number}","body":"{release_body}","draft":true,"prerelease":false}}', + ] + response_json = subprocess.run(command, check=True, capture_output=True) + parsed_json = json.loads(response_json.stdout) + print("Release created successfully: " + parsed_json["html_url"]) + except subprocess.CalledProcessError as error: + print(f"Failed to create release: {error}") + def upload_build_to_pypi() -> None: """Upload the build to PyPI.""" @@ -168,6 +194,9 @@ def main() -> None: remove_previous_dist() create_build() verify_build() + create_github_release_draft() + + print("Upload process complete.") if __name__ == "__main__": From f2e334cba8f503814e7e86a5c44b0bcaed06a38f Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Sun, 18 Feb 2024 17:42:26 -0800 Subject: [PATCH 09/75] Refactor build verification logic --- scripts/release.py | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index fe03d1a4..a11684ab 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -40,29 +40,24 @@ def create_build() -> None: def verify_build() -> None: """Verify the build.""" try: + if len(os.listdir("dist")) != 2: + print("WARNING: dist folder contains incorrect number of files.") + print("Contents of dist folder:") subprocess.run(["ls", "-l", "dist/"], check=True) - confirmation = input("Does the build look correct? (y/n): ") - if confirmation == "y": - print("Build verified successfully.") - upload_build_to_pypi() - push_git_tags() - else: - print("Build not uploaded.") - sys.exit(1) - except subprocess.CalledProcessError as error: - print(f"Failed to verify build: {error}") - sys.exit(1) - try: - subprocess.run( - ["parallel", "-j", "1", "-t", "tar", "-tvf", ":::", "dist*"], check=True - ) - confirmation = input("Does the build look correct? (y/n): ") - if confirmation == "y": - print("Build verified successfully.") - upload_build_to_pypi() - push_git_tags() - else: - print("Build not uploaded.") + try: + print("Contents of tar files in dist folder:") + for dir in os.listdir("dist"): + subprocess.run(["tar", "tvf", "dist/" + dir], check=True) + confirmation = input("Does the build look correct? (y/n): ") + if confirmation == "y": + print("Build verified successfully.") + upload_build_to_pypi() + push_git_tags() + else: + print("Could not verify. Build was not uploaded.") + sys.exit(1) + except subprocess.CalledProcessError as error: + print(f"Failed to verify build: {error}") sys.exit(1) except subprocess.CalledProcessError as error: print(f"Failed to verify build: {error}") From 083b8275ac2078306eaa2187939afb7432b858ee Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Mon, 19 Feb 2024 16:37:29 -0800 Subject: [PATCH 10/75] Update tag push logic, remove exit --- scripts/release.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index a11684ab..153b5c52 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -174,8 +174,7 @@ def push_git_tags() -> None: subprocess.run(["git", "push", "--tags", "origin", "master"], check=True) print("Tags pushed successfully.") except subprocess.CalledProcessError as error: - print(f"Failed to push tags: {error}") - sys.exit(1) + print(f"Failed to push tag(s) to Github: {error}") version_number = input("Enter the version number: ") From 7abfb1a84c6c6d7f5b84c3602aff68b396dfb322 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Thu, 22 Feb 2024 15:16:40 -0800 Subject: [PATCH 11/75] Refactor release draft url check --- scripts/release.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/release.py b/scripts/release.py index 153b5c52..3eb85bcb 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -150,7 +150,10 @@ def create_github_release_draft() -> None: ] response_json = subprocess.run(command, check=True, capture_output=True) parsed_json = json.loads(response_json.stdout) - print("Release created successfully: " + parsed_json["html_url"]) + if "html_url" in parsed_json: + print("Release created successfully: " + parsed_json["html_url"]) + else: + print("There may have been an error creating this release. Visit https://github.com/john-kurkowski/tldextract/releases to confirm release was created.") except subprocess.CalledProcessError as error: print(f"Failed to create release: {error}") From 728382f2d8d5ca974682125e657e3a9cc92b5acd Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Thu, 22 Feb 2024 15:23:40 -0800 Subject: [PATCH 12/75] Clean up style and improve logging --- scripts/release.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 3eb85bcb..b46de773 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -22,6 +22,7 @@ def remove_previous_dist() -> None: """Check for dist folder, and if it exists, remove it.""" try: subprocess.run(["rm", "-rf", "dist/"], check=True) + print("Previous dist folder removed successfully.") except subprocess.CalledProcessError as error: print(f"Failed to clean repo: {error}") sys.exit(1) @@ -87,7 +88,7 @@ def generate_github_release_notes_body(version) -> str: body = parsed_json["body"] return body except subprocess.CalledProcessError as error: - print(f"Failed to generate release notes: {error}") + print(f"Failed to generate release notes from Github: {error}") return "" @@ -153,7 +154,9 @@ def create_github_release_draft() -> None: if "html_url" in parsed_json: print("Release created successfully: " + parsed_json["html_url"]) else: - print("There may have been an error creating this release. Visit https://github.com/john-kurkowski/tldextract/releases to confirm release was created.") + print( + "There may have been an error creating this release. Visit https://github.com/john-kurkowski/tldextract/releases to confirm release was created." + ) except subprocess.CalledProcessError as error: print(f"Failed to create release: {error}") @@ -185,7 +188,11 @@ def push_git_tags() -> None: def main() -> None: """Run the main program.""" - print("Starting the upload process...") + print("Starting the release process...") + print("Checking for github token environment variable...") + if not GITHUB_TOKEN: + print("GITHUB_TOKEN environment variable not set.") + sys.exit(1) add_git_tag_for_version(version_number) remove_previous_dist() @@ -193,7 +200,7 @@ def main() -> None: verify_build() create_github_release_draft() - print("Upload process complete.") + print("Release process complete.") if __name__ == "__main__": From 6417210c904d12922d1b8f7622d8cb80b6b6464e Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Fri, 23 Feb 2024 14:25:58 -0800 Subject: [PATCH 13/75] Add check for test release --- scripts/release.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/release.py b/scripts/release.py index b46de773..2b3dc7ab 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -163,10 +163,14 @@ def create_github_release_draft() -> None: def upload_build_to_pypi() -> None: """Upload the build to PyPI.""" + target_repository = "pypi" + if is_test == "y": + target_repository = "testpypi" try: # Note current version uses the testpypi repository subprocess.run( - ["twine", "upload", "--repository", "testpypi", "dist/*"], check=True + ["twine", "upload", "--repository", f"{target_repository}", "dist/*"], + check=True, ) print("Build uploaded successfully.") except subprocess.CalledProcessError as error: @@ -183,6 +187,7 @@ def push_git_tags() -> None: print(f"Failed to push tag(s) to Github: {error}") +is_test = input("Is this a test release? (y/n): ") version_number = input("Enter the version number: ") From ecc3767470be6eb3ab8e1d7c6679fb9340a896da Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Mon, 26 Feb 2024 10:12:23 -0500 Subject: [PATCH 14/75] List dependencies for release --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index d323aae7..e56e42e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,10 @@ dependencies = [ ] [project.optional-dependencies] +release = [ + "build", + "twine", +] testing = [ "black", "mypy", From 4621470a4ae76b845c6c0d07e31aa53871aefe1e Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Mon, 26 Feb 2024 10:12:46 -0500 Subject: [PATCH 15/75] Check scripts --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 89b8802c..b4d819d6 100644 --- a/tox.ini +++ b/tox.ini @@ -18,5 +18,5 @@ extras = testing [testenv:typecheck] basepython = python3.8 -commands = mypy --show-error-codes tldextract tests +commands = mypy --show-error-codes scripts tldextract tests extras = testing From e88e4235c3876300a51f0b7384b2361b9d6c450b Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Mon, 26 Feb 2024 18:40:57 -0800 Subject: [PATCH 16/75] Move basic setup (input) inside main --- scripts/release.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 2b3dc7ab..78aee827 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -6,6 +6,8 @@ import os GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") +is_test = None +version_number = None def add_git_tag_for_version(version: str) -> None: @@ -187,17 +189,24 @@ def push_git_tags() -> None: print(f"Failed to push tag(s) to Github: {error}") -is_test = input("Is this a test release? (y/n): ") -version_number = input("Enter the version number: ") - - def main() -> None: """Run the main program.""" + global is_test, version_number + print("Starting the release process...") - print("Checking for github token environment variable...") + print("Checking for github token environment variable...") if not GITHUB_TOKEN: print("GITHUB_TOKEN environment variable not set.") sys.exit(1) + else: + print("GITHUB_TOKEN environment variable is good to go.") + + is_test = input("Is this a test release? (y/n): ") + while is_test not in ["y", "n"]: + print("Invalid input. Please enter 'y' or 'n'.") + is_test = input("Is this a test release? (y/n): ") + + version_number = input("Enter the version number: ") add_git_tag_for_version(version_number) remove_previous_dist() From bac124802068bb989807f172cbc7fb86f6cab1c7 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Mon, 26 Feb 2024 19:09:03 -0800 Subject: [PATCH 17/75] Refactor releasing to pypi --- scripts/release.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 78aee827..68413a92 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -165,19 +165,26 @@ def create_github_release_draft() -> None: def upload_build_to_pypi() -> None: """Upload the build to PyPI.""" - target_repository = "pypi" - if is_test == "y": - target_repository = "testpypi" - try: - # Note current version uses the testpypi repository - subprocess.run( - ["twine", "upload", "--repository", f"{target_repository}", "dist/*"], - check=True, - ) - print("Build uploaded successfully.") - except subprocess.CalledProcessError as error: - print(f"Failed to upload build: {error}") - sys.exit(1) + if is_test == "n": + try: + subprocess.run( + ["twine", "upload", "dist/*"], + check=True, + ) + print("Build uploaded successfully.") + except subprocess.CalledProcessError as error: + print(f"Failed to upload build: {error}") + sys.exit(1) + else: + try: + subprocess.run( + ["twine", "upload", "--repository", "testpypi", "dist/*"], + check=True, + ) + print("Build uploaded successfully.") + except subprocess.CalledProcessError as error: + print(f"Failed to upload build: {error}") + sys.exit(1) def push_git_tags() -> None: From 6fce0f1f2a2f5258a159c90cbb7ce4adb957aac6 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 27 Feb 2024 15:17:50 -0800 Subject: [PATCH 18/75] Add docstring --- scripts/release.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index 68413a92..f88e2f20 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,3 +1,14 @@ +""" +This script automates the release process for a Python package. It will: +- Add a git tag for the given version. +- Remove the previous dist folder. +- Create a build. +- Ask the user to verify the build. +- Upload the build to PyPI. +- Push all git tags to the remote. +- Create a draft release on GitHub using the version notes in CHANGELOG.md. +""" + import subprocess import sys import json From 0c9bd7e343f47245665a4015e22a947cdcd4c893 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 27 Feb 2024 15:18:04 -0800 Subject: [PATCH 19/75] Remove unused import --- scripts/release.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/release.py b/scripts/release.py index f88e2f20..18d7a564 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -13,7 +13,6 @@ import sys import json import re -from getpass import getpass import os GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") From 97334adf188235a5f732c710091c46820a1469ad Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 27 Feb 2024 15:18:26 -0800 Subject: [PATCH 20/75] Sort imports --- scripts/release.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 18d7a564..9a03ef97 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -9,11 +9,11 @@ - Create a draft release on GitHub using the version notes in CHANGELOG.md. """ -import subprocess -import sys import json -import re import os +import re +import subprocess +import sys GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") is_test = None From be3c80114f147df6a286d68c4c4a5b2f6b2b2cae Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 27 Feb 2024 15:19:47 -0800 Subject: [PATCH 21/75] Rename ambiguous dir variable --- scripts/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 9a03ef97..7f2aed87 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -59,8 +59,8 @@ def verify_build() -> None: subprocess.run(["ls", "-l", "dist/"], check=True) try: print("Contents of tar files in dist folder:") - for dir in os.listdir("dist"): - subprocess.run(["tar", "tvf", "dist/" + dir], check=True) + for directory in os.listdir("dist"): + subprocess.run(["tar", "tvf", "dist/" + directory], check=True) confirmation = input("Does the build look correct? (y/n): ") if confirmation == "y": print("Build verified successfully.") From 27c9048d2d7efafbd9a2aec3baf06f34c6fe0d41 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 27 Feb 2024 15:21:29 -0800 Subject: [PATCH 22/75] Remove unecessary params from open() --- scripts/release.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/release.py b/scripts/release.py index 7f2aed87..b99463e3 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -119,7 +119,8 @@ def get_release_notes_url(body) -> str: def get_changelog_release_notes() -> str: """Get the changelog release notes.""" changelog_text = None - with open("CHANGELOG.md", "r", encoding="utf-8") as file: + with open("CHANGELOG.md") as file: + changelog_text = file.read() pattern = re.compile(rf"## {re.escape(version_number)}[^\n]*(.*?)##", re.DOTALL) match = pattern.search(changelog_text) From d74891f87fca15e23f1da5c16a7f7799209b21eb Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Sun, 3 Mar 2024 17:51:43 -0800 Subject: [PATCH 23/75] Remove unnecessary try/ except blocks --- scripts/release.py | 145 +++++++++++++++++---------------------------- 1 file changed, 55 insertions(+), 90 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index b99463e3..4752cc61 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -22,59 +22,38 @@ def add_git_tag_for_version(version: str) -> None: """Add a git tag for the given version.""" - try: - subprocess.run(["git", "tag", "-a", version, "-m", version], check=True) - print(f"Version {version} tag added successfully.") - except subprocess.CalledProcessError as error: - print(f"Failed to add version tag: {error}") - sys.exit(1) + subprocess.run(["git", "tag", "-a", version, "-m", version], check=True) + print(f"Version {version} tag added successfully.") def remove_previous_dist() -> None: """Check for dist folder, and if it exists, remove it.""" - try: - subprocess.run(["rm", "-rf", "dist/"], check=True) - print("Previous dist folder removed successfully.") - except subprocess.CalledProcessError as error: - print(f"Failed to clean repo: {error}") - sys.exit(1) + subprocess.run(["rm", "-rf", "dist/"], check=True) + print("Previous dist folder removed successfully.") def create_build() -> None: """Create a build.""" - try: - subprocess.run(["python", "-m", "build"], check=True) - print("Build created successfully.") - except subprocess.CalledProcessError as error: - print(f"Failed to create build: {error}") - sys.exit(1) + subprocess.run(["python", "-m", "build"], check=True) + print("Build created successfully.") def verify_build() -> None: """Verify the build.""" - try: - if len(os.listdir("dist")) != 2: - print("WARNING: dist folder contains incorrect number of files.") - print("Contents of dist folder:") - subprocess.run(["ls", "-l", "dist/"], check=True) - try: - print("Contents of tar files in dist folder:") - for directory in os.listdir("dist"): - subprocess.run(["tar", "tvf", "dist/" + directory], check=True) - confirmation = input("Does the build look correct? (y/n): ") - if confirmation == "y": - print("Build verified successfully.") - upload_build_to_pypi() - push_git_tags() - else: - print("Could not verify. Build was not uploaded.") - sys.exit(1) - except subprocess.CalledProcessError as error: - print(f"Failed to verify build: {error}") - sys.exit(1) - except subprocess.CalledProcessError as error: - print(f"Failed to verify build: {error}") - sys.exit(1) + if len(os.listdir("dist")) != 2: + print("WARNING: dist folder contains incorrect number of files.") + print("Contents of dist folder:") + subprocess.run(["ls", "-l", "dist/"], check=True) + print("Contents of tar files in dist folder:") + for directory in os.listdir("dist"): + subprocess.run(["tar", "tvf", "dist/" + directory], check=True) + confirmation = input("Does the build look correct? (y/n): ") + if confirmation == "y": + print("Build verified successfully.") + upload_build_to_pypi() + push_git_tags() + else: + raise Exception("Could not verify. Build was not uploaded.") def generate_github_release_notes_body(version) -> str: @@ -146,65 +125,51 @@ def create_github_release_draft() -> None: """Create a release on GitHub.""" release_body = create_release_notes_body() release_body = release_body.replace("\n", "\\n") - try: - command = [ - "curl", - "-L", - "-X", - "POST", - "-H", - "Accept: application/vnd.github+json", - "-H", - f"Authorization: Bearer {GITHUB_TOKEN}", - "-H", - "X-GitHub-Api-Version: 2022-11-28", - "https://api.github.com/repos/ekcorso/releasetestrepo2/releases", - "-d", - f'{{"tag_name":"{version_number}","name":"{version_number}","body":"{release_body}","draft":true,"prerelease":false}}', - ] - response_json = subprocess.run(command, check=True, capture_output=True) - parsed_json = json.loads(response_json.stdout) - if "html_url" in parsed_json: - print("Release created successfully: " + parsed_json["html_url"]) - else: - print( - "There may have been an error creating this release. Visit https://github.com/john-kurkowski/tldextract/releases to confirm release was created." - ) - except subprocess.CalledProcessError as error: - print(f"Failed to create release: {error}") + command = [ + "curl", + "-L", + "-X", + "POST", + "-H", + "Accept: application/vnd.github+json", + "-H", + f"Authorization: Bearer {GITHUB_TOKEN}", + "-H", + "X-GitHub-Api-Version: 2022-11-28", + "https://api.github.com/repos/ekcorso/releasetestrepo2/releases", + "-d", + f'{{"tag_name":"{version_number}","name":"{version_number}","body":"{release_body}","draft":true,"prerelease":false}}', + ] + response_json = subprocess.run(command, check=True, capture_output=True) + parsed_json = json.loads(response_json.stdout) + if "html_url" in parsed_json: + print("Release created successfully: " + parsed_json["html_url"]) + else: + print( + "There may have been an error creating this release. Visit https://github.com/john-kurkowski/tldextract/releases to confirm release was created." + ) def upload_build_to_pypi() -> None: """Upload the build to PyPI.""" if is_test == "n": - try: - subprocess.run( - ["twine", "upload", "dist/*"], - check=True, - ) - print("Build uploaded successfully.") - except subprocess.CalledProcessError as error: - print(f"Failed to upload build: {error}") - sys.exit(1) + subprocess.run( + ["twine", "upload", "dist/*"], + check=True, + ) + print("Build uploaded successfully.") else: - try: - subprocess.run( - ["twine", "upload", "--repository", "testpypi", "dist/*"], - check=True, - ) - print("Build uploaded successfully.") - except subprocess.CalledProcessError as error: - print(f"Failed to upload build: {error}") - sys.exit(1) + subprocess.run( + ["twine", "upload", "--repository", "testpypi", "dist/*"], + check=True, + ) + print("Build uploaded successfully.") def push_git_tags() -> None: """Push all git tags to the remote.""" - try: - subprocess.run(["git", "push", "--tags", "origin", "master"], check=True) - print("Tags pushed successfully.") - except subprocess.CalledProcessError as error: - print(f"Failed to push tag(s) to Github: {error}") + subprocess.run(["git", "push", "--tags", "origin", "master"], check=True) + print("Tags pushed successfully.") def main() -> None: From d3ecd318234dc83653fcae4953bfa2f46f2f4fa6 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Mon, 4 Mar 2024 21:31:25 -0800 Subject: [PATCH 24/75] Streamline logging --- scripts/release.py | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 4752cc61..b40b698e 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -41,7 +41,9 @@ def create_build() -> None: def verify_build() -> None: """Verify the build.""" if len(os.listdir("dist")) != 2: - print("WARNING: dist folder contains incorrect number of files.") + print( + "WARNING: dist folder contains incorrect number of files.", file=sys.stderr + ) print("Contents of dist folder:") subprocess.run(["ls", "-l", "dist/"], check=True) print("Contents of tar files in dist folder:") @@ -79,7 +81,10 @@ def generate_github_release_notes_body(version) -> str: body = parsed_json["body"] return body except subprocess.CalledProcessError as error: - print(f"Failed to generate release notes from Github: {error}") + print( + f"WARNING: Failed to generate release notes from Github: {error}", + file=sys.stderr, + ) return "" @@ -90,12 +95,15 @@ def get_release_notes_url(body) -> str: if match: return match.group(1) else: - print("Failed to parse release notes URL from GitHub response.") + print( + "WARNING: Failed to parse release notes URL from GitHub response.", + file=sys.stderr, + ) return "" # TODO: Refactor to use markdown parsing library instead of regex -def get_changelog_release_notes() -> str: +def get_changelog_release_notes(release_notes_url) -> str: """Get the changelog release notes.""" changelog_text = None with open("CHANGELOG.md") as file: @@ -106,15 +114,18 @@ def get_changelog_release_notes() -> str: if match: return str(match.group(1)).strip() else: - print("Failed to parse changelog release notes.") + print( + f"WARNING: Failed to parse changelog release notes. Manually copy this version's notes from the CHANGELOG.md file to {release_notes_url}.", + file=sys.stderr, + ) return "" def create_release_notes_body() -> str: """Compile the release notes.""" - changelog_notes = get_changelog_release_notes() github_release_body = generate_github_release_notes_body(version_number) release_notes_url = get_release_notes_url(github_release_body) + changelog_notes = get_changelog_release_notes(release_notes_url) full_release_notes = ( changelog_notes + "\n\n**Full Changelog**: " + release_notes_url ) @@ -146,7 +157,8 @@ def create_github_release_draft() -> None: print("Release created successfully: " + parsed_json["html_url"]) else: print( - "There may have been an error creating this release. Visit https://github.com/john-kurkowski/tldextract/releases to confirm release was created." + "WARNING: There may have been an error creating this release. Visit https://github.com/john-kurkowski/tldextract/releases to confirm release was created.", + file=sys.stderr, ) @@ -157,19 +169,16 @@ def upload_build_to_pypi() -> None: ["twine", "upload", "dist/*"], check=True, ) - print("Build uploaded successfully.") else: subprocess.run( ["twine", "upload", "--repository", "testpypi", "dist/*"], check=True, ) - print("Build uploaded successfully.") def push_git_tags() -> None: """Push all git tags to the remote.""" subprocess.run(["git", "push", "--tags", "origin", "master"], check=True) - print("Tags pushed successfully.") def main() -> None: @@ -197,8 +206,6 @@ def main() -> None: verify_build() create_github_release_draft() - print("Release process complete.") - if __name__ == "__main__": main() From 82cf56f2e9b2773a29c553e6a82ce17198499b77 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Mon, 4 Mar 2024 21:55:25 -0800 Subject: [PATCH 25/75] Fix regex and update it's documentation --- scripts/release.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index b40b698e..0df46a22 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -102,14 +102,17 @@ def get_release_notes_url(body) -> str: return "" -# TODO: Refactor to use markdown parsing library instead of regex def get_changelog_release_notes(release_notes_url) -> str: - """Get the changelog release notes.""" + """Get the changelog release notes. + + Uses a regex starting on a heading beginning with the version number literal, and matching until the next heading. Using regex to match markup is brittle. Consider a Markdown-parsing library instead. + """ + changelog_text = None with open("CHANGELOG.md") as file: changelog_text = file.read() - pattern = re.compile(rf"## {re.escape(version_number)}[^\n]*(.*?)##", re.DOTALL) + pattern = re.compile(rf"## {re.escape(version_number)}[^\n]*(.*?)## ", re.DOTALL) match = pattern.search(changelog_text) if match: return str(match.group(1)).strip() From 885a46b35c63c268652eca63044f89213570866b Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 5 Mar 2024 16:35:34 -0800 Subject: [PATCH 26/75] Fix type errors --- scripts/release.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 0df46a22..63d095d7 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -58,7 +58,7 @@ def verify_build() -> None: raise Exception("Could not verify. Build was not uploaded.") -def generate_github_release_notes_body(version) -> str: +def generate_github_release_notes_body(version: str) -> str: """Generate and grab release notes URL from Github.""" try: command = [ @@ -79,7 +79,7 @@ def generate_github_release_notes_body(version) -> str: response_json = subprocess.run(command, check=True, capture_output=True) parsed_json = json.loads(response_json.stdout) body = parsed_json["body"] - return body + return str(body) except subprocess.CalledProcessError as error: print( f"WARNING: Failed to generate release notes from Github: {error}", @@ -88,7 +88,7 @@ def generate_github_release_notes_body(version) -> str: return "" -def get_release_notes_url(body) -> str: +def get_release_notes_url(body: str) -> str: """Parse the release notes content to get the changelog URL.""" url_pattern = re.compile(r"\*\*Full Changelog\*\*: (.*)$") match = url_pattern.search(body) @@ -102,15 +102,13 @@ def get_release_notes_url(body) -> str: return "" -def get_changelog_release_notes(release_notes_url) -> str: +def get_changelog_release_notes(release_notes_url: str) -> str: """Get the changelog release notes. Uses a regex starting on a heading beginning with the version number literal, and matching until the next heading. Using regex to match markup is brittle. Consider a Markdown-parsing library instead. """ - changelog_text = None with open("CHANGELOG.md") as file: - changelog_text = file.read() pattern = re.compile(rf"## {re.escape(version_number)}[^\n]*(.*?)## ", re.DOTALL) match = pattern.search(changelog_text) From b17d244338351f7520238beb940ff6ac89c5c946 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 5 Mar 2024 16:51:11 -0800 Subject: [PATCH 27/75] Parameterize use of version number (remove global) --- scripts/release.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 63d095d7..613118a9 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -17,7 +17,6 @@ GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") is_test = None -version_number = None def add_git_tag_for_version(version: str) -> None: @@ -74,7 +73,7 @@ def generate_github_release_notes_body(version: str) -> str: "X-GitHub-Api-Version: 2022-11-28", "https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes", "-d", - f'{{"tag_name":"{version_number}"}}', + f'{{"tag_name":"{version}"}}', ] response_json = subprocess.run(command, check=True, capture_output=True) parsed_json = json.loads(response_json.stdout) @@ -102,7 +101,7 @@ def get_release_notes_url(body: str) -> str: return "" -def get_changelog_release_notes(release_notes_url: str) -> str: +def get_changelog_release_notes(release_notes_url: str, version: str) -> str: """Get the changelog release notes. Uses a regex starting on a heading beginning with the version number literal, and matching until the next heading. Using regex to match markup is brittle. Consider a Markdown-parsing library instead. @@ -110,7 +109,7 @@ def get_changelog_release_notes(release_notes_url: str) -> str: with open("CHANGELOG.md") as file: changelog_text = file.read() - pattern = re.compile(rf"## {re.escape(version_number)}[^\n]*(.*?)## ", re.DOTALL) + pattern = re.compile(rf"## {re.escape(version)}[^\n]*(.*?)## ", re.DOTALL) match = pattern.search(changelog_text) if match: return str(match.group(1)).strip() @@ -122,20 +121,20 @@ def get_changelog_release_notes(release_notes_url: str) -> str: return "" -def create_release_notes_body() -> str: +def create_release_notes_body(version: str) -> str: """Compile the release notes.""" - github_release_body = generate_github_release_notes_body(version_number) + github_release_body = generate_github_release_notes_body(version) release_notes_url = get_release_notes_url(github_release_body) - changelog_notes = get_changelog_release_notes(release_notes_url) + changelog_notes = get_changelog_release_notes(release_notes_url, version) full_release_notes = ( changelog_notes + "\n\n**Full Changelog**: " + release_notes_url ) return full_release_notes -def create_github_release_draft() -> None: +def create_github_release_draft(version: str) -> None: """Create a release on GitHub.""" - release_body = create_release_notes_body() + release_body = create_release_notes_body(version) release_body = release_body.replace("\n", "\\n") command = [ "curl", @@ -150,7 +149,7 @@ def create_github_release_draft() -> None: "X-GitHub-Api-Version: 2022-11-28", "https://api.github.com/repos/ekcorso/releasetestrepo2/releases", "-d", - f'{{"tag_name":"{version_number}","name":"{version_number}","body":"{release_body}","draft":true,"prerelease":false}}', + f'{{"tag_name":"{version}","name":"{version}","body":"{release_body}","draft":true,"prerelease":false}}', ] response_json = subprocess.run(command, check=True, capture_output=True) parsed_json = json.loads(response_json.stdout) @@ -184,7 +183,7 @@ def push_git_tags() -> None: def main() -> None: """Run the main program.""" - global is_test, version_number + global is_test print("Starting the release process...") print("Checking for github token environment variable...") @@ -205,7 +204,7 @@ def main() -> None: remove_previous_dist() create_build() verify_build() - create_github_release_draft() + create_github_release_draft(version_number) if __name__ == "__main__": From 149610705c2e888f946f2d891cf732fffe1216a9 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Wed, 6 Mar 2024 16:07:54 -0800 Subject: [PATCH 28/75] Parameterize is_test --- scripts/release.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 613118a9..f7ecd339 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -16,7 +16,6 @@ import sys GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") -is_test = None def add_git_tag_for_version(version: str) -> None: @@ -37,7 +36,7 @@ def create_build() -> None: print("Build created successfully.") -def verify_build() -> None: +def verify_build(is_test: str) -> None: """Verify the build.""" if len(os.listdir("dist")) != 2: print( @@ -51,7 +50,7 @@ def verify_build() -> None: confirmation = input("Does the build look correct? (y/n): ") if confirmation == "y": print("Build verified successfully.") - upload_build_to_pypi() + upload_build_to_pypi(is_test) push_git_tags() else: raise Exception("Could not verify. Build was not uploaded.") @@ -162,7 +161,7 @@ def create_github_release_draft(version: str) -> None: ) -def upload_build_to_pypi() -> None: +def upload_build_to_pypi(is_test: str) -> None: """Upload the build to PyPI.""" if is_test == "n": subprocess.run( @@ -183,7 +182,6 @@ def push_git_tags() -> None: def main() -> None: """Run the main program.""" - global is_test print("Starting the release process...") print("Checking for github token environment variable...") @@ -203,7 +201,7 @@ def main() -> None: add_git_tag_for_version(version_number) remove_previous_dist() create_build() - verify_build() + verify_build(is_test) create_github_release_draft(version_number) From 29c618ad27d1cce7a09f989074c24e19788c6ba9 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Wed, 6 Mar 2024 16:34:52 -0800 Subject: [PATCH 29/75] Pass GITHUB_TOKEN in as parameter --- scripts/release.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index f7ecd339..5c7c6990 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -15,7 +15,6 @@ import subprocess import sys -GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") def add_git_tag_for_version(version: str) -> None: @@ -56,7 +55,7 @@ def verify_build(is_test: str) -> None: raise Exception("Could not verify. Build was not uploaded.") -def generate_github_release_notes_body(version: str) -> str: +def generate_github_release_notes_body(token:str, version: str) -> str: """Generate and grab release notes URL from Github.""" try: command = [ @@ -67,7 +66,7 @@ def generate_github_release_notes_body(version: str) -> str: "-H", "Accept: application/vnd.github+json", "-H", - f"Authorization: Bearer {GITHUB_TOKEN}", + f"Authorization: Bearer {token}", "-H", "X-GitHub-Api-Version: 2022-11-28", "https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes", @@ -120,9 +119,9 @@ def get_changelog_release_notes(release_notes_url: str, version: str) -> str: return "" -def create_release_notes_body(version: str) -> str: +def create_release_notes_body(token:str, version: str) -> str: """Compile the release notes.""" - github_release_body = generate_github_release_notes_body(version) + github_release_body = generate_github_release_notes_body(token, version) release_notes_url = get_release_notes_url(github_release_body) changelog_notes = get_changelog_release_notes(release_notes_url, version) full_release_notes = ( @@ -131,9 +130,9 @@ def create_release_notes_body(version: str) -> str: return full_release_notes -def create_github_release_draft(version: str) -> None: +def create_github_release_draft(token: str, version: str) -> None: """Create a release on GitHub.""" - release_body = create_release_notes_body(version) + release_body = create_release_notes_body(token, version) release_body = release_body.replace("\n", "\\n") command = [ "curl", @@ -143,7 +142,7 @@ def create_github_release_draft(version: str) -> None: "-H", "Accept: application/vnd.github+json", "-H", - f"Authorization: Bearer {GITHUB_TOKEN}", + f"Authorization: Bearer {token}", "-H", "X-GitHub-Api-Version: 2022-11-28", "https://api.github.com/repos/ekcorso/releasetestrepo2/releases", @@ -183,6 +182,8 @@ def push_git_tags() -> None: def main() -> None: """Run the main program.""" + GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") + print("Starting the release process...") print("Checking for github token environment variable...") if not GITHUB_TOKEN: @@ -202,7 +203,7 @@ def main() -> None: remove_previous_dist() create_build() verify_build(is_test) - create_github_release_draft(version_number) + create_github_release_draft(GITHUB_TOKEN, version_number) if __name__ == "__main__": From 56659e602d2358dfb21bf27802c23b68f62e96f2 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Thu, 7 Mar 2024 16:54:14 -0800 Subject: [PATCH 30/75] Use pathlib for dist paths throughout --- scripts/release.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 5c7c6990..f02cbe8e 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -11,12 +11,12 @@ import json import os +from pathlib import Path import re import subprocess import sys - def add_git_tag_for_version(version: str) -> None: """Add a git tag for the given version.""" subprocess.run(["git", "tag", "-a", version, "-m", version], check=True) @@ -25,7 +25,7 @@ def add_git_tag_for_version(version: str) -> None: def remove_previous_dist() -> None: """Check for dist folder, and if it exists, remove it.""" - subprocess.run(["rm", "-rf", "dist/"], check=True) + subprocess.run(["rm", "-rf", Path("dist")], check=True) print("Previous dist folder removed successfully.") @@ -42,10 +42,10 @@ def verify_build(is_test: str) -> None: "WARNING: dist folder contains incorrect number of files.", file=sys.stderr ) print("Contents of dist folder:") - subprocess.run(["ls", "-l", "dist/"], check=True) + subprocess.run(["ls", "-l", Path("dist")], check=True) print("Contents of tar files in dist folder:") for directory in os.listdir("dist"): - subprocess.run(["tar", "tvf", "dist/" + directory], check=True) + subprocess.run(["tar", "tvf", Path("dist") / directory], check=True) confirmation = input("Does the build look correct? (y/n): ") if confirmation == "y": print("Build verified successfully.") @@ -164,12 +164,12 @@ def upload_build_to_pypi(is_test: str) -> None: """Upload the build to PyPI.""" if is_test == "n": subprocess.run( - ["twine", "upload", "dist/*"], + ["twine", "upload", Path("dist")], check=True, ) else: subprocess.run( - ["twine", "upload", "--repository", "testpypi", "dist/*"], + ["twine", "upload", "--repository", "testpypi", Path("dist") / "*"], check=True, ) From 35902790518775719a2f3a112971a9d9a878df3a Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Thu, 7 Mar 2024 17:14:56 -0800 Subject: [PATCH 31/75] Dedupe twine test/ reg upload logic --- scripts/release.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index f02cbe8e..c51dcc52 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -55,7 +55,7 @@ def verify_build(is_test: str) -> None: raise Exception("Could not verify. Build was not uploaded.") -def generate_github_release_notes_body(token:str, version: str) -> str: +def generate_github_release_notes_body(token: str, version: str) -> str: """Generate and grab release notes URL from Github.""" try: command = [ @@ -119,7 +119,7 @@ def get_changelog_release_notes(release_notes_url: str, version: str) -> str: return "" -def create_release_notes_body(token:str, version: str) -> str: +def create_release_notes_body(token: str, version: str) -> str: """Compile the release notes.""" github_release_body = generate_github_release_notes_body(token, version) release_notes_url = get_release_notes_url(github_release_body) @@ -162,16 +162,13 @@ def create_github_release_draft(token: str, version: str) -> None: def upload_build_to_pypi(is_test: str) -> None: """Upload the build to PyPI.""" + upload_command = ["twine", "upload", "--repository", "testpypi", Path("dist") / "*"] if is_test == "n": - subprocess.run( - ["twine", "upload", Path("dist")], - check=True, - ) - else: - subprocess.run( - ["twine", "upload", "--repository", "testpypi", Path("dist") / "*"], - check=True, - ) + upload_command = ["twine", "upload", Path("dist") / "*"] + subprocess.run( + upload_command, + check=True, + ) def push_git_tags() -> None: From a0b8e5c62715cd9b2772bdd3040aa6823e233709 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Fri, 8 Mar 2024 19:50:49 -0800 Subject: [PATCH 32/75] Use requests lib instead of curl --- scripts/release.py | 76 ++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index c51dcc52..c03138f8 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -13,6 +13,7 @@ import os from pathlib import Path import re +import requests import subprocess import sys @@ -57,32 +58,24 @@ def verify_build(is_test: str) -> None: def generate_github_release_notes_body(token: str, version: str) -> str: """Generate and grab release notes URL from Github.""" - try: - command = [ - "curl", - "-L", - "-X", - "POST", - "-H", - "Accept: application/vnd.github+json", - "-H", - f"Authorization: Bearer {token}", - "-H", - "X-GitHub-Api-Version: 2022-11-28", - "https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes", - "-d", - f'{{"tag_name":"{version}"}}', - ] - response_json = subprocess.run(command, check=True, capture_output=True) - parsed_json = json.loads(response_json.stdout) - body = parsed_json["body"] - return str(body) - except subprocess.CalledProcessError as error: + response = requests.post( + f"https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes", + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": "2022-11-28", + }, + json={"tag_name": version}, + ) + response_json = response.json() + if response_json.get("message"): print( - f"WARNING: Failed to generate release notes from Github: {error}", + f"WARNING: Failed to generate release notes from Github: {response_json['message']}", file=sys.stderr, ) return "" + else: + return str(response_json["body"]) def get_release_notes_url(body: str) -> str: @@ -133,26 +126,29 @@ def create_release_notes_body(token: str, version: str) -> str: def create_github_release_draft(token: str, version: str) -> None: """Create a release on GitHub.""" release_body = create_release_notes_body(token, version) + """ + print("The release body before mod is:" + release_body) release_body = release_body.replace("\n", "\\n") - command = [ - "curl", - "-L", - "-X", - "POST", - "-H", - "Accept: application/vnd.github+json", - "-H", - f"Authorization: Bearer {token}", - "-H", - "X-GitHub-Api-Version: 2022-11-28", + print("The release body is:" + release_body) + """ + response = requests.post( "https://api.github.com/repos/ekcorso/releasetestrepo2/releases", - "-d", - f'{{"tag_name":"{version}","name":"{version}","body":"{release_body}","draft":true,"prerelease":false}}', - ] - response_json = subprocess.run(command, check=True, capture_output=True) - parsed_json = json.loads(response_json.stdout) - if "html_url" in parsed_json: - print("Release created successfully: " + parsed_json["html_url"]) + headers={ + "Accept": "application/vnd.github+json", + "Authorization": f"Bearer {token}", + "X-GitHub-Api-Version": "2022-11-28", + }, + json={ + "tag_name": version, + "name": version, + "body": release_body, + "draft": True, + "prerelease": False, + }, + ) + + if "html_url" in response.json(): + print("Release created successfully: " + response.json()["html_url"]) else: print( "WARNING: There may have been an error creating this release. Visit https://github.com/john-kurkowski/tldextract/releases to confirm release was created.", From 3dd1fbe06037983bf2bd3700ca254f00d2e7feae Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Fri, 8 Mar 2024 20:33:09 -0800 Subject: [PATCH 33/75] Fix lint errors --- scripts/release.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index c03138f8..701956be 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,5 +1,7 @@ """ -This script automates the release process for a Python package. It will: +This script automates the release process for a Python package. + +It will: - Add a git tag for the given version. - Remove the previous dist folder. - Create a build. @@ -9,13 +11,13 @@ - Create a draft release on GitHub using the version notes in CHANGELOG.md. """ -import json import os -from pathlib import Path import re -import requests import subprocess import sys +from pathlib import Path + +import requests def add_git_tag_for_version(version: str) -> None: @@ -59,7 +61,7 @@ def verify_build(is_test: str) -> None: def generate_github_release_notes_body(token: str, version: str) -> str: """Generate and grab release notes URL from Github.""" response = requests.post( - f"https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes", + "https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes", headers={ "Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}", @@ -97,7 +99,6 @@ def get_changelog_release_notes(release_notes_url: str, version: str) -> str: Uses a regex starting on a heading beginning with the version number literal, and matching until the next heading. Using regex to match markup is brittle. Consider a Markdown-parsing library instead. """ - with open("CHANGELOG.md") as file: changelog_text = file.read() pattern = re.compile(rf"## {re.escape(version)}[^\n]*(.*?)## ", re.DOTALL) @@ -174,12 +175,11 @@ def push_git_tags() -> None: def main() -> None: """Run the main program.""" - - GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") + github_token = os.environ.get("GITHUB_TOKEN") print("Starting the release process...") print("Checking for github token environment variable...") - if not GITHUB_TOKEN: + if not github_token: print("GITHUB_TOKEN environment variable not set.") sys.exit(1) else: @@ -196,7 +196,7 @@ def main() -> None: remove_previous_dist() create_build() verify_build(is_test) - create_github_release_draft(GITHUB_TOKEN, version_number) + create_github_release_draft(github_token, version_number) if __name__ == "__main__": From dc40d6f3b6b19840e1297db67d628ca1a9eb6a7d Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Fri, 8 Mar 2024 21:18:19 -0800 Subject: [PATCH 34/75] Add type annotation for upload command --- scripts/release.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/release.py b/scripts/release.py index 701956be..34681794 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -159,7 +159,13 @@ def create_github_release_draft(token: str, version: str) -> None: def upload_build_to_pypi(is_test: str) -> None: """Upload the build to PyPI.""" - upload_command = ["twine", "upload", "--repository", "testpypi", Path("dist") / "*"] + upload_command: list[str | Path] = [ + "twine", + "upload", + "--repository", + "testpypi", + Path("dist") / "*", + ] if is_test == "n": upload_command = ["twine", "upload", Path("dist") / "*"] subprocess.run( From 4d375f03927041336a02c70ab98bc99be2f96f98 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Fri, 8 Mar 2024 21:34:53 -0800 Subject: [PATCH 35/75] Allow newer syntax on older Python --- scripts/release.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index 34681794..b550c67e 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -11,6 +11,8 @@ - Create a draft release on GitHub using the version notes in CHANGELOG.md. """ +from __future__ import annotations + import os import re import subprocess From f060e8656dd8ec2fbb94794d429bd1450e4c2175 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Sun, 10 Mar 2024 17:00:49 -0700 Subject: [PATCH 36/75] Scaffold test for release script --- tests/test_release.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/test_release.py diff --git a/tests/test_release.py b/tests/test_release.py new file mode 100644 index 00000000..88e10e7c --- /dev/null +++ b/tests/test_release.py @@ -0,0 +1,11 @@ +"""Test the library maintainer release script.""" + +import pytest + +from scripts import release + + +def test_happy_path() -> None: + """Test the release script happy path.""" + assert release + pytest.xfail("Not implemented yet") From c74da79ea9f7f1b78e781e980e84d350bf36f24b Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Sun, 10 Mar 2024 17:17:29 -0700 Subject: [PATCH 37/75] Allow typechecking non-package code --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6547aa95..c9d68730 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ write_to = "tldextract/_version.py" version = {attr = "setuptools_scm.get_version"} [tool.mypy] +explicit_package_bases = true strict = true [tool.pytest.ini_options] From af9b799715459712b54fd8d6a7c11ffb642c15ef Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Sun, 10 Mar 2024 19:49:03 -0700 Subject: [PATCH 38/75] Simplify upload to Pypi --- scripts/release.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index b550c67e..97016741 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -161,15 +161,10 @@ def create_github_release_draft(token: str, version: str) -> None: def upload_build_to_pypi(is_test: str) -> None: """Upload the build to PyPI.""" - upload_command: list[str | Path] = [ - "twine", - "upload", - "--repository", - "testpypi", - Path("dist") / "*", - ] - if is_test == "n": - upload_command = ["twine", "upload", Path("dist") / "*"] + repository: list[str | Path] = ( + [] if is_test == "n" else ["--repository", "testpypi"] + ) + upload_command = ["twine", "upload", *repository, Path("dist") / "*"] subprocess.run( upload_command, check=True, From d089ba74f027b6c9e4b36f13f64994a5e77b77e8 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Sun, 10 Mar 2024 19:51:41 -0700 Subject: [PATCH 39/75] Remove comments --- scripts/release.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 97016741..8221fbff 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -129,11 +129,6 @@ def create_release_notes_body(token: str, version: str) -> str: def create_github_release_draft(token: str, version: str) -> None: """Create a release on GitHub.""" release_body = create_release_notes_body(token, version) - """ - print("The release body before mod is:" + release_body) - release_body = release_body.replace("\n", "\\n") - print("The release body is:" + release_body) - """ response = requests.post( "https://api.github.com/repos/ekcorso/releasetestrepo2/releases", headers={ From 1a425430972cc92f18d5244d06f2810eb0d43e86 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Sun, 10 Mar 2024 20:05:17 -0700 Subject: [PATCH 40/75] Refactor error handling for requests --- scripts/release.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 8221fbff..e35c4952 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -71,15 +71,16 @@ def generate_github_release_notes_body(token: str, version: str) -> str: }, json={"tag_name": version}, ) - response_json = response.json() - if response_json.get("message"): + + try: + response.raise_for_status() + except requests.exceptions.HTTPError as err: print( - f"WARNING: Failed to generate release notes from Github: {response_json['message']}", + f"WARNING: Failed to generate release notes from Github: {err}", file=sys.stderr, ) return "" - else: - return str(response_json["body"]) + return str(response.json()["body"]) def get_release_notes_url(body: str) -> str: @@ -145,13 +146,15 @@ def create_github_release_draft(token: str, version: str) -> None: }, ) - if "html_url" in response.json(): - print("Release created successfully: " + response.json()["html_url"]) - else: + try: + response.raise_for_status() + except requests.exceptions.HTTPError as err: print( - "WARNING: There may have been an error creating this release. Visit https://github.com/john-kurkowski/tldextract/releases to confirm release was created.", + f"WARNING: Failed to create release on Github: {err}", file=sys.stderr, ) + return + print("Release created successfully: " + response.json()["html_url"]) def upload_build_to_pypi(is_test: str) -> None: From d9d0e5aa36a8b031104e997755669e9197acf21a Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Sun, 10 Mar 2024 20:48:20 -0700 Subject: [PATCH 41/75] Refactor imput validation for is_test check --- scripts/release.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index e35c4952..1150d4e7 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -186,10 +186,12 @@ def main() -> None: else: print("GITHUB_TOKEN environment variable is good to go.") - is_test = input("Is this a test release? (y/n): ") - while is_test not in ["y", "n"]: - print("Invalid input. Please enter 'y' or 'n'.") + while True: is_test = input("Is this a test release? (y/n): ") + if is_test in ["y", "n"]: + break + else: + print("Invalid input. Please enter 'y' or 'n'.") version_number = input("Enter the version number: ") From 7f64e21d3b2ad689438e9df14fc6345526d2b48b Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 15:35:50 -0700 Subject: [PATCH 42/75] Reduce calls --- scripts/release.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 1150d4e7..ad8a1f44 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -42,15 +42,16 @@ def create_build() -> None: def verify_build(is_test: str) -> None: """Verify the build.""" - if len(os.listdir("dist")) != 2: + build_files = os.listdir("dist") + if len(build_files) != 2: print( "WARNING: dist folder contains incorrect number of files.", file=sys.stderr ) print("Contents of dist folder:") subprocess.run(["ls", "-l", Path("dist")], check=True) print("Contents of tar files in dist folder:") - for directory in os.listdir("dist"): - subprocess.run(["tar", "tvf", Path("dist") / directory], check=True) + for build_file in build_files: + subprocess.run(["tar", "tvf", Path("dist") / build_file], check=True) confirmation = input("Does the build look correct? (y/n): ") if confirmation == "y": print("Build verified successfully.") From 25f03a400aeb1239ebce4384ab23195cb06e78da Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 15:36:04 -0700 Subject: [PATCH 43/75] Prefer f-strings to concatenation Produces more sensible strings when using the `Mock` class. --- scripts/release.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index ad8a1f44..37724da2 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -122,9 +122,7 @@ def create_release_notes_body(token: str, version: str) -> str: github_release_body = generate_github_release_notes_body(token, version) release_notes_url = get_release_notes_url(github_release_body) changelog_notes = get_changelog_release_notes(release_notes_url, version) - full_release_notes = ( - changelog_notes + "\n\n**Full Changelog**: " + release_notes_url - ) + full_release_notes = f"{changelog_notes}\n\n**Full Changelog**: {release_notes_url}" return full_release_notes @@ -155,7 +153,7 @@ def create_github_release_draft(token: str, version: str) -> None: file=sys.stderr, ) return - print("Release created successfully: " + response.json()["html_url"]) + print(f'Release created successfully: {response.json()["html_url"]}') def upload_build_to_pypi(is_test: str) -> None: From d6f177c1d6a0a16592a03e77d6471e05695e3c1a Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 12 Mar 2024 14:52:49 -0700 Subject: [PATCH 44/75] Document prerequisites --- scripts/release.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index 1150d4e7..4cd83b77 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -9,6 +9,14 @@ - Upload the build to PyPI. - Push all git tags to the remote. - Create a draft release on GitHub using the version notes in CHANGELOG.md. + +Prerequisites: + - This must be run from the root of the repository. + - The repo must have a clean git working tree. + - The user must have the GITHUB_TOKEN environment variable set to a valid GitHub personal access token. + - The CHANGELOG.md file must already contain an entry for the version being released. + - Install requirements with: pip install --upgrade --editable '.[release]' + """ from __future__ import annotations From 8535304056f720033c13dcdd3cefe09912276f6d Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Mon, 11 Mar 2024 17:15:38 -0700 Subject: [PATCH 45/75] Cleanup main and extract env setup checks to helpers --- scripts/release.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 4cd83b77..23b58e6a 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -182,25 +182,43 @@ def push_git_tags() -> None: subprocess.run(["git", "push", "--tags", "origin", "master"], check=True) -def main() -> None: - """Run the main program.""" - github_token = os.environ.get("GITHUB_TOKEN") +def check_for_clean_working_tree() -> None: + """Check for a clean git working tree.""" + git_status = subprocess.run( + ["git", "status", "--porcelain"], capture_output=True, text=True + ) + if git_status.stdout: + print( + "Git working tree is not clean. Please commit or stash changes.", + file=sys.stderr, + ) + sys.exit(1) - print("Starting the release process...") - print("Checking for github token environment variable...") + +def get_env_github_token() -> str: + """Check for the GITHUB_TOKEN environment variable.""" + github_token = os.environ.get("GITHUB_TOKEN") if not github_token: - print("GITHUB_TOKEN environment variable not set.") + print("GITHUB_TOKEN environment variable not set.", file=sys.stderr) sys.exit(1) - else: - print("GITHUB_TOKEN environment variable is good to go.") + return github_token + +def get_is_test_response() -> str: + """Ask the user if this is a test release.""" while True: is_test = input("Is this a test release? (y/n): ") if is_test in ["y", "n"]: - break + return is_test else: - print("Invalid input. Please enter 'y' or 'n'.") + print("Invalid input. Please enter 'y' or 'n.'") + +def main() -> None: + """Run the main program.""" + check_for_clean_working_tree() + github_token = get_env_github_token() + is_test = get_is_test_response() version_number = input("Enter the version number: ") add_git_tag_for_version(version_number) From 1b74692fae069b469df157d63cfad2e430cb6609 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 15:36:51 -0700 Subject: [PATCH 46/75] Test release script happy path --- pyproject.toml | 1 + tests/__snapshots__/test_release.ambr | 161 ++++++++++++++++++++++++++ tests/test_release.py | 79 ++++++++++++- 3 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 tests/__snapshots__/test_release.ambr diff --git a/pyproject.toml b/pyproject.toml index c9d68730..52b1e2ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ testing = [ "pytest-mock", "responses", "ruff", + "syrupy", "tox", "types-filelock", "types-requests", diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr new file mode 100644 index 00000000..4977cb56 --- /dev/null +++ b/tests/__snapshots__/test_release.ambr @@ -0,0 +1,161 @@ +# serializer version: 1 +# name: test_happy_path + _CallList([ + _Call( + tuple( + 'dist', + ), + dict({ + }), + ), + ]) +# --- +# name: test_happy_path.1 + _CallList([ + _Call( + tuple( + 'https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes', + ), + dict({ + 'headers': dict({ + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer fake-token', + 'X-GitHub-Api-Version': '2022-11-28', + }), + 'json': dict({ + 'tag_name': '5.0.1', + }), + }), + ), + _Call( + tuple( + 'https://api.github.com/repos/ekcorso/releasetestrepo2/releases', + ), + dict({ + 'headers': dict({ + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer fake-token', + 'X-GitHub-Api-Version': '2022-11-28', + }), + 'json': dict({ + 'body': ''' + * Bugfixes + * Indicate MD5 not used in a security context (FIPS compliance) ([#309](https://github.com/john-kurkowski/tldextract/issues/309)) + * Misc. + * Increase typecheck aggression + + **Full Changelog**: fake-body + ''', + 'draft': True, + 'name': '5.0.1', + 'prerelease': False, + 'tag_name': '5.0.1', + }), + }), + ), + ]) +# --- +# name: test_happy_path.2 + _CallList([ + _Call( + tuple( + list([ + 'git', + 'tag', + '-a', + '5.0.1', + '-m', + '5.0.1', + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + tuple( + list([ + 'rm', + '-rf', + PosixPath('dist'), + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + tuple( + list([ + 'python', + '-m', + 'build', + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + tuple( + list([ + 'ls', + '-l', + PosixPath('dist'), + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + tuple( + list([ + 'twine', + 'upload', + '--repository', + 'testpypi', + PosixPath('dist/*'), + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + tuple( + list([ + 'git', + 'push', + '--tags', + 'origin', + 'master', + ]), + ), + dict({ + 'check': True, + }), + ), + ]) +# --- +# name: test_happy_path.3 + ''' + Starting the release process... + Checking for github token environment variable... + GITHUB_TOKEN environment variable is good to go. + Version 5.0.1 tag added successfully. + Previous dist folder removed successfully. + Build created successfully. + Contents of dist folder: + Contents of tar files in dist folder: + Build verified successfully. + Release created successfully: https://github.com/path/to/release + + ''' +# --- +# name: test_happy_path.4 + ''' + WARNING: dist folder contains incorrect number of files. + + ''' +# --- diff --git a/tests/test_release.py b/tests/test_release.py index 88e10e7c..d61442f3 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -1,11 +1,82 @@ """Test the library maintainer release script.""" +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any +from unittest import mock + import pytest +from syrupy.assertion import SnapshotAssertion from scripts import release -def test_happy_path() -> None: - """Test the release script happy path.""" - assert release - pytest.xfail("Not implemented yet") +@pytest.fixture +def listdir() -> Iterator[mock.Mock]: + """Stub listing directory.""" + with mock.patch("os.listdir") as patched: + yield patched + + +@pytest.fixture +def requests() -> Iterator[mock.Mock]: + """Stub network requests.""" + with mock.patch("requests.post") as patched: + yield patched + + +@pytest.fixture +def subprocess() -> Iterator[mock.Mock]: + """Stub running external commands.""" + with mock.patch("subprocess.run") as patched: + yield patched + + +def test_happy_path( + capsys: pytest.CaptureFixture[str], + listdir: mock.Mock, + monkeypatch: pytest.MonkeyPatch, + requests: mock.Mock, + snapshot: SnapshotAssertion, + subprocess: mock.Mock, +) -> None: + """Test the release script happy path. + + Simulate user input for a typical, existing release. + + This one test case covers most lines of the release script, without + actually making network requests or running subprocesses. For an + infrequently used script, this coverage is useful without being too brittle + to change. + """ + monkeypatch.setenv("GITHUB_TOKEN", "fake-token") + + input_values = iter(["y", "5.0.1", "y"]) + + def cycle_input_values(prompt: str) -> str: + return next(input_values) + + monkeypatch.setattr("builtins.input", cycle_input_values) + + def mock_post(*args: Any, **kwargs: Any) -> mock.Mock: + return mock.Mock( + json=mock.Mock( + return_value={ + "body": "Body start **Full Changelog**: fake-body", + "html_url": "https://github.com/path/to/release", + } + ), + ) + + requests.side_effect = mock_post + + release.main() + + out, err = capsys.readouterr() + + assert listdir.call_args_list == snapshot + assert requests.call_args_list == snapshot + assert subprocess.call_args_list == snapshot + assert out == snapshot + assert err == snapshot From e1be439dedfbb24e7cf376c9e9e0f99dc1534a9a Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 12 Mar 2024 15:49:58 -0700 Subject: [PATCH 47/75] Check if version tag already exists --- scripts/release.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index 23b58e6a..c8fd70a3 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -214,12 +214,23 @@ def get_is_test_response() -> str: print("Invalid input. Please enter 'y' or 'n.'") +def check_if_tag_exists(tag: str) -> None: + """Check if the tag already exists.""" + tag_check = subprocess.run( + ["git", "tag", "--list", tag], capture_output=True, text=True + ) + if tag_check.stdout: + raise Exception(f"Tag {tag} already exists.") + + def main() -> None: """Run the main program.""" check_for_clean_working_tree() github_token = get_env_github_token() is_test = get_is_test_response() + version_number = input("Enter the version number: ") + check_if_tag_exists(version_number) add_git_tag_for_version(version_number) remove_previous_dist() From a231fe19eda076f05cf106c6c10378534513c165 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 16:10:58 -0700 Subject: [PATCH 48/75] Simplify --- tests/__snapshots__/test_release.ambr | 33 +++++++++++++++++++++--- tests/test_release.py | 37 +++++++++++++++------------ 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr index a88b62f0..562af154 100644 --- a/tests/__snapshots__/test_release.ambr +++ b/tests/__snapshots__/test_release.ambr @@ -3,7 +3,21 @@ _CallList([ _Call( tuple( - 'dist', + 'Is this a test release? (y/n): ', + ), + dict({ + }), + ), + _Call( + tuple( + 'Enter the version number: ', + ), + dict({ + }), + ), + _Call( + tuple( + 'Does the build look correct? (y/n): ', ), dict({ }), @@ -11,6 +25,17 @@ ]) # --- # name: test_happy_path.1 + _CallList([ + _Call( + tuple( + 'dist', + ), + dict({ + }), + ), + ]) +# --- +# name: test_happy_path.2 _CallList([ _Call( tuple( @@ -55,7 +80,7 @@ ), ]) # --- -# name: test_happy_path.2 +# name: test_happy_path.3 _CallList([ _Call( tuple( @@ -151,7 +176,7 @@ ), ]) # --- -# name: test_happy_path.3 +# name: test_happy_path.4 ''' Version 5.0.1 tag added successfully. Previous dist folder removed successfully. @@ -163,7 +188,7 @@ ''' # --- -# name: test_happy_path.4 +# name: test_happy_path.5 ''' WARNING: dist folder contains incorrect number of files. diff --git a/tests/test_release.py b/tests/test_release.py index 6b728a02..f84ff11e 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -13,21 +13,28 @@ @pytest.fixture -def listdir() -> Iterator[mock.Mock]: +def mock_input() -> Iterator[mock.Mock]: + """Stub reading user input.""" + with mock.patch("builtins.input") as patched: + yield patched + + +@pytest.fixture +def mock_listdir() -> Iterator[mock.Mock]: """Stub listing directory.""" with mock.patch("os.listdir") as patched: yield patched @pytest.fixture -def requests() -> Iterator[mock.Mock]: +def mock_requests() -> Iterator[mock.Mock]: """Stub network requests.""" with mock.patch("requests.post") as patched: yield patched @pytest.fixture -def subprocess() -> Iterator[mock.Mock]: +def mock_subprocess() -> Iterator[mock.Mock]: """Stub running external commands.""" with mock.patch("subprocess.run") as patched: patched.return_value.stdout = "" @@ -36,11 +43,12 @@ def subprocess() -> Iterator[mock.Mock]: def test_happy_path( capsys: pytest.CaptureFixture[str], - listdir: mock.Mock, + mock_input: mock.Mock, + mock_listdir: mock.Mock, + mock_requests: mock.Mock, + mock_subprocess: mock.Mock, monkeypatch: pytest.MonkeyPatch, - requests: mock.Mock, snapshot: SnapshotAssertion, - subprocess: mock.Mock, ) -> None: """Test the release script happy path. @@ -53,14 +61,10 @@ def test_happy_path( """ monkeypatch.setenv("GITHUB_TOKEN", "fake-token") - input_values = iter(["y", "5.0.1", "y"]) - - def cycle_input_values(prompt: str) -> str: - return next(input_values) - - monkeypatch.setattr("builtins.input", cycle_input_values) + mock_input.side_effect = ["y", "5.0.1", "y"] def mock_post(*args: Any, **kwargs: Any) -> mock.Mock: + """Return _one_ response JSON that happens to match expectations for multiple requests.""" return mock.Mock( json=mock.Mock( return_value={ @@ -70,14 +74,15 @@ def mock_post(*args: Any, **kwargs: Any) -> mock.Mock: ), ) - requests.side_effect = mock_post + mock_requests.side_effect = mock_post release.main() out, err = capsys.readouterr() - assert listdir.call_args_list == snapshot - assert requests.call_args_list == snapshot - assert subprocess.call_args_list == snapshot + assert mock_input.call_args_list == snapshot + assert mock_listdir.call_args_list == snapshot + assert mock_requests.call_args_list == snapshot + assert mock_subprocess.call_args_list == snapshot assert out == snapshot assert err == snapshot From 3d99f1c5c569457fc3827f4443ce13bd62b2d11f Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 12 Mar 2024 16:25:32 -0700 Subject: [PATCH 49/75] Revert "Check if version tag already exists" This reverts commit e1be439dedfbb24e7cf376c9e9e0f99dc1534a9a. --- scripts/release.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index c8fd70a3..23b58e6a 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -214,23 +214,12 @@ def get_is_test_response() -> str: print("Invalid input. Please enter 'y' or 'n.'") -def check_if_tag_exists(tag: str) -> None: - """Check if the tag already exists.""" - tag_check = subprocess.run( - ["git", "tag", "--list", tag], capture_output=True, text=True - ) - if tag_check.stdout: - raise Exception(f"Tag {tag} already exists.") - - def main() -> None: """Run the main program.""" check_for_clean_working_tree() github_token = get_env_github_token() is_test = get_is_test_response() - version_number = input("Enter the version number: ") - check_if_tag_exists(version_number) add_git_tag_for_version(version_number) remove_previous_dist() From 72141cd2c57494c80d44194994ce749cc308d1c3 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 12 Mar 2024 16:47:41 -0700 Subject: [PATCH 50/75] Replace test repo url --- scripts/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 23b58e6a..a913536d 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -71,7 +71,7 @@ def verify_build(is_test: str) -> None: def generate_github_release_notes_body(token: str, version: str) -> str: """Generate and grab release notes URL from Github.""" response = requests.post( - "https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes", + "https://api.github.com/repos/john-kurkowski/tldextract/releases/generate-notes", headers={ "Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}", @@ -139,7 +139,7 @@ def create_github_release_draft(token: str, version: str) -> None: """Create a release on GitHub.""" release_body = create_release_notes_body(token, version) response = requests.post( - "https://api.github.com/repos/ekcorso/releasetestrepo2/releases", + "https://api.github.com/repos/john-kurkowski/tldextract/releases", headers={ "Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}", From 32b4c9a4969417904be4b12aebdcb12e44b79d02 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 16:51:20 -0700 Subject: [PATCH 51/75] Expect Windows snapshot test to fail --- tests/test_release.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_release.py b/tests/test_release.py index f84ff11e..ad3e6e2d 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -2,6 +2,7 @@ from __future__ import annotations +import sys from collections.abc import Iterator from typing import Any from unittest import mock @@ -41,6 +42,9 @@ def mock_subprocess() -> Iterator[mock.Mock]: yield patched +@pytest.mark.xfail( + sys.platform == "win32", reason="Snapshot paths are different on Windows" +) def test_happy_path( capsys: pytest.CaptureFixture[str], mock_input: mock.Mock, From 978913b60629dec1dacb32511659004d6ea7bd1c Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 17:04:36 -0700 Subject: [PATCH 52/75] fixup! Expect Windows snapshot test to fail --- tests/test_release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_release.py b/tests/test_release.py index ad3e6e2d..c227aac1 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -42,7 +42,7 @@ def mock_subprocess() -> Iterator[mock.Mock]: yield patched -@pytest.mark.xfail( +@pytest.mark.skipif( sys.platform == "win32", reason="Snapshot paths are different on Windows" ) def test_happy_path( From 83e706bbd508179a4073ec1bfdc70ace8a324ac0 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 12 Mar 2024 16:25:32 -0700 Subject: [PATCH 53/75] Revert "Check if version tag already exists" This reverts commit e1be439dedfbb24e7cf376c9e9e0f99dc1534a9a. --- scripts/release.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 0ca98e8e..62be649e 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -213,23 +213,12 @@ def get_is_test_response() -> str: print("Invalid input. Please enter 'y' or 'n.'") -def check_if_tag_exists(tag: str) -> None: - """Check if the tag already exists.""" - tag_check = subprocess.run( - ["git", "tag", "--list", tag], capture_output=True, text=True - ) - if tag_check.stdout: - raise Exception(f"Tag {tag} already exists.") - - def main() -> None: """Run the main program.""" check_for_clean_working_tree() github_token = get_env_github_token() is_test = get_is_test_response() - version_number = input("Enter the version number: ") - check_if_tag_exists(version_number) add_git_tag_for_version(version_number) remove_previous_dist() From dbb2bd17cf3f88c9181fb5525e5b758854bb6dd5 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Tue, 12 Mar 2024 16:47:41 -0700 Subject: [PATCH 54/75] Replace test repo url --- scripts/release.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 62be649e..a1fd539b 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -72,7 +72,7 @@ def verify_build(is_test: str) -> None: def generate_github_release_notes_body(token: str, version: str) -> str: """Generate and grab release notes URL from Github.""" response = requests.post( - "https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes", + "https://api.github.com/repos/john-kurkowski/tldextract/releases/generate-notes", headers={ "Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}", @@ -138,7 +138,7 @@ def create_github_release_draft(token: str, version: str) -> None: """Create a release on GitHub.""" release_body = create_release_notes_body(token, version) response = requests.post( - "https://api.github.com/repos/ekcorso/releasetestrepo2/releases", + "https://api.github.com/repos/john-kurkowski/tldextract/releases", headers={ "Accept": "application/vnd.github+json", "Authorization": f"Bearer {token}", From 939e9bafc17d8888aae1e028216e5ac318e01c25 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Wed, 13 Mar 2024 13:11:09 -0700 Subject: [PATCH 55/75] Update snapshot --- tests/__snapshots__/test_release.ambr | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr index 359af38e..9a44d749 100644 --- a/tests/__snapshots__/test_release.ambr +++ b/tests/__snapshots__/test_release.ambr @@ -39,7 +39,7 @@ _CallList([ _Call( tuple( - 'https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes', + 'https://api.github.com/repos/john-kurkowski/tldextract/releases/generate-notes', ), dict({ 'headers': dict({ @@ -54,7 +54,7 @@ ), _Call( tuple( - 'https://api.github.com/repos/ekcorso/releasetestrepo2/releases', + 'https://api.github.com/repos/john-kurkowski/tldextract/releases', ), dict({ 'headers': dict({ @@ -95,20 +95,6 @@ 'text': True, }), ), - _Call( - tuple( - list([ - 'git', - 'tag', - '--list', - '5.0.1', - ]), - ), - dict({ - 'capture_output': True, - 'text': True, - }), - ), _Call( tuple( list([ From 64541991e0dbbd91605051bec49aa18850a9a10f Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Sun, 10 Mar 2024 17:00:49 -0700 Subject: [PATCH 56/75] Scaffold test for release script --- tests/test_release.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/test_release.py diff --git a/tests/test_release.py b/tests/test_release.py new file mode 100644 index 00000000..88e10e7c --- /dev/null +++ b/tests/test_release.py @@ -0,0 +1,11 @@ +"""Test the library maintainer release script.""" + +import pytest + +from scripts import release + + +def test_happy_path() -> None: + """Test the release script happy path.""" + assert release + pytest.xfail("Not implemented yet") From bbd4e6dec4b8b1c36acc82858fa306ebc3a6acb4 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Sun, 10 Mar 2024 17:17:29 -0700 Subject: [PATCH 57/75] Allow typechecking non-package code --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6547aa95..c9d68730 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,6 +83,7 @@ write_to = "tldextract/_version.py" version = {attr = "setuptools_scm.get_version"} [tool.mypy] +explicit_package_bases = true strict = true [tool.pytest.ini_options] From 99ac9ef984deedbf3cb70585bb08c88841ce5095 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 15:35:50 -0700 Subject: [PATCH 58/75] Reduce calls --- scripts/release.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index a913536d..6e927c21 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -50,15 +50,16 @@ def create_build() -> None: def verify_build(is_test: str) -> None: """Verify the build.""" - if len(os.listdir("dist")) != 2: + build_files = os.listdir("dist") + if len(build_files) != 2: print( "WARNING: dist folder contains incorrect number of files.", file=sys.stderr ) print("Contents of dist folder:") subprocess.run(["ls", "-l", Path("dist")], check=True) print("Contents of tar files in dist folder:") - for directory in os.listdir("dist"): - subprocess.run(["tar", "tvf", Path("dist") / directory], check=True) + for build_file in build_files: + subprocess.run(["tar", "tvf", Path("dist") / build_file], check=True) confirmation = input("Does the build look correct? (y/n): ") if confirmation == "y": print("Build verified successfully.") From 8bc06eefc626dbc6ae70e8a2482df10489e5441a Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 15:36:04 -0700 Subject: [PATCH 59/75] Prefer f-strings to concatenation Produces more sensible strings when using the `Mock` class. --- scripts/release.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 6e927c21..a1fd539b 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -130,9 +130,7 @@ def create_release_notes_body(token: str, version: str) -> str: github_release_body = generate_github_release_notes_body(token, version) release_notes_url = get_release_notes_url(github_release_body) changelog_notes = get_changelog_release_notes(release_notes_url, version) - full_release_notes = ( - changelog_notes + "\n\n**Full Changelog**: " + release_notes_url - ) + full_release_notes = f"{changelog_notes}\n\n**Full Changelog**: {release_notes_url}" return full_release_notes @@ -163,7 +161,7 @@ def create_github_release_draft(token: str, version: str) -> None: file=sys.stderr, ) return - print("Release created successfully: " + response.json()["html_url"]) + print(f'Release created successfully: {response.json()["html_url"]}') def upload_build_to_pypi(is_test: str) -> None: From 78baee717fc0e0b513d31063c725e5d9e9b1ad99 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 15:36:51 -0700 Subject: [PATCH 60/75] Test release script happy path --- pyproject.toml | 1 + tests/__snapshots__/test_release.ambr | 161 ++++++++++++++++++++++++++ tests/test_release.py | 79 ++++++++++++- 3 files changed, 237 insertions(+), 4 deletions(-) create mode 100644 tests/__snapshots__/test_release.ambr diff --git a/pyproject.toml b/pyproject.toml index c9d68730..52b1e2ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ testing = [ "pytest-mock", "responses", "ruff", + "syrupy", "tox", "types-filelock", "types-requests", diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr new file mode 100644 index 00000000..4977cb56 --- /dev/null +++ b/tests/__snapshots__/test_release.ambr @@ -0,0 +1,161 @@ +# serializer version: 1 +# name: test_happy_path + _CallList([ + _Call( + tuple( + 'dist', + ), + dict({ + }), + ), + ]) +# --- +# name: test_happy_path.1 + _CallList([ + _Call( + tuple( + 'https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes', + ), + dict({ + 'headers': dict({ + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer fake-token', + 'X-GitHub-Api-Version': '2022-11-28', + }), + 'json': dict({ + 'tag_name': '5.0.1', + }), + }), + ), + _Call( + tuple( + 'https://api.github.com/repos/ekcorso/releasetestrepo2/releases', + ), + dict({ + 'headers': dict({ + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer fake-token', + 'X-GitHub-Api-Version': '2022-11-28', + }), + 'json': dict({ + 'body': ''' + * Bugfixes + * Indicate MD5 not used in a security context (FIPS compliance) ([#309](https://github.com/john-kurkowski/tldextract/issues/309)) + * Misc. + * Increase typecheck aggression + + **Full Changelog**: fake-body + ''', + 'draft': True, + 'name': '5.0.1', + 'prerelease': False, + 'tag_name': '5.0.1', + }), + }), + ), + ]) +# --- +# name: test_happy_path.2 + _CallList([ + _Call( + tuple( + list([ + 'git', + 'tag', + '-a', + '5.0.1', + '-m', + '5.0.1', + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + tuple( + list([ + 'rm', + '-rf', + PosixPath('dist'), + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + tuple( + list([ + 'python', + '-m', + 'build', + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + tuple( + list([ + 'ls', + '-l', + PosixPath('dist'), + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + tuple( + list([ + 'twine', + 'upload', + '--repository', + 'testpypi', + PosixPath('dist/*'), + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + tuple( + list([ + 'git', + 'push', + '--tags', + 'origin', + 'master', + ]), + ), + dict({ + 'check': True, + }), + ), + ]) +# --- +# name: test_happy_path.3 + ''' + Starting the release process... + Checking for github token environment variable... + GITHUB_TOKEN environment variable is good to go. + Version 5.0.1 tag added successfully. + Previous dist folder removed successfully. + Build created successfully. + Contents of dist folder: + Contents of tar files in dist folder: + Build verified successfully. + Release created successfully: https://github.com/path/to/release + + ''' +# --- +# name: test_happy_path.4 + ''' + WARNING: dist folder contains incorrect number of files. + + ''' +# --- diff --git a/tests/test_release.py b/tests/test_release.py index 88e10e7c..d61442f3 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -1,11 +1,82 @@ """Test the library maintainer release script.""" +from __future__ import annotations + +from collections.abc import Iterator +from typing import Any +from unittest import mock + import pytest +from syrupy.assertion import SnapshotAssertion from scripts import release -def test_happy_path() -> None: - """Test the release script happy path.""" - assert release - pytest.xfail("Not implemented yet") +@pytest.fixture +def listdir() -> Iterator[mock.Mock]: + """Stub listing directory.""" + with mock.patch("os.listdir") as patched: + yield patched + + +@pytest.fixture +def requests() -> Iterator[mock.Mock]: + """Stub network requests.""" + with mock.patch("requests.post") as patched: + yield patched + + +@pytest.fixture +def subprocess() -> Iterator[mock.Mock]: + """Stub running external commands.""" + with mock.patch("subprocess.run") as patched: + yield patched + + +def test_happy_path( + capsys: pytest.CaptureFixture[str], + listdir: mock.Mock, + monkeypatch: pytest.MonkeyPatch, + requests: mock.Mock, + snapshot: SnapshotAssertion, + subprocess: mock.Mock, +) -> None: + """Test the release script happy path. + + Simulate user input for a typical, existing release. + + This one test case covers most lines of the release script, without + actually making network requests or running subprocesses. For an + infrequently used script, this coverage is useful without being too brittle + to change. + """ + monkeypatch.setenv("GITHUB_TOKEN", "fake-token") + + input_values = iter(["y", "5.0.1", "y"]) + + def cycle_input_values(prompt: str) -> str: + return next(input_values) + + monkeypatch.setattr("builtins.input", cycle_input_values) + + def mock_post(*args: Any, **kwargs: Any) -> mock.Mock: + return mock.Mock( + json=mock.Mock( + return_value={ + "body": "Body start **Full Changelog**: fake-body", + "html_url": "https://github.com/path/to/release", + } + ), + ) + + requests.side_effect = mock_post + + release.main() + + out, err = capsys.readouterr() + + assert listdir.call_args_list == snapshot + assert requests.call_args_list == snapshot + assert subprocess.call_args_list == snapshot + assert out == snapshot + assert err == snapshot From 7bd161d68a4f204e60ec11f6e8d4cd5ad5024671 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 16:10:58 -0700 Subject: [PATCH 61/75] Simplify --- tests/__snapshots__/test_release.ambr | 33 +++++++++++++++++++++--- tests/test_release.py | 37 +++++++++++++++------------ 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr index 4977cb56..bd0ff99f 100644 --- a/tests/__snapshots__/test_release.ambr +++ b/tests/__snapshots__/test_release.ambr @@ -3,7 +3,21 @@ _CallList([ _Call( tuple( - 'dist', + 'Is this a test release? (y/n): ', + ), + dict({ + }), + ), + _Call( + tuple( + 'Enter the version number: ', + ), + dict({ + }), + ), + _Call( + tuple( + 'Does the build look correct? (y/n): ', ), dict({ }), @@ -11,6 +25,17 @@ ]) # --- # name: test_happy_path.1 + _CallList([ + _Call( + tuple( + 'dist', + ), + dict({ + }), + ), + ]) +# --- +# name: test_happy_path.2 _CallList([ _Call( tuple( @@ -55,7 +80,7 @@ ), ]) # --- -# name: test_happy_path.2 +# name: test_happy_path.3 _CallList([ _Call( tuple( @@ -138,7 +163,7 @@ ), ]) # --- -# name: test_happy_path.3 +# name: test_happy_path.4 ''' Starting the release process... Checking for github token environment variable... @@ -153,7 +178,7 @@ ''' # --- -# name: test_happy_path.4 +# name: test_happy_path.5 ''' WARNING: dist folder contains incorrect number of files. diff --git a/tests/test_release.py b/tests/test_release.py index d61442f3..1363a999 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -13,21 +13,28 @@ @pytest.fixture -def listdir() -> Iterator[mock.Mock]: +def mock_input() -> Iterator[mock.Mock]: + """Stub reading user input.""" + with mock.patch("builtins.input") as patched: + yield patched + + +@pytest.fixture +def mock_listdir() -> Iterator[mock.Mock]: """Stub listing directory.""" with mock.patch("os.listdir") as patched: yield patched @pytest.fixture -def requests() -> Iterator[mock.Mock]: +def mock_requests() -> Iterator[mock.Mock]: """Stub network requests.""" with mock.patch("requests.post") as patched: yield patched @pytest.fixture -def subprocess() -> Iterator[mock.Mock]: +def mock_subprocess() -> Iterator[mock.Mock]: """Stub running external commands.""" with mock.patch("subprocess.run") as patched: yield patched @@ -35,11 +42,12 @@ def subprocess() -> Iterator[mock.Mock]: def test_happy_path( capsys: pytest.CaptureFixture[str], - listdir: mock.Mock, + mock_input: mock.Mock, + mock_listdir: mock.Mock, + mock_requests: mock.Mock, + mock_subprocess: mock.Mock, monkeypatch: pytest.MonkeyPatch, - requests: mock.Mock, snapshot: SnapshotAssertion, - subprocess: mock.Mock, ) -> None: """Test the release script happy path. @@ -52,14 +60,10 @@ def test_happy_path( """ monkeypatch.setenv("GITHUB_TOKEN", "fake-token") - input_values = iter(["y", "5.0.1", "y"]) - - def cycle_input_values(prompt: str) -> str: - return next(input_values) - - monkeypatch.setattr("builtins.input", cycle_input_values) + mock_input.side_effect = ["y", "5.0.1", "y"] def mock_post(*args: Any, **kwargs: Any) -> mock.Mock: + """Return _one_ response JSON that happens to match expectations for multiple requests.""" return mock.Mock( json=mock.Mock( return_value={ @@ -69,14 +73,15 @@ def mock_post(*args: Any, **kwargs: Any) -> mock.Mock: ), ) - requests.side_effect = mock_post + mock_requests.side_effect = mock_post release.main() out, err = capsys.readouterr() - assert listdir.call_args_list == snapshot - assert requests.call_args_list == snapshot - assert subprocess.call_args_list == snapshot + assert mock_input.call_args_list == snapshot + assert mock_listdir.call_args_list == snapshot + assert mock_requests.call_args_list == snapshot + assert mock_subprocess.call_args_list == snapshot assert out == snapshot assert err == snapshot From 4153e833351ae47c8fbfa47bdcd6b7d32b25af3a Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 16:51:20 -0700 Subject: [PATCH 62/75] Expect Windows snapshot test to fail --- tests/test_release.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_release.py b/tests/test_release.py index 1363a999..19cd2aa4 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -2,6 +2,7 @@ from __future__ import annotations +import sys from collections.abc import Iterator from typing import Any from unittest import mock @@ -40,6 +41,9 @@ def mock_subprocess() -> Iterator[mock.Mock]: yield patched +@pytest.mark.xfail( + sys.platform == "win32", reason="Snapshot paths are different on Windows" +) def test_happy_path( capsys: pytest.CaptureFixture[str], mock_input: mock.Mock, From aa2233571cc5a4ec355bbf217ecb94fdd6227641 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Tue, 12 Mar 2024 17:04:36 -0700 Subject: [PATCH 63/75] fixup! Expect Windows snapshot test to fail --- tests/test_release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_release.py b/tests/test_release.py index 19cd2aa4..966dbbea 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -41,7 +41,7 @@ def mock_subprocess() -> Iterator[mock.Mock]: yield patched -@pytest.mark.xfail( +@pytest.mark.skipif( sys.platform == "win32", reason="Snapshot paths are different on Windows" ) def test_happy_path( From ffea6985ee7c2e908fd5ac508aa9ad5ebc37173a Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Wed, 13 Mar 2024 13:11:09 -0700 Subject: [PATCH 64/75] Update snapshot --- tests/__snapshots__/test_release.ambr | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr index bd0ff99f..71187f02 100644 --- a/tests/__snapshots__/test_release.ambr +++ b/tests/__snapshots__/test_release.ambr @@ -39,7 +39,7 @@ _CallList([ _Call( tuple( - 'https://api.github.com/repos/ekcorso/releasetestrepo2/releases/generate-notes', + 'https://api.github.com/repos/john-kurkowski/tldextract/releases/generate-notes', ), dict({ 'headers': dict({ @@ -54,7 +54,7 @@ ), _Call( tuple( - 'https://api.github.com/repos/ekcorso/releasetestrepo2/releases', + 'https://api.github.com/repos/john-kurkowski/tldextract/releases', ), dict({ 'headers': dict({ @@ -82,6 +82,19 @@ # --- # name: test_happy_path.3 _CallList([ + _Call( + tuple( + list([ + 'git', + 'status', + '--porcelain', + ]), + ), + dict({ + 'capture_output': True, + 'text': True, + }), + ), _Call( tuple( list([ @@ -165,9 +178,6 @@ # --- # name: test_happy_path.4 ''' - Starting the release process... - Checking for github token environment variable... - GITHUB_TOKEN environment variable is good to go. Version 5.0.1 tag added successfully. Previous dist folder removed successfully. Build created successfully. @@ -175,7 +185,7 @@ Contents of tar files in dist folder: Build verified successfully. Release created successfully: https://github.com/path/to/release - + ''' # --- # name: test_happy_path.5 From 5492d66605c2448f427c45c0eba0f210671b880e Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Thu, 14 Mar 2024 19:00:36 -0700 Subject: [PATCH 65/75] Temp commit for test --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dc3190d5..b5b6933c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ tldextract.egg-info tldextract/.suffix_cache/* .tox .pytest_cache +.python-version From d8601ffef1c3ebc37b4be913f24fa88558dafcb1 Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Thu, 14 Mar 2024 19:27:07 -0700 Subject: [PATCH 66/75] Revert "Temp commit for test" This reverts commit 5492d66605c2448f427c45c0eba0f210671b880e. --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index b5b6933c..dc3190d5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,3 @@ tldextract.egg-info tldextract/.suffix_cache/* .tox .pytest_cache -.python-version From 10424f699b3f04fb1b18678cbd0053437e3c21d3 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Fri, 15 Mar 2024 15:02:58 -0700 Subject: [PATCH 67/75] Update snapshot --- tests/__snapshots__/test_release.ambr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr index 71187f02..9a44d749 100644 --- a/tests/__snapshots__/test_release.ambr +++ b/tests/__snapshots__/test_release.ambr @@ -185,7 +185,7 @@ Contents of tar files in dist folder: Build verified successfully. Release created successfully: https://github.com/path/to/release - + ''' # --- # name: test_happy_path.5 From f14f8e78182d0969af08cdedb5f04d89522d732e Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Fri, 15 Mar 2024 15:26:40 -0700 Subject: [PATCH 68/75] Document --- scripts/release.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index a1fd539b..6eb6d848 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -49,7 +49,11 @@ def create_build() -> None: def verify_build(is_test: str) -> None: - """Verify the build.""" + """Verify the build. + + Print the archives in dist/ and ask the user to manually inspect and + confirm they contain the expected files, e.g. source files and test files. + """ build_files = os.listdir("dist") if len(build_files) != 2: print( @@ -109,7 +113,9 @@ def get_release_notes_url(body: str) -> str: def get_changelog_release_notes(release_notes_url: str, version: str) -> str: """Get the changelog release notes. - Uses a regex starting on a heading beginning with the version number literal, and matching until the next heading. Using regex to match markup is brittle. Consider a Markdown-parsing library instead. + Uses a regex starting on a heading beginning with the version number + literal, and matching until the next heading. Using regex to match markup + is brittle. Consider a Markdown-parsing library instead. """ with open("CHANGELOG.md") as file: changelog_text = file.read() From 9c7bce994a18574a5d899ca32cdd4f7b29f49d6f Mon Sep 17 00:00:00 2001 From: Emily Corso Date: Fri, 15 Mar 2024 15:47:24 -0700 Subject: [PATCH 69/75] Update pre req documentation --- scripts/release.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/release.py b/scripts/release.py index 6eb6d848..d5d93b74 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -14,6 +14,7 @@ - This must be run from the root of the repository. - The repo must have a clean git working tree. - The user must have the GITHUB_TOKEN environment variable set to a valid GitHub personal access token. + - The user will need credentials for the PyPI repository, which the user will be prompted for during the upload step. The user will need to paste the token manually from a password manager or similar. - The CHANGELOG.md file must already contain an entry for the version being released. - Install requirements with: pip install --upgrade --editable '.[release]' From 7ed31c1539e828cfb43988d7df8084d30ab820e2 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Fri, 15 Mar 2024 15:45:15 -0700 Subject: [PATCH 70/75] Snapshot all mock calls --- tests/__snapshots__/test_release.ambr | 27 +++++++++++++++++++++++++++ tests/test_release.py | 8 ++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr index 9a44d749..47a52973 100644 --- a/tests/__snapshots__/test_release.ambr +++ b/tests/__snapshots__/test_release.ambr @@ -2,6 +2,7 @@ # name: test_happy_path _CallList([ _Call( + '', tuple( 'Is this a test release? (y/n): ', ), @@ -9,6 +10,7 @@ }), ), _Call( + '', tuple( 'Enter the version number: ', ), @@ -16,6 +18,7 @@ }), ), _Call( + '', tuple( 'Does the build look correct? (y/n): ', ), @@ -27,17 +30,33 @@ # name: test_happy_path.1 _CallList([ _Call( + '', tuple( 'dist', ), dict({ }), ), + _Call( + '().__len__', + tuple( + ), + dict({ + }), + ), + _Call( + '().__iter__', + tuple( + ), + dict({ + }), + ), ]) # --- # name: test_happy_path.2 _CallList([ _Call( + '', tuple( 'https://api.github.com/repos/john-kurkowski/tldextract/releases/generate-notes', ), @@ -53,6 +72,7 @@ }), ), _Call( + '', tuple( 'https://api.github.com/repos/john-kurkowski/tldextract/releases', ), @@ -83,6 +103,7 @@ # name: test_happy_path.3 _CallList([ _Call( + '', tuple( list([ 'git', @@ -96,6 +117,7 @@ }), ), _Call( + '', tuple( list([ 'git', @@ -111,6 +133,7 @@ }), ), _Call( + '', tuple( list([ 'rm', @@ -123,6 +146,7 @@ }), ), _Call( + '', tuple( list([ 'python', @@ -135,6 +159,7 @@ }), ), _Call( + '', tuple( list([ 'ls', @@ -147,6 +172,7 @@ }), ), _Call( + '', tuple( list([ 'twine', @@ -161,6 +187,7 @@ }), ), _Call( + '', tuple( list([ 'git', diff --git a/tests/test_release.py b/tests/test_release.py index c227aac1..2312aacc 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -84,9 +84,9 @@ def mock_post(*args: Any, **kwargs: Any) -> mock.Mock: out, err = capsys.readouterr() - assert mock_input.call_args_list == snapshot - assert mock_listdir.call_args_list == snapshot - assert mock_requests.call_args_list == snapshot - assert mock_subprocess.call_args_list == snapshot + assert mock_input.mock_calls == snapshot + assert mock_listdir.mock_calls == snapshot + assert mock_requests.mock_calls == snapshot + assert mock_subprocess.mock_calls == snapshot assert out == snapshot assert err == snapshot From 19e7b5b5505e17090b00783464f84305479a0fda Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Fri, 15 Mar 2024 15:55:59 -0700 Subject: [PATCH 71/75] DRY --- tests/__snapshots__/test_release.ambr | 370 +++++++++++++------------- tests/test_release.py | 66 ++--- 2 files changed, 217 insertions(+), 219 deletions(-) diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr index 47a52973..d9e5465a 100644 --- a/tests/__snapshots__/test_release.ambr +++ b/tests/__snapshots__/test_release.ambr @@ -1,209 +1,205 @@ # serializer version: 1 # name: test_happy_path - _CallList([ - _Call( - '', - tuple( - 'Is this a test release? (y/n): ', - ), - dict({ - }), - ), - _Call( - '', - tuple( - 'Enter the version number: ', - ), - dict({ - }), - ), - _Call( - '', - tuple( - 'Does the build look correct? (y/n): ', + dict({ + 'input': _CallList([ + _Call( + '', + tuple( + 'Is this a test release? (y/n): ', + ), + dict({ + }), ), - dict({ - }), - ), - ]) -# --- -# name: test_happy_path.1 - _CallList([ - _Call( - '', - tuple( - 'dist', + _Call( + '', + tuple( + 'Enter the version number: ', + ), + dict({ + }), ), - dict({ - }), - ), - _Call( - '().__len__', - tuple( + _Call( + '', + tuple( + 'Does the build look correct? (y/n): ', + ), + dict({ + }), ), - dict({ - }), - ), - _Call( - '().__iter__', - tuple( + ]), + 'listdir': _CallList([ + _Call( + '', + tuple( + 'dist', + ), + dict({ + }), ), - dict({ - }), - ), - ]) -# --- -# name: test_happy_path.2 - _CallList([ - _Call( - '', - tuple( - 'https://api.github.com/repos/john-kurkowski/tldextract/releases/generate-notes', + _Call( + '().__len__', + tuple( + ), + dict({ + }), ), - dict({ - 'headers': dict({ - 'Accept': 'application/vnd.github+json', - 'Authorization': 'Bearer fake-token', - 'X-GitHub-Api-Version': '2022-11-28', + _Call( + '().__iter__', + tuple( + ), + dict({ }), - 'json': dict({ - 'tag_name': '5.0.1', + ), + ]), + 'requests': _CallList([ + _Call( + '', + tuple( + 'https://api.github.com/repos/john-kurkowski/tldextract/releases/generate-notes', + ), + dict({ + 'headers': dict({ + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer fake-token', + 'X-GitHub-Api-Version': '2022-11-28', + }), + 'json': dict({ + 'tag_name': '5.0.1', + }), }), - }), - ), - _Call( - '', - tuple( - 'https://api.github.com/repos/john-kurkowski/tldextract/releases', ), - dict({ - 'headers': dict({ - 'Accept': 'application/vnd.github+json', - 'Authorization': 'Bearer fake-token', - 'X-GitHub-Api-Version': '2022-11-28', + _Call( + '', + tuple( + 'https://api.github.com/repos/john-kurkowski/tldextract/releases', + ), + dict({ + 'headers': dict({ + 'Accept': 'application/vnd.github+json', + 'Authorization': 'Bearer fake-token', + 'X-GitHub-Api-Version': '2022-11-28', + }), + 'json': dict({ + 'body': ''' + * Bugfixes + * Indicate MD5 not used in a security context (FIPS compliance) ([#309](https://github.com/john-kurkowski/tldextract/issues/309)) + * Misc. + * Increase typecheck aggression + + **Full Changelog**: fake-body + ''', + 'draft': True, + 'name': '5.0.1', + 'prerelease': False, + 'tag_name': '5.0.1', + }), }), - 'json': dict({ - 'body': ''' - * Bugfixes - * Indicate MD5 not used in a security context (FIPS compliance) ([#309](https://github.com/john-kurkowski/tldextract/issues/309)) - * Misc. - * Increase typecheck aggression - - **Full Changelog**: fake-body - ''', - 'draft': True, - 'name': '5.0.1', - 'prerelease': False, - 'tag_name': '5.0.1', + ), + ]), + 'subprocess': _CallList([ + _Call( + '', + tuple( + list([ + 'git', + 'status', + '--porcelain', + ]), + ), + dict({ + 'capture_output': True, + 'text': True, }), - }), - ), - ]) -# --- -# name: test_happy_path.3 - _CallList([ - _Call( - '', - tuple( - list([ - 'git', - 'status', - '--porcelain', - ]), ), - dict({ - 'capture_output': True, - 'text': True, - }), - ), - _Call( - '', - tuple( - list([ - 'git', - 'tag', - '-a', - '5.0.1', - '-m', - '5.0.1', - ]), + _Call( + '', + tuple( + list([ + 'git', + 'tag', + '-a', + '5.0.1', + '-m', + '5.0.1', + ]), + ), + dict({ + 'check': True, + }), ), - dict({ - 'check': True, - }), - ), - _Call( - '', - tuple( - list([ - 'rm', - '-rf', - PosixPath('dist'), - ]), + _Call( + '', + tuple( + list([ + 'rm', + '-rf', + PosixPath('dist'), + ]), + ), + dict({ + 'check': True, + }), ), - dict({ - 'check': True, - }), - ), - _Call( - '', - tuple( - list([ - 'python', - '-m', - 'build', - ]), + _Call( + '', + tuple( + list([ + 'python', + '-m', + 'build', + ]), + ), + dict({ + 'check': True, + }), ), - dict({ - 'check': True, - }), - ), - _Call( - '', - tuple( - list([ - 'ls', - '-l', - PosixPath('dist'), - ]), + _Call( + '', + tuple( + list([ + 'ls', + '-l', + PosixPath('dist'), + ]), + ), + dict({ + 'check': True, + }), ), - dict({ - 'check': True, - }), - ), - _Call( - '', - tuple( - list([ - 'twine', - 'upload', - '--repository', - 'testpypi', - PosixPath('dist/*'), - ]), + _Call( + '', + tuple( + list([ + 'twine', + 'upload', + '--repository', + 'testpypi', + PosixPath('dist/*'), + ]), + ), + dict({ + 'check': True, + }), ), - dict({ - 'check': True, - }), - ), - _Call( - '', - tuple( - list([ - 'git', - 'push', - '--tags', - 'origin', - 'master', - ]), + _Call( + '', + tuple( + list([ + 'git', + 'push', + '--tags', + 'origin', + 'master', + ]), + ), + dict({ + 'check': True, + }), ), - dict({ - 'check': True, - }), - ), - ]) + ]), + }) # --- -# name: test_happy_path.4 +# name: test_happy_path.1 ''' Version 5.0.1 tag added successfully. Previous dist folder removed successfully. @@ -215,7 +211,7 @@ ''' # --- -# name: test_happy_path.5 +# name: test_happy_path.2 ''' WARNING: dist folder contains incorrect number of files. diff --git a/tests/test_release.py b/tests/test_release.py index 2312aacc..a1bbc92c 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -2,6 +2,7 @@ from __future__ import annotations +import dataclasses import sys from collections.abc import Iterator from typing import Any @@ -13,33 +14,38 @@ from scripts import release -@pytest.fixture -def mock_input() -> Iterator[mock.Mock]: - """Stub reading user input.""" - with mock.patch("builtins.input") as patched: - yield patched +@dataclasses.dataclass(kw_only=True) +class Mocks: + """Collection of all mocked objects used in the release script.""" + input: mock.Mock + listdir: mock.Mock + requests: mock.Mock + subprocess: mock.Mock -@pytest.fixture -def mock_listdir() -> Iterator[mock.Mock]: - """Stub listing directory.""" - with mock.patch("os.listdir") as patched: - yield patched + @property + def mock_calls(self) -> dict[str, Any]: + """A dict of _all_ calls to this class's mock objects.""" + return { + k.name: getattr(self, k.name).mock_calls for k in dataclasses.fields(self) + } @pytest.fixture -def mock_requests() -> Iterator[mock.Mock]: - """Stub network requests.""" - with mock.patch("requests.post") as patched: - yield patched - - -@pytest.fixture -def mock_subprocess() -> Iterator[mock.Mock]: - """Stub running external commands.""" - with mock.patch("subprocess.run") as patched: - patched.return_value.stdout = "" - yield patched +def mocks() -> Iterator[Mocks]: + """Stub reading user input.""" + with ( + mock.patch("builtins.input") as mock_input, + mock.patch("os.listdir") as mock_listdir, + mock.patch("requests.post") as mock_requests, + mock.patch("subprocess.run") as mock_subprocess, + ): + yield Mocks( + input=mock_input, + listdir=mock_listdir, + requests=mock_requests, + subprocess=mock_subprocess, + ) @pytest.mark.skipif( @@ -47,10 +53,7 @@ def mock_subprocess() -> Iterator[mock.Mock]: ) def test_happy_path( capsys: pytest.CaptureFixture[str], - mock_input: mock.Mock, - mock_listdir: mock.Mock, - mock_requests: mock.Mock, - mock_subprocess: mock.Mock, + mocks: Mocks, monkeypatch: pytest.MonkeyPatch, snapshot: SnapshotAssertion, ) -> None: @@ -65,7 +68,7 @@ def test_happy_path( """ monkeypatch.setenv("GITHUB_TOKEN", "fake-token") - mock_input.side_effect = ["y", "5.0.1", "y"] + mocks.input.side_effect = ["y", "5.0.1", "y"] def mock_post(*args: Any, **kwargs: Any) -> mock.Mock: """Return _one_ response JSON that happens to match expectations for multiple requests.""" @@ -78,15 +81,14 @@ def mock_post(*args: Any, **kwargs: Any) -> mock.Mock: ), ) - mock_requests.side_effect = mock_post + mocks.requests.side_effect = mock_post + + mocks.subprocess.return_value.stdout = "" release.main() out, err = capsys.readouterr() - assert mock_input.mock_calls == snapshot - assert mock_listdir.mock_calls == snapshot - assert mock_requests.mock_calls == snapshot - assert mock_subprocess.mock_calls == snapshot + assert mocks.mock_calls == snapshot assert out == snapshot assert err == snapshot From a8ae3bfc5a1ffb902f7ef7d33edebbaeb2e8cb5c Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Fri, 15 Mar 2024 16:00:04 -0700 Subject: [PATCH 72/75] Fix missing `tar` test --- tests/__snapshots__/test_release.ambr | 53 ++++++++++++++++++++------- tests/test_release.py | 2 + 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr index d9e5465a..23354d3b 100644 --- a/tests/__snapshots__/test_release.ambr +++ b/tests/__snapshots__/test_release.ambr @@ -36,20 +36,6 @@ dict({ }), ), - _Call( - '().__len__', - tuple( - ), - dict({ - }), - ), - _Call( - '().__iter__', - tuple( - ), - dict({ - }), - ), ]), 'requests': _CallList([ _Call( @@ -166,6 +152,45 @@ 'check': True, }), ), + _Call( + '', + tuple( + list([ + 'tar', + 'tvf', + PosixPath('dist/archive1'), + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + '', + tuple( + list([ + 'tar', + 'tvf', + PosixPath('dist/archive2'), + ]), + ), + dict({ + 'check': True, + }), + ), + _Call( + '', + tuple( + list([ + 'tar', + 'tvf', + PosixPath('dist/archive3'), + ]), + ), + dict({ + 'check': True, + }), + ), _Call( '', tuple( diff --git a/tests/test_release.py b/tests/test_release.py index a1bbc92c..70ac150d 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -70,6 +70,8 @@ def test_happy_path( mocks.input.side_effect = ["y", "5.0.1", "y"] + mocks.listdir.return_value = ["archive1", "archive2", "archive3"] + def mock_post(*args: Any, **kwargs: Any) -> mock.Mock: """Return _one_ response JSON that happens to match expectations for multiple requests.""" return mock.Mock( From 7f5936cbe05293cb983e062564b3d798a514348e Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Fri, 15 Mar 2024 16:25:39 -0700 Subject: [PATCH 73/75] fixup! DRY --- tests/test_release.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_release.py b/tests/test_release.py index 70ac150d..5cfebd45 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -14,7 +14,7 @@ from scripts import release -@dataclasses.dataclass(kw_only=True) +@dataclasses.dataclass class Mocks: """Collection of all mocked objects used in the release script.""" @@ -33,13 +33,12 @@ def mock_calls(self) -> dict[str, Any]: @pytest.fixture def mocks() -> Iterator[Mocks]: - """Stub reading user input.""" - with ( - mock.patch("builtins.input") as mock_input, - mock.patch("os.listdir") as mock_listdir, - mock.patch("requests.post") as mock_requests, - mock.patch("subprocess.run") as mock_subprocess, - ): + """Stub network and subprocesses.""" + with mock.patch("builtins.input") as mock_input, mock.patch( + "os.listdir" + ) as mock_listdir, mock.patch("requests.post") as mock_requests, mock.patch( + "subprocess.run" + ) as mock_subprocess: yield Mocks( input=mock_input, listdir=mock_listdir, From 9f6a5302ddbe706f827782fb6eb173ec30f4e10c Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Fri, 15 Mar 2024 16:08:44 -0700 Subject: [PATCH 74/75] Unskip tests on Windows --- tests/__snapshots__/test_release.ambr | 12 ++++++------ tests/test_release.py | 18 ++++++++++++++---- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr index 23354d3b..d8268724 100644 --- a/tests/__snapshots__/test_release.ambr +++ b/tests/__snapshots__/test_release.ambr @@ -119,7 +119,7 @@ list([ 'rm', '-rf', - PosixPath('dist'), + 'dist', ]), ), dict({ @@ -145,7 +145,7 @@ list([ 'ls', '-l', - PosixPath('dist'), + 'dist', ]), ), dict({ @@ -158,7 +158,7 @@ list([ 'tar', 'tvf', - PosixPath('dist/archive1'), + 'dist/archive1', ]), ), dict({ @@ -171,7 +171,7 @@ list([ 'tar', 'tvf', - PosixPath('dist/archive2'), + 'dist/archive2', ]), ), dict({ @@ -184,7 +184,7 @@ list([ 'tar', 'tvf', - PosixPath('dist/archive3'), + 'dist/archive3', ]), ), dict({ @@ -199,7 +199,7 @@ 'upload', '--repository', 'testpypi', - PosixPath('dist/*'), + 'dist/*', ]), ), dict({ diff --git a/tests/test_release.py b/tests/test_release.py index 5cfebd45..0ec724cb 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -3,13 +3,14 @@ from __future__ import annotations import dataclasses -import sys from collections.abc import Iterator +from pathlib import Path from typing import Any from unittest import mock import pytest from syrupy.assertion import SnapshotAssertion +from syrupy.matchers import path_type from scripts import release @@ -47,9 +48,18 @@ def mocks() -> Iterator[Mocks]: ) -@pytest.mark.skipif( - sys.platform == "win32", reason="Snapshot paths are different on Windows" -) +@pytest.fixture +def snapshot(snapshot: SnapshotAssertion) -> SnapshotAssertion: + """Override syrupy's snapshot. + + * Simplify platform-dependent `Path` serialization, so the same snapshot + works on multiple platforms. + """ + return snapshot.with_defaults( + matcher=path_type(types=(Path,), replacer=lambda data, _: str(data)) + ) + + def test_happy_path( capsys: pytest.CaptureFixture[str], mocks: Mocks, From e9d64d60cc2ac0966a9195445e936a05ead18262 Mon Sep 17 00:00:00 2001 From: John Kurkowski Date: Fri, 15 Mar 2024 16:50:39 -0700 Subject: [PATCH 75/75] Revert "Unskip tests on Windows" Nice try. This reverts commit 9f6a5302ddbe706f827782fb6eb173ec30f4e10c. --- tests/__snapshots__/test_release.ambr | 12 ++++++------ tests/test_release.py | 18 ++++-------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/tests/__snapshots__/test_release.ambr b/tests/__snapshots__/test_release.ambr index d8268724..23354d3b 100644 --- a/tests/__snapshots__/test_release.ambr +++ b/tests/__snapshots__/test_release.ambr @@ -119,7 +119,7 @@ list([ 'rm', '-rf', - 'dist', + PosixPath('dist'), ]), ), dict({ @@ -145,7 +145,7 @@ list([ 'ls', '-l', - 'dist', + PosixPath('dist'), ]), ), dict({ @@ -158,7 +158,7 @@ list([ 'tar', 'tvf', - 'dist/archive1', + PosixPath('dist/archive1'), ]), ), dict({ @@ -171,7 +171,7 @@ list([ 'tar', 'tvf', - 'dist/archive2', + PosixPath('dist/archive2'), ]), ), dict({ @@ -184,7 +184,7 @@ list([ 'tar', 'tvf', - 'dist/archive3', + PosixPath('dist/archive3'), ]), ), dict({ @@ -199,7 +199,7 @@ 'upload', '--repository', 'testpypi', - 'dist/*', + PosixPath('dist/*'), ]), ), dict({ diff --git a/tests/test_release.py b/tests/test_release.py index 0ec724cb..5cfebd45 100644 --- a/tests/test_release.py +++ b/tests/test_release.py @@ -3,14 +3,13 @@ from __future__ import annotations import dataclasses +import sys from collections.abc import Iterator -from pathlib import Path from typing import Any from unittest import mock import pytest from syrupy.assertion import SnapshotAssertion -from syrupy.matchers import path_type from scripts import release @@ -48,18 +47,9 @@ def mocks() -> Iterator[Mocks]: ) -@pytest.fixture -def snapshot(snapshot: SnapshotAssertion) -> SnapshotAssertion: - """Override syrupy's snapshot. - - * Simplify platform-dependent `Path` serialization, so the same snapshot - works on multiple platforms. - """ - return snapshot.with_defaults( - matcher=path_type(types=(Path,), replacer=lambda data, _: str(data)) - ) - - +@pytest.mark.skipif( + sys.platform == "win32", reason="Snapshot paths are different on Windows" +) def test_happy_path( capsys: pytest.CaptureFixture[str], mocks: Mocks,