""" Version management utilities. This module provides utilities for working with versions and setuptools-scm. """ import subprocess from pathlib import Path from typing import Optional, Dict, Any from packaging import version class VersionManager: """Utilities for version management with setuptools-scm.""" def __init__(self, project_root: Optional[Path] = None): """Initialize version manager. Args: project_root: Root directory of the project """ self.project_root = project_root or Path.cwd() def get_current_version(self) -> str: """Get current version using setuptools-scm. Returns: Current version string or "unknown" if unavailable """ try: result = subprocess.run( ['python', '-m', 'setuptools_scm'], capture_output=True, text=True, check=True, cwd=self.project_root ) return result.stdout.strip() except subprocess.CalledProcessError: return "unknown" def parse_version(self, version_string: str) -> Dict[str, Any]: """Parse a version string and return components. Args: version_string: Version string to parse Returns: Dictionary with version components """ try: v = version.Version(version_string) return { 'major': v.major, 'minor': v.minor, 'micro': v.micro, 'is_prerelease': v.is_prerelease, 'is_devrelease': v.is_devrelease, 'local': v.local, 'public': v.public, 'base_version': v.base_version, } except version.InvalidVersion: return {'error': f'Invalid version: {version_string}'} def is_development_version(self, version_string: Optional[str] = None) -> bool: """Check if version is a development version. Args: version_string: Version to check. If None, uses current version. Returns: True if development version, False otherwise """ if version_string is None: version_string = self.get_current_version() try: v = version.Version(version_string) return v.is_devrelease or 'dev' in version_string.lower() except version.InvalidVersion: return True # Assume unknown versions are dev def compare_versions(self, version1: str, version2: str) -> int: """Compare two version strings. Args: version1: First version to compare version2: Second version to compare Returns: -1 if version1 < version2, 0 if equal, 1 if version1 > version2 """ try: v1 = version.Version(version1) v2 = version.Version(version2) if v1 < v2: return -1 elif v1 > v2: return 1 else: return 0 except version.InvalidVersion: # Fallback to string comparison if version1 < version2: return -1 elif version1 > version2: return 1 else: return 0 def get_next_version(self, current_version: str, bump_type: str = 'patch') -> str: """Get the next version based on bump type. Args: current_version: Current version string bump_type: Type of bump ('major', 'minor', 'patch') Returns: Next version string Raises: ValueError: If bump_type is invalid """ try: v = version.Version(current_version) major, minor, micro = v.major, v.minor, v.micro if bump_type == 'major': return f"{major + 1}.0.0" elif bump_type == 'minor': return f"{major}.{minor + 1}.0" elif bump_type == 'patch': return f"{major}.{minor}.{micro + 1}" else: raise ValueError(f"Invalid bump type: {bump_type}") except version.InvalidVersion: raise ValueError(f"Cannot parse version: {current_version}") def suggest_version(self, current_version: Optional[str] = None) -> Dict[str, str]: """Suggest next version options. Args: current_version: Current version. If None, gets from setuptools-scm. Returns: Dictionary with version suggestions """ if current_version is None: current_version = self.get_current_version() if current_version == "unknown": return { 'error': 'Cannot determine current version', 'suggestion': 'Consider creating an initial tag like v0.1.0' } try: # Strip development version info to get base v = version.Version(current_version) base_version = v.base_version return { 'current': current_version, 'base': base_version, 'patch': self.get_next_version(base_version, 'patch'), 'minor': self.get_next_version(base_version, 'minor'), 'major': self.get_next_version(base_version, 'major'), } except Exception as e: return { 'error': str(e), 'current': current_version } def validate_version_format(self, version_string: str) -> bool: """Validate if a version string follows semantic versioning. Args: version_string: Version string to validate Returns: True if valid semantic version, False otherwise """ try: version.Version(version_string) return True except version.InvalidVersion: return False def get_version_info(self, project_root: Optional[Path] = None) -> Dict[str, Any]: """Get comprehensive version information for a project. Args: project_root: Root directory of project. If None, uses current directory. Returns: Dictionary with version information """ if project_root: original_root = self.project_root self.project_root = project_root try: current_version = self.get_current_version() # Try to get git information git_info = self._get_git_info() # Parse version components version_parts = self.parse_version(current_version) if current_version != "unknown" else {} return { 'full_version': current_version, 'short_version': current_version.split('.dev')[0] if '.dev' in current_version else current_version, 'version_components': version_parts, 'is_dev': self.is_development_version(current_version), 'git_commit': git_info.get('commit'), 'git_branch': git_info.get('branch'), 'is_git_repo': git_info.get('is_repo', False) } finally: if project_root: self.project_root = original_root def get_release_info(self, project_root: Optional[Path] = None) -> Dict[str, Any]: """Get release information for a project. Args: project_root: Root directory of project. If None, uses current directory. Returns: Dictionary with release information """ from datetime import datetime version_info = self.get_version_info(project_root) return { 'name': 'MarkiTect', 'version': version_info['full_version'], 'short_version': version_info['short_version'], 'is_development': version_info['is_dev'], 'git_branch': version_info.get('git_branch', 'unknown'), 'git_commit': version_info.get('git_commit', 'unknown'), 'build_date': datetime.now().isoformat(), 'python_version': f"{__import__('sys').version_info.major}.{__import__('sys').version_info.minor}.{__import__('sys').version_info.micro}" } def _get_git_info(self) -> Dict[str, Any]: """Get git repository information. Returns: Dictionary with git information """ git_info = {'is_repo': False} try: # Check if in git repo subprocess.run(['git', 'rev-parse', '--git-dir'], cwd=self.project_root, check=True, capture_output=True) git_info['is_repo'] = True # Get branch try: result = subprocess.run(['git', 'rev-parse', '--abbrev-ref', 'HEAD'], cwd=self.project_root, capture_output=True, text=True, check=True) git_info['branch'] = result.stdout.strip() except subprocess.CalledProcessError: git_info['branch'] = 'unknown' # Get commit try: result = subprocess.run(['git', 'rev-parse', 'HEAD'], cwd=self.project_root, capture_output=True, text=True, check=True) git_info['commit'] = result.stdout.strip() except subprocess.CalledProcessError: git_info['commit'] = 'unknown' except subprocess.CalledProcessError: pass # Not a git repo return git_info # Convenience functions for backward compatibility and easy import def get_version_info(project_root: Optional[Path] = None) -> Dict[str, Any]: """Get version information using default VersionManager.""" manager = VersionManager(project_root) return manager.get_version_info() def get_release_info(project_root: Optional[Path] = None) -> Dict[str, Any]: """Get release information using default VersionManager.""" manager = VersionManager(project_root) return manager.get_release_info()