pypiserver/bin/bumpver.py

276 lines
7.3 KiB
Python
Executable File

#!/usr/bin/env python
#
# NEED POSIX (i.e. *Cygwin* on Windows).
"""
Script to bump, commit and tag new versions.
USAGE:
bumpver
bumpver [-n] [-f] [-c] [-a] [-t <message>] <new-ver>
Without <new-ver> prints version extracted from current file.
Don't add a 'v' prefix!
OPTIONS:
-a, --amend Amend current commit for setting the "chore(ver): ..." msg.
-f, --force Bump (and optionally) commit/tag if version exists/is same.
-n, --dry-run Do not write files - just pretend.
-c, --commit Commit afterwardswith a commit-message describing version bump.
-t, --tag=<msg> Adds a signed tag with the given message (commit implied).
- Pre-releases: when working on some version
X.YbN # Beta release
X.YrcN or X.YcN # Release Candidate
X.Y # Final release
- Post-release:
X.YaN.postM # Post-release of an alpha release
X.YrcN.postM # Post-release of a release candidate
- Dev-release:
X.YaN.devM # Developmental release of an alpha release
X.Y.postN.devM # Developmental release of a post-release
EXAMPLE:
bumpver -t 'Mostly model changes' 1.6.2b0
"""
import functools as fnt
import os.path as osp
import re
import sys
from datetime import datetime
import docopt
my_dir = osp.dirname(__file__)
VFILE = osp.join(my_dir, "..", "pypiserver", "__init__.py")
VFILE_regex_version = re.compile(r'version *= *__version__ *= *"([^"]+)"')
VFILE_regex_datetime = re.compile(r'__updated__ *= *"([^"]+)"')
VFILE_regex_date = re.compile(r'__updated__ *= *"([^"\s]+)\s')
RFILE = osp.join(my_dir, "..", "README.md")
PYTEST_ARGS = [osp.join("tests", "test_docs.py")]
class CmdException(Exception):
pass
def get_current_date_info() -> (str, str):
now = datetime.now()
new_datetime = now.strftime("%Y-%m-%d %H:%M:%S%z")
new_date = now.strftime("%Y-%m-%d")
return (new_datetime, new_date)
@fnt.lru_cache()
def read_txtfile(fpath):
with open(fpath, "rt", encoding="utf-8") as fp:
return fp.read()
def extract_file_regexes(fpath, regexes):
"""
:param regexes:
A sequence of regexes to "search", having a single capturing-group.
:return:
One groups per regex, or raise if any regex did not match.
"""
txt = read_txtfile(fpath)
matches = [regex.search(txt) for regex in regexes]
if not all(matches):
raise CmdException(
"Failed extracting current versions with: %s"
"\n matches: %s" % (regexes, matches)
)
return [m.group(1) for m in matches]
def replace_substrings(files, subst_pairs):
for fpath in files:
txt = read_txtfile(fpath)
replacements = []
for old, new in subst_pairs:
replacements.append((old, new, txt.count(old)))
txt = txt.replace(old, new)
yield (txt, fpath, replacements)
def format_syscmd(cmd):
if isinstance(cmd, (list, tuple)):
cmd = " ".join('"%s"' % s if " " in s else s for s in cmd)
else:
assert isinstance(cmd, str), cmd
return cmd
def strip_ver2_commonprefix(ver1, ver2):
cprefix = osp.commonprefix([ver1, ver2])
if cprefix:
striplen = cprefix.rfind(".")
if striplen > 0:
striplen += 1
else:
striplen = len(cprefix)
ver2 = ver2[striplen:]
return ver2
def run_testcases():
import pytest
retcode = pytest.main(PYTEST_ARGS)
if retcode:
raise CmdException(
"Doc TCs failed(%s), probably version-bumping has failed!" % retcode
)
def exec_cmd(cmd):
import subprocess as sbp
err = sbp.call(cmd, stderr=sbp.STDOUT)
if err:
raise CmdException("Failed(%i) on: %s" % (err, format_syscmd(cmd)))
def do_commit(new_ver, old_ver, dry_run, amend, ver_files):
import pathlib
cmt_msg = "chore(ver): bump %s-->%s" % (old_ver, new_ver)
ver_files = [pathlib.Path(f).as_posix() for f in ver_files]
git_add = ["git", "add"] + ver_files
git_cmt = ["git", "commit", "-m", cmt_msg]
if amend:
git_cmt.append("--amend")
commands = [git_add, git_cmt]
for cmd in commands:
cmd_str = format_syscmd(cmd)
if dry_run:
yield "DRYRUN: %s" % cmd_str
else:
yield "EXEC: %s" % cmd_str
exec_cmd(cmd)
def do_tag(tag, tag_msg, dry_run, force):
cmd = ["git", "tag", tag, "-s", "-m", tag_msg]
if force:
cmd.append("--force")
cmd_str = format_syscmd(cmd)
if dry_run:
yield "DRYRUN: %s" % cmd_str
else:
yield "EXEC: %s" % cmd_str
exec_cmd(cmd)
def bumpver(
new_ver, dry_run=False, force=False, amend=False, tag_name_or_commit=None
):
"""
:param tag_name_or_commit:
if true, do `git commit`, if string, also `git tag` with that as msg.
"""
if amend:
## Restore previous version before extracting it.
cmd = "git checkout HEAD~ --".split()
cmd.append(VFILE)
cmd.append(RFILE)
exec_cmd(cmd)
regexes = [VFILE_regex_version, VFILE_regex_datetime, VFILE_regex_date]
old_ver, old_datetime, old_date = extract_file_regexes(VFILE, regexes)
if not new_ver:
yield old_ver
yield old_datetime
yield old_date
else:
if new_ver == old_ver:
msg = "Version '%s'already bumped"
if force:
msg += ", but --force effected."
yield msg % new_ver
else:
msg += "!\n Use of --force recommended."
raise CmdException(msg % new_ver)
new_datetime, new_date = get_current_date_info()
ver_files = [osp.normpath(f) for f in [VFILE, RFILE]]
subst_pairs = [
(old_ver, new_ver),
(old_datetime, new_datetime),
(old_date, new_date),
]
for repl in replace_substrings(ver_files, subst_pairs):
new_txt, fpath, replacements = repl
if not dry_run:
with open(fpath, "wt", encoding="utf-8") as fp:
fp.write(new_txt)
yield "%s: " % fpath
for old, new, nrepl in replacements:
yield " %i x (%24s --> %s)" % (nrepl, old, new)
yield "...now launching DocTCs..."
run_testcases()
if tag_name_or_commit is not None:
yield from do_commit(new_ver, old_ver, dry_run, amend, ver_files)
if isinstance(tag_name_or_commit, str):
tag = "v%s" % new_ver
yield from do_tag(tag, tag_name_or_commit, dry_run, force)
def main(*args):
opts = docopt.docopt(__doc__, argv=args)
new_ver = opts["<new-ver>"]
assert not new_ver or new_ver[0] != "v", (
"Version '%s' must NOT start with `v`!" % new_ver
)
commit = opts["--commit"]
tag = opts["--tag"]
if tag:
tag_name_or_commit = tag
elif commit:
tag_name_or_commit = True
else:
tag_name_or_commit = None
try:
for i in bumpver(
new_ver,
opts["--dry-run"],
opts["--force"],
opts["--amend"],
tag_name_or_commit,
):
print(i)
except CmdException as ex:
sys.exit(str(ex))
except Exception as ex:
print("Unexpected error happened.")
raise ex
if __name__ == "__main__":
main(*sys.argv[1:])