From be3b4e3aae7439948090b92729df496b82092929 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 13 Feb 2026 17:22:38 +0100 Subject: [PATCH] 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 --- markitect/__version__.py | 193 +++++++++++++++++++++++---------------- 1 file changed, 115 insertions(+), 78 deletions(-) diff --git a/markitect/__version__.py b/markitect/__version__.py index e4c5b0cb..6c9e7a67 100644 --- a/markitect/__version__.py +++ b/markitect/__version__.py @@ -1,109 +1,146 @@ """ Version information for MarkiTect. -This module provides version information using setuptools-scm. -Version is automatically derived from git tags. +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 .``. """ -try: - from ._version import version as __version__ -except ImportError: - # Fallback when _version.py is not available (e.g., during development without setuptools-scm) - __version__ = "unknown" +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 get_version_info(): - """Get comprehensive version information by delegating to release-management capability.""" - try: - # Delegate to release-management capability - from pathlib import Path - project_root = Path(__file__).parent.parent +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: - from release_management.utils.version import get_version_info as rm_get_version_info - return rm_get_version_info(project_root) - except ImportError: - # Fallback if release-management capability is not available - pass - except Exception: + 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 - # Simple fallback implementation - try: - from ._version import version_tuple, commit_id - except ImportError: - version_tuple = ("unknown",) - commit_id = "unknown" + git = _git_info() + is_dev = ".dev" in __version__ return { - 'full_version': __version__, - 'short_version': __version__.split('.dev')[0] if '.dev' in __version__ else __version__, - 'version_tuple': version_tuple, - 'commit_id': commit_id, - 'is_dev': '.dev' in __version__, - 'git_commit': commit_id, - 'git_branch': 'unknown', - 'is_git_repo': False + "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 # already in expected format + if "full_version" in raw: + return raw - import subprocess - - version = raw.get('version', 'unknown') - is_dev = raw.get('is_development', '.dev' in version) - commit = raw.get('git_commit', 'unknown') - - # Detect git repo and current tag - is_git_repo = False - git_tag = None - try: - subprocess.check_output(['git', 'rev-parse', '--git-dir'], stderr=subprocess.DEVNULL) - is_git_repo = True - tag_out = subprocess.check_output( - ['git', 'describe', '--tags', '--exact-match', 'HEAD'], - stderr=subprocess.DEVNULL, - ).decode().strip() - if tag_out: - git_tag = tag_out - except (subprocess.CalledProcessError, FileNotFoundError): - pass + 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 is_git_repo else 'source', - 'commit': commit, - 'clean_build': not is_dev, - 'is_git_repo': is_git_repo, - 'git_tag': git_tag, + "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 by delegating to release-management capability.""" + """Get release information.""" try: - from pathlib import Path - project_root = Path(__file__).parent.parent - - 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: - pass - except Exception: + 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 - # Fallback — build from version_info directly - version_info = get_version_info() + info = get_version_info() return _normalize_release_info({ - 'version': version_info['full_version'], - 'is_development': version_info['is_dev'], - 'git_commit': version_info.get('git_commit', 'unknown'), - }) \ No newline at end of file + "version": info["full_version"], + "is_development": info["is_dev"], + "git_commit": info.get("git_commit", "unknown"), + })