Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport release-1.11] [python] Better version numbers during development #2570

Merged
merged 1 commit into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,5 @@ build:
tools:
python: "3.11"
commands:
- git fetch --unshallow || true
- pip install --upgrade pip
- doc/build.sh -r -V # install deps, don't make a venv
2 changes: 1 addition & 1 deletion apis/python/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,5 +339,5 @@ def run(self):
},
python_requires=">=3.8",
cmdclass={"build_ext": build_ext, "bdist_wheel": bdist_wheel},
version=version.getVersion(),
version=version.get_version(),
)
185 changes: 139 additions & 46 deletions apis/python/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,80 +71,173 @@

import os
import re
import subprocess
import shlex
import sys
from datetime import date
from os.path import basename
from subprocess import DEVNULL, CalledProcessError, check_output
from typing import List, Optional

RELEASE_VERSION_FILE = os.path.join(os.path.dirname(__file__), "RELEASE-VERSION")

# http://www.python.org/dev/peps/pep-0386/
_PEP386_SHORT_VERSION_RE = r"\d+(?:\.\d+)+(?:(?:[abc]|rc)\d+(?:\.\d+)*)?"
_PEP386_VERSION_RE = r"^%s(?:\.post\d+)?(?:\.dev\d+)?$" % _PEP386_SHORT_VERSION_RE
_GIT_DESCRIPTION_RE = r"^(?P<ver>%s)-(?P<commits>\d+)-g(?P<sha>[\da-f]+)$" % (
_PEP386_SHORT_VERSION_RE
_GIT_DESCRIPTION_RE = (
r"^(?P<ver>%s)-(?P<commits>\d+)-g(?P<sha>[\da-f]+)$" % _PEP386_SHORT_VERSION_RE
)


def readGitVersion():
# NOTE: this will fail if on a fork with unsynchronized tags.
# use `git fetch --tags upstream`
# and `git push --tags <your fork>`
try:
proc = subprocess.Popen(
("git", "describe", "--long", "--tags", "--match", "[0-9]*.*"),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
data, stderr = proc.communicate()
if proc.returncode:
return None
ver = data.decode().splitlines()[0].strip()
except Exception:
return None
def err(*args, **kwargs):
"""Print to stderr."""
print(*args, file=sys.stderr, **kwargs)

if not ver:
return None
m = re.search(_GIT_DESCRIPTION_RE, ver)
if not m:
sys.stderr.write(
"version: git description (%s) is invalid, " "ignoring\n" % ver

def lines(*cmd, drop_trailing_newline: bool = True, **kwargs) -> List[str]:
"""Run a command and return its output as a list of lines.

Strip trailing newlines, and drop the last line if it's empty, by default."""
lns = [ln.rstrip("\n") for ln in check_output(cmd, **kwargs).decode().splitlines()]
if lns and drop_trailing_newline and not lns[-1]:
lns.pop()
return lns


def line(*cmd, **kwargs) -> Optional[str]:
"""Run a command, verify exactly one line of stdout, return it."""
lns = lines(*cmd, **kwargs)
if len(lns) != 1:
raise RuntimeError(f"Expected 1 line, found {len(lns)}: {shlex.join(cmd)}")
return lns[0]


def get_latest_tag() -> Optional[str]:
"""Return the most recent local Git tag of the form `[0-9].*.*` (or `None` if none exist)."""
tags = lines("git", "tag", "--list", "--sort=v:refname", "[0-9].*.*")
return tags[-1] if tags else None


def get_latest_remote_tag(remote: str) -> str:
"""Return the most recent Git tag of the form `[0-9].*.*`, from a remote Git repository."""
tags = lines("git", "ls-remote", "--tags", "--sort=v:refname", remote, "[0-9].*.*")
if not tags:
raise RuntimeError(f"No tags found in remote {remote}")
return tags[-1].split(" ")[-1].split("/")[-1]


def get_sha_base10() -> int:
"""Return the current Git SHA, abbreviated and then converted to base 10.

This is unfortunately necessary because PEP440 prohibits hexadecimal characters"""
sha = line("git", "log", "-1", "--format=%h")
return int(sha, 16)


def get_git_version() -> Optional[str]:
"""Construct a PEP440-compatible version string that encodes various Git state.

- If `git describe` returns a plain release tag, use that.
- Otherwise, it will return something like `1.10.2-5-gbabb931f2`, which we'd convert to
`1.10.2.post5.dev50125681138` (abbreviated Git SHA gets converted to base 10, for
PEP440-compliance).
- However, if the `git describe` version starts with `1.5.0`, we do something else. 1.5.0 was
the last release tag before we moved to release branches, so it ends up being returned for
everything on the `main` branch. Instead:
- Find the latest release tag in the local repo (or tracked remote, if there are no local
tags).
- Build a version string from it, e.g. `1.11.1.post0.dev61976836339` (again using the
abbreviated Git SHA, converted to base 10 for PEP440 compliance).
"""
try:
git_version = line(
"git", "describe", "--long", "--tags", "--match", "[0-9]*.*", stderr=DEVNULL
)
return None
except CalledProcessError:
git_version = None

m = re.search(_GIT_DESCRIPTION_RE, git_version) if git_version else None
ver = m.group("ver") if m else None

# `1.5.0` (Nov '23) is typically the most recent tag that's an ancestor of `main`; subsequent
# release tags all exist on release branches (by design).
#
# If `git describe` above returned `1.5.0` as the nearest tagged ancestor, synthesize a
# more meaningful version number below:
#
# 1. Find the latest release tag in the local repo (or tracked remote, if there are no local
# tags, e.g. in case of a shallow clone).
# 2. Return a PEP440-compatible version of the form `A.B.C.post0.devN`, where:
# - `A.B.C` is the most recent release tag in the repo, and
# - `N` is the current short Git SHA, converted to base 10.
if not ver or ver.startswith("1.5.0"):
latest_tag = get_latest_tag()
if latest_tag:
err(f"Git traversal returned {ver}, using latest local tag {latest_tag}")
else:
try:
tracked_branch = line(
"git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"
)
tracked_remote = tracked_branch.split("/")[0]
err(
f"Parsed tracked remote {tracked_remote} from branch {tracked_branch}"
)
except CalledProcessError:
tracked_remote = line("git", "remote")
err(f"Checking tags at default/only remote {tracked_remote}")
latest_tag = get_latest_remote_tag(tracked_remote)
err(
f"Git traversal returned {ver}, using latest tag {latest_tag} from tracked remote {tracked_remote}"
)

commits = int(m.group("commits"))
if not commits:
return m.group("ver")
return f"{latest_tag}.post0.dev{get_sha_base10()}"
else:
return "%s.post%d.dev%d" % (m.group("ver"), commits, int(m.group("sha"), 16))
commits = int(m.group("commits"))
if commits:
sha_base10 = int(m.group("sha"), 16)
return f"{ver}.post{commits}.dev{sha_base10}"
else:
return ver


def readReleaseVersion():
def read_release_version():
try:
fd = open(RELEASE_VERSION_FILE)
try:
with open(RELEASE_VERSION_FILE) as fd:
ver = fd.readline().strip()
finally:
fd.close()
if not re.search(_PEP386_VERSION_RE, ver):
sys.stderr.write(
err(
"version: release version (%s) is invalid, "
"will use it anyway\n" % ver
"will use it anyway\n" % ver,
)
return ver
except FileNotFoundError:
return None


def writeReleaseVersion(version):
fd = open(RELEASE_VERSION_FILE, "w")
fd.write("%s\n" % version)
fd.close()
def generate_cal_version():
today = date.today().strftime("%Y.%m.%d")
return f"{today}.dev{get_sha_base10()}"


def getVersion():
release_version = readReleaseVersion()
version = readGitVersion() or release_version
def write_release_version(version):
with open(RELEASE_VERSION_FILE, "w") as fd:
print(version, file=fd)


def get_version():
release_version = read_release_version()
version = get_git_version()
if not version:
version = release_version
if not version:
raise ValueError("Cannot find the version number")
version = generate_cal_version()
err(
f"No {basename(RELEASE_VERSION_FILE)} or Git version found, using calver {version}"
)
if version != release_version:
writeReleaseVersion(version)
write_release_version(version)
return version


if __name__ == "__main__":
print(get_version())
Loading