Files
markitect-main/markitect/__version__.py
tegwick be3b4e3aae fix(version): resolve version dynamically from git in dev checkouts
When running from a git repo, use setuptools-scm at runtime to derive
the version from tags. Falls back to the static _version.py only when
not in a git repo (e.g. installed from wheel). This ensures
`markitect version` stays correct without requiring `pip install -e .`
after every tag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:22:38 +01:00

147 lines
4.2 KiB
Python

"""
Version information for MarkiTect.
Uses setuptools-scm for version derivation. When running from a dev
checkout (git repo present), the version is resolved dynamically from
git tags so it stays correct even without ``pip install -e .``.
"""
import subprocess
from pathlib import Path
_PROJECT_ROOT = Path(__file__).parent.parent
def _git_version() -> str | None:
"""Derive version from git via setuptools-scm (runtime, no install needed)."""
try:
from setuptools_scm import get_version
return get_version(root=str(_PROJECT_ROOT))
except Exception:
return None
def _static_version() -> str:
"""Read the version baked into _version.py at install time."""
try:
from ._version import version
return version
except ImportError:
return "unknown"
def _is_git_repo() -> bool:
try:
subprocess.check_output(
["git", "rev-parse", "--git-dir"],
cwd=_PROJECT_ROOT,
stderr=subprocess.DEVNULL,
)
return True
except (subprocess.CalledProcessError, FileNotFoundError):
return False
def _resolve_version() -> str:
"""Pick the best available version string.
In a git checkout, setuptools-scm gives the authoritative answer
(reflects tags even before reinstall). Otherwise fall back to the
static _version.py produced at install time.
"""
if _is_git_repo():
v = _git_version()
if v:
return v
return _static_version()
__version__ = _resolve_version()
def get_version():
"""Get the current version string."""
return __version__
def _git_info() -> dict:
"""Gather git metadata (commit, branch, tag)."""
info: dict = {"is_git_repo": False}
if not _is_git_repo():
return info
info["is_git_repo"] = True
def _run(*args: str) -> str | None:
try:
return subprocess.check_output(
["git", *args],
cwd=_PROJECT_ROOT,
stderr=subprocess.DEVNULL,
).decode().strip()
except (subprocess.CalledProcessError, FileNotFoundError):
return None
info["git_commit"] = _run("rev-parse", "--short", "HEAD") or "unknown"
info["git_branch"] = _run("rev-parse", "--abbrev-ref", "HEAD") or "unknown"
info["git_tag"] = _run("describe", "--tags", "--exact-match", "HEAD")
return info
def get_version_info():
"""Get comprehensive version information."""
try:
from release_management.utils.version import get_version_info as rm_get_version_info
return rm_get_version_info(_PROJECT_ROOT)
except (ImportError, Exception):
pass
git = _git_info()
is_dev = ".dev" in __version__
return {
"full_version": __version__,
"short_version": __version__.split(".dev")[0] if is_dev else __version__,
"is_dev": is_dev,
"is_git_repo": git.get("is_git_repo", False),
"git_commit": git.get("git_commit", "unknown"),
"git_branch": git.get("git_branch", "unknown"),
"git_tag": git.get("git_tag"),
}
def _normalize_release_info(raw):
"""Ensure release info dict has the keys the CLI release command expects."""
if "full_version" in raw:
return raw
version = raw.get("version", "unknown")
is_dev = raw.get("is_development", ".dev" in version)
commit = raw.get("git_commit", "unknown")
git = _git_info()
return {
"full_version": version,
"release_type": "development" if is_dev else "release",
"build_from": "git" if git.get("is_git_repo") else "source",
"commit": commit,
"clean_build": not is_dev,
"is_git_repo": git.get("is_git_repo", False),
"git_tag": git.get("git_tag"),
}
def get_release_info():
"""Get release information."""
try:
from release_management.utils.version import get_release_info as rm_get_release_info
return _normalize_release_info(rm_get_release_info(_PROJECT_ROOT))
except (ImportError, Exception):
pass
info = get_version_info()
return _normalize_release_info({
"version": info["full_version"],
"is_development": info["is_dev"],
"git_commit": info.get("git_commit", "unknown"),
})