Files
pipenv/tasks/release.py
T
Kian-Meng Ang 810fb745ae Fix typos
Found via `codespell -S ./peeps,./pipenv/patched,./pipenv/vendor,./pipenv/pipenv.1,./get-pipenv.py,./tests/test_artifacts`
2023-07-23 21:55:03 +08:00

368 lines
12 KiB
Python

import datetime
import os
import pathlib
import re
import subprocess
import sys
import invoke
from parver import Version
from pipenv.__version__ import __version__
from pipenv.vendor.requirementslib.utils import temp_environ
from .vendoring import _get_git_root, drop_dir
VERSION_FILE = "pipenv/__version__.py"
ROOT = pathlib.Path(".").parent.parent.absolute()
PACKAGE_NAME = "pipenv"
def log(msg):
print("[release] %s" % msg)
def get_version_file(ctx):
return _get_git_root(ctx).joinpath(VERSION_FILE)
def find_version(ctx):
version_file = get_version_file(ctx).read_text()
version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
def get_history_file(ctx):
return _get_git_root(ctx).joinpath("HISTORY.txt")
def get_dist_dir(ctx):
return _get_git_root(ctx) / "dist"
def get_build_dir(ctx):
return _get_git_root(ctx) / "build"
def _render_log():
"""Totally tap into Towncrier internals to get an in-memory result."""
rendered = subprocess.check_output(["towncrier", "--draft"]).decode("utf-8")
return rendered
release_help = {
"manual": "Build the man pages.",
"dry_run": "No-op, simulate what would happen if run for real.",
"local": "Build package locally and upload to PyPI.",
"pre": "Build a pre-release version, must be paired with a tag.",
"tag": "A release tag, e.g. 'a', 'b', 'rc', 'post'.",
"month_offset": "How many months to offset the release date by.",
}
@invoke.task(help=release_help)
def release(
ctx,
manual=False,
local=False,
dry_run=False,
pre=False,
tag=None,
month_offset="0",
version=None,
):
trunc_month = False
if pre:
trunc_month = True
drop_dist_dirs(ctx)
if not version:
version = bump_version(
ctx,
dry_run=dry_run,
pre=pre,
tag=tag,
month_offset=month_offset,
trunc_month=trunc_month,
)
version = bump_version(
ctx,
dry_run=dry_run,
pre=pre,
tag=tag,
month_offset=month_offset,
trunc_month=trunc_month,
)
tag_content = _render_log()
if dry_run:
ctx.run("towncrier --draft > CHANGELOG.draft.rst")
log("would remove: news/*")
log("would remove: CHANGELOG.draft.rst")
log("would update: pipenv/pipenv.1")
log(f'Would commit with message: "Release v{version}"')
else:
if pre:
log("generating towncrier draft...")
ctx.run("towncrier --draft > CHANGELOG.draft.rst")
ctx.run(f"git add {get_version_file(ctx).as_posix()}")
else:
ctx.run("towncrier")
ctx.run(f"git add CHANGELOG.rst news/ {get_version_file(ctx).as_posix()}")
log("removing changelog draft if present")
draft_changelog = pathlib.Path("CHANGELOG.draft.rst")
if draft_changelog.exists():
draft_changelog.unlink()
log("generating man files...")
generate_manual(ctx)
ctx.run("git add pipenv/pipenv.1")
ctx.run(f'git commit -m "Release v{version}"')
tag_content = tag_content.replace('"', '\\"')
if dry_run or pre:
log(f"Generated tag content: {tag_content}")
# draft_rstfile = "CHANGELOG.draft.rst"
# markdown_path = pathlib.Path(draft_rstfile).with_suffix(".md")
# generate_markdown(ctx, source_rstfile=draft_rstfile)
# clean_mdchangelog(ctx, markdown_path.as_posix())
# log(f"would generate markdown: {markdown_path.read_text()}")
if not dry_run:
ctx.run(f'git tag -a v{version} -m "Version v{version}\n\n{tag_content}"')
else:
# generate_markdown(ctx)
# clean_mdchangelog(ctx)
ctx.run(f'git tag -a v{version} -m "Version v{version}\n\n{tag_content}"')
if local:
build_dists(ctx)
dist_pattern = f'{PACKAGE_NAME.replace("-", "[-_]")}-*'
artifacts = list(ROOT.joinpath("dist").glob(dist_pattern))
if dry_run:
filename_display = "\n".join(f" {a}" for a in artifacts)
log(f"Would upload dists: {filename_display}")
else:
upload_dists(ctx)
bump_version(ctx, dev=True)
def drop_dist_dirs(ctx):
log("Dropping Dist dir...")
drop_dir(get_dist_dir(ctx))
log("Dropping build dir...")
drop_dir(get_build_dir(ctx))
@invoke.task
def build_dists(ctx):
drop_dist_dirs(ctx)
py_version = ".".join(str(v) for v in sys.version_info[:2])
env = {"PIPENV_PYTHON": py_version}
with ctx.cd(ROOT.as_posix()), temp_environ():
executable = ctx.run(
"python -c 'import sys; print(sys.executable)'", hide=True
).stdout.strip()
log("Building sdist using %s ...." % executable)
os.environ["PIPENV_PYTHON"] = py_version
ctx.run("pipenv install --dev", env=env)
ctx.run("pipenv run pip install -e . --upgrade --upgrade-strategy=eager", env=env)
log("Building wheel using python %s ...." % py_version)
ctx.run("pipenv run python setup.py sdist bdist_wheel", env=env)
@invoke.task(build_dists)
def upload_dists(ctx, repo="pypi"):
dist_pattern = f'{PACKAGE_NAME.replace("-", "[-_]")}-*'
artifacts = list(ROOT.joinpath("dist").glob(dist_pattern))
filename_display = "\n".join(f" {a}" for a in artifacts)
print(f"[release] Will upload:\n{filename_display}")
try:
input("[release] Release ready. ENTER to upload, CTRL-C to abort: ")
except KeyboardInterrupt:
print("\nAborted!")
return
arg_display = " ".join(f'"{n}"' for n in artifacts)
ctx.run(f'twine upload --repository="{repo}" {arg_display}')
@invoke.task
def generate_markdown(ctx, source_rstfile=None):
log("Generating markdown from changelog...")
if source_rstfile is None:
source_rstfile = "CHANGELOG.rst"
source_file = pathlib.Path(source_rstfile)
dest_file = source_file.with_suffix(".md")
ctx.run(
f"pandoc {source_file.as_posix()} -f rst -t markdown -o {dest_file.as_posix()}"
)
@invoke.task
def generate_manual(ctx, commit=False):
log("Generating manual from reStructuredText source...")
ctx.run("make man")
ctx.run("cp docs/_build/man/pipenv.1 pipenv/")
if commit:
log("Committing...")
ctx.run("git add pipenv/pipenv.1")
ctx.run('git commit -m "Update manual page."')
@invoke.task
def generate_contributing_md(ctx, commit=False):
log("Generating CONTRIBUTING.md from reStructuredText source...")
ctx.run("pandoc docs/dev/contributing.rst -f rst -t markdown -o CONTRIBUTING.md")
if commit:
log("Committing...")
ctx.run("git add CONTRIBUTING.md")
ctx.run('git commit -m "Update CONTRIBUTING.md."')
@invoke.task
def generate_changelog(ctx, commit=False, draft=False):
log("Generating changelog...")
if draft:
commit = False
log("Writing draft to file...")
ctx.run("towncrier --draft > CHANGELOG.draft.rst")
else:
ctx.run("towncrier")
if commit:
log("Committing...")
ctx.run("git add CHANGELOG.rst")
ctx.run("git rm CHANGELOG.draft.rst")
ctx.run('git commit -m "Update changelog."')
@invoke.task
def clean_mdchangelog(ctx, filename=None, content=None):
changelog = None
if not content:
if filename is not None:
changelog = pathlib.Path(filename)
else:
changelog = _get_git_root(ctx) / "CHANGELOG.md"
content = changelog.read_text()
content = re.sub(
r"([^\n]+)\n?\s+\[[\\]+(#\d+)\]\(https://github\.com/pypa/[\w\-]+/issues/\d+\)",
r"\1 \2",
content,
flags=re.MULTILINE,
)
if changelog:
changelog.write_text(content)
else:
return content
@invoke.task
def tag_version(ctx, push=False):
version = find_version(ctx)
version = Version.parse(version)
log("Tagging revision: v%s" % version.normalize())
ctx.run("git tag v%s" % version.normalize())
if push:
log("Pushing tags...")
ctx.run("git push origin master")
ctx.run("git push --tags")
def add_one_day(dt):
return dt + datetime.timedelta(days=1)
def date_offset(dt, month_offset=0, day_offset=0, truncate=False):
new_month = (dt.month + month_offset) % 12
year_offset = new_month // 12
replace_args = {
"month": dt.month + month_offset,
"year": dt.year + year_offset,
}
log(
"Getting updated date from date: {} using month offset: {} and year offset {}".format(
dt, new_month, replace_args["year"]
)
)
if day_offset:
dt = dt + datetime.timedelta(days=day_offset)
log(f"updated date using day offset: {day_offset} => {dt}")
if truncate:
log("Truncating...")
replace_args["day"] = 1
return dt.replace(**replace_args)
@invoke.task
def bump_version(
ctx,
dry_run=False,
dev=False,
pre=False,
tag=None,
commit=False,
month_offset="0",
trunc_month=False,
):
current_version = Version.parse(__version__)
current_date = datetime.date(*current_version.release)
today = datetime.date.today()
day_offset = 0
month_offset = int(month_offset)
if month_offset:
# if we are offsetting by a month, grab the first day of the month
trunc_month = True
elif (
current_date == today
and not current_version.is_prerelease
and not current_version.is_release_candidate
):
day_offset = 1
target_day = date_offset(
today, month_offset=month_offset, day_offset=day_offset, truncate=trunc_month
)
log(f"target_day: {target_day}")
target_timetuple = target_day.timetuple()[:3]
new_version = current_version.replace(release=target_timetuple)
if pre and dev:
raise RuntimeError("Can't use 'pre' and 'dev' together!")
if dev:
new_version = new_version.replace(pre=None).bump_dev()
elif pre:
if not tag:
print('Using "pre" requires a corresponding tag.')
return
tag_version = re.match(
r"(?P<tag>alpha|a|beta|b|c|preview|pre|rc)(?P<version>[0-9]+)?", tag
)
tag_dict = tag_version.groupdict()
tag = tag_dict.get("tag", tag)
tag_version = int(tag_dict["version"]) if tag_dict["version"] is not None else 0
if new_version.dev is not None:
new_version = new_version.replace(dev=None)
if new_version.pre_tag:
if new_version.pre_tag != tag:
log(f"Swapping prerelease tag: {new_version.pre_tag} for {tag}")
new_version = new_version.replace(pre_tag=tag, pre=tag_version)
else:
new_version = new_version.replace(pre_tag=tag, pre=tag_version)
if tag_version == 0:
new_version = new_version.bump_pre(tag=tag)
else:
new_version = new_version.replace(pre=None, dev=None)
log("Updating version to %s" % new_version.normalize())
version = find_version(ctx)
log("Found current version: %s" % version)
if dry_run:
log("Would update to: %s" % new_version.normalize())
else:
log("Updating to: %s" % new_version.normalize())
version_file = get_version_file(ctx)
file_contents = version_file.read_text()
version_file.write_text(
file_contents.replace(version, str(new_version.normalize()))
)
if commit:
ctx.run(f"git add {version_file.as_posix()}")
log("Committing...")
ctx.run('git commit -s -m "Bumped version."')
return str(new_version)