""" 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"), })