Initial commit

This commit is contained in:
Miguel Sozinho Ramalho
2022-06-17 13:25:27 +01:00
commit b3c7ac8e5d
45 changed files with 1726 additions and 0 deletions

175
scripts/personalize.py Normal file
View File

@@ -0,0 +1,175 @@
"""
Run this script once after first creating your project from this template repo to personalize
it for own project.
This script is interactive and will prompt you for various inputs.
"""
from pathlib import Path
from typing import Generator, List, Tuple
import click
from click_help_colors import HelpColorsCommand
from rich import print
from rich.markdown import Markdown
from rich.prompt import Confirm
from rich.syntax import Syntax
from rich.traceback import install
install(show_locals=True, suppress=[click])
REPO_BASE = (Path(__file__).parent / "..").resolve()
FILES_TO_REMOVE = {
REPO_BASE / ".github" / "workflows" / "setup.yml",
REPO_BASE / "setup-requirements.txt",
REPO_BASE / "scripts" / "personalize.py",
}
PATHS_TO_IGNORE = {
REPO_BASE / "README.md",
REPO_BASE / ".git",
REPO_BASE / "docs" / "source" / "_static" / "favicon.ico",
}
GITIGNORE_LIST = [
line.strip()
for line in (REPO_BASE / ".gitignore").open().readlines()
if line.strip() and not line.startswith("#")
]
REPO_NAME_TO_REPLACE = "python-package-template"
BASE_URL_TO_REPLACE = "https://github.com/allenai/python-package-template"
@click.command(
cls=HelpColorsCommand,
help_options_color="green",
help_headers_color="yellow",
context_settings={"max_content_width": 115},
)
@click.option(
"--github-org",
prompt="GitHub organization or user (e.g. 'allenai')",
help="The name of your GitHub organization or user.",
)
@click.option(
"--github-repo",
prompt="GitHub repository (e.g. 'python-package-template')",
help="The name of your GitHub repository.",
)
@click.option(
"--package-name",
prompt="Python package name (e.g. 'my-package')",
help="The name of your Python package.",
)
@click.option(
"-y",
"--yes",
is_flag=True,
help="Run the script without prompting for a confirmation.",
default=False,
)
@click.option(
"--dry-run",
is_flag=True,
hidden=True,
default=False,
)
def main(
github_org: str, github_repo: str, package_name: str, yes: bool = False, dry_run: bool = False
):
repo_url = f"https://github.com/{github_org}/{github_repo}"
package_actual_name = package_name.replace("_", "-")
package_dir_name = package_name.replace("-", "_")
# Confirm before continuing.
print(f"Repository URL set to: [link={repo_url}]{repo_url}[/]")
print(f"Package name set to: [cyan]{package_actual_name}[/]")
if not yes:
yes = Confirm.ask("Is this correct?")
if not yes:
raise click.ClickException("Aborted, please run script again")
# Delete files that we don't need.
for path in FILES_TO_REMOVE:
assert path.is_file(), path
if not dry_run:
path.unlink()
else:
print(f"Removing {path}")
# Personalize remaining files.
replacements = [
(BASE_URL_TO_REPLACE, repo_url),
(REPO_NAME_TO_REPLACE, github_repo),
("my-package", package_actual_name),
("my_package", package_dir_name),
]
if dry_run:
for old, new in replacements:
print(f"Replacing '{old}' with '{new}'")
for path in iterfiles(REPO_BASE):
personalize_file(path, dry_run, replacements)
# Rename 'my_package' directory to `package_dir_name`.
if not dry_run:
(REPO_BASE / "my_package").replace(REPO_BASE / package_dir_name)
else:
print(f"Renaming 'my_package' directory to '{package_dir_name}'")
# Start with a fresh README.
readme_contents = f"""# {package_actual_name}\n"""
if not dry_run:
with open(REPO_BASE / "README.md", "w+t") as readme_file:
readme_file.write(readme_contents)
else:
print("Replacing README.md contents with:\n", Markdown(readme_contents))
install_example = Syntax("pip install -e '.[dev]'", "bash")
print(
"[green]\N{check mark} Success![/] You can now install your package locally in development mode with:\n",
install_example,
)
def iterfiles(dir: Path) -> Generator[Path, None, None]:
assert dir.is_dir()
for path in dir.iterdir():
if path in PATHS_TO_IGNORE:
continue
is_ignored_file = False
for gitignore_entry in GITIGNORE_LIST:
if path.relative_to(REPO_BASE).match(gitignore_entry):
is_ignored_file = True
break
if is_ignored_file:
continue
if path.is_dir():
yield from iterfiles(path)
else:
yield path
def personalize_file(path: Path, dry_run: bool, replacements: List[Tuple[str, str]]):
with path.open("r+t") as file:
filedata = file.read()
should_update: bool = False
for old, new in replacements:
if filedata.count(old):
should_update = True
filedata = filedata.replace(old, new)
if should_update:
if not dry_run:
with path.open("w+t") as file:
file.write(filedata)
else:
print(f"Updating {path}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,39 @@
from datetime import datetime
from pathlib import Path
from my_package.version import VERSION
def main():
changelog = Path("CHANGELOG.md")
with changelog.open() as f:
lines = f.readlines()
insert_index: int = -1
for i in range(len(lines)):
line = lines[i]
if line.startswith("## Unreleased"):
insert_index = i + 1
elif line.startswith(f"## [v{VERSION}]"):
print("CHANGELOG already up-to-date")
return
elif line.startswith("## [v"):
break
if insert_index < 0:
raise RuntimeError("Couldn't find 'Unreleased' section")
lines.insert(insert_index, "\n")
lines.insert(
insert_index + 1,
f"## [v{VERSION}](https://github.com/allenai/python-package-template/releases/tag/v{VERSION}) - "
f"{datetime.now().strftime('%Y-%m-%d')}\n",
)
with changelog.open("w") as f:
f.writelines(lines)
if __name__ == "__main__":
main()

19
scripts/release.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash
set -e
TAG=$(python -c 'from my_package.version import VERSION; print("v" + VERSION)')
read -p "Creating new release for $TAG. Do you want to continue? [Y/n] " prompt
if [[ $prompt == "y" || $prompt == "Y" || $prompt == "yes" || $prompt == "Yes" ]]; then
python scripts/prepare_changelog.py
git add -A
git commit -m "Bump version to $TAG for release" || true && git push
echo "Creating new git tag $TAG"
git tag "$TAG" -m "$TAG"
git push --tags
else
echo "Cancelled"
exit 1
fi

78
scripts/release_notes.py Executable file
View File

@@ -0,0 +1,78 @@
# encoding: utf-8
"""
Prepares markdown release notes for GitHub releases.
"""
import os
from typing import List, Optional
import packaging.version
TAG = os.environ["TAG"]
ADDED_HEADER = "### Added 🎉"
CHANGED_HEADER = "### Changed ⚠️"
FIXED_HEADER = "### Fixed ✅"
REMOVED_HEADER = "### Removed 👋"
def get_change_log_notes() -> str:
in_current_section = False
current_section_notes: List[str] = []
with open("CHANGELOG.md") as changelog:
for line in changelog:
if line.startswith("## "):
if line.startswith("## Unreleased"):
continue
if line.startswith(f"## [{TAG}]"):
in_current_section = True
continue
break
if in_current_section:
if line.startswith("### Added"):
line = ADDED_HEADER + "\n"
elif line.startswith("### Changed"):
line = CHANGED_HEADER + "\n"
elif line.startswith("### Fixed"):
line = FIXED_HEADER + "\n"
elif line.startswith("### Removed"):
line = REMOVED_HEADER + "\n"
current_section_notes.append(line)
assert current_section_notes
return "## What's new\n\n" + "".join(current_section_notes).strip() + "\n"
def get_commit_history() -> str:
new_version = packaging.version.parse(TAG)
# Get all tags sorted by version, latest first.
all_tags = os.popen("git tag -l --sort=-version:refname 'v*'").read().split("\n")
# Out of `all_tags`, find the latest previous version so that we can collect all
# commits between that version and the new version we're about to publish.
# Note that we ignore pre-releases unless the new version is also a pre-release.
last_tag: Optional[str] = None
for tag in all_tags:
if not tag.strip(): # could be blank line
continue
version = packaging.version.parse(tag)
if new_version.pre is None and version.pre is not None:
continue
if version < new_version:
last_tag = tag
break
if last_tag is not None:
commits = os.popen(f"git log {last_tag}..{TAG}^ --oneline --first-parent").read()
else:
commits = os.popen("git log --oneline --first-parent").read()
return "## Commits\n\n" + commits
def main():
print(get_change_log_notes())
print(get_commit_history())
if __name__ == "__main__":
main()