#!/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 ] Without 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= 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[""] 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:])