# scripts/update_pyproject_versions.py import re import sys from pathlib import Path from typing import List def update_version(file_path_str: str, new_version: str) -> bool: """ Updates the version = "..." line in a single pyproject.toml file. Returns True on success, False on failure for this specific file. """ file_path = Path(file_path_str) print(f"Processing: {file_path}...") # Keep progress on stdout if not file_path.is_file(): print(f"❌ Error: File not found at {file_path}", file=sys.stderr) return False # Indicate failure try: content = file_path.read_text(encoding="utf-8") # Regex to find version = "..." or version = '...' (more robust) # Captures: group 1 = 'version = "', group 2 = '"' (or single quotes) # This finds the line regardless of the current version format (e.g., '1.2.3-dev') version_pattern = r'(version\s*=\s*["\']).*?(["\'])' # Replacement uses the captured quotes (\g<1>, \g<2>) and inserts the new version replacement = rf"\g<1>{new_version}\g<2>" new_content, num_replacements = re.subn( version_pattern, replacement, content, count=1, flags=re.IGNORECASE | re.MULTILINE, ) if num_replacements == 1: # Version found and replaced successfully if new_content != content: file_path.write_text(new_content, encoding="utf-8") print(f'✅ Patched {file_path.name} -> version = "{new_version}"') else: # Regex matched but content didn't change (likely already correct version) print(f'ℹ️ Version in {file_path.name} is already "{new_version}"') return True else: # Version line not found with the expected pattern, attempt to add it print( f"⚠️ Warning: Version line like 'version = \"...\"' not found/matched in {file_path}. Attempting to add.", file=sys.stderr, ) # Define patterns to find the relevant sections, anchored to the start of a line project_pattern = r"^\s*\[project\]" # Prefer PEP 621 standard tool_poetry_pattern = r"^\s*\[tool\.poetry\]" added = False # Try adding under [project] first if re.search(project_pattern, content, re.MULTILINE): target_pattern = project_pattern section_name = "[project]" # Fallback to [tool.poetry] elif re.search(tool_poetry_pattern, content, re.MULTILINE): target_pattern = tool_poetry_pattern section_name = "[tool.poetry]" else: target_pattern = None section_name = None if target_pattern: # Add the version line immediately after the section header new_content = re.sub( target_pattern, rf'\1\nversion = "{new_version}"', # \1 is the matched section header content, count=1, flags=re.MULTILINE, ) if new_content != content: # Check if substitution actually happened file_path.write_text(new_content, encoding="utf-8") print( f'✅ Added version = "{new_version}" under {section_name} in {file_path.name}' ) added = True else: print( f"❌ Error: Failed to insert version under {section_name} in {file_path}.", file=sys.stderr, ) if not added: print( f"❌ Error: Could not find a suitable '[project]' or '[tool.poetry]' section header " f"to add the version line in {file_path}.", file=sys.stderr, ) return False # Indicate failure: Could not find or add return True # Return True if we successfully added the version except Exception as e: print(f"❌ Error processing file {file_path}: {e}", file=sys.stderr) return False # Indicate failure if __name__ == "__main__": if len(sys.argv) < 3: print("❌ Error: Missing arguments.", file=sys.stderr) print( "Usage: python scripts/update_pyproject_versions.py [path/to/pyproject2.toml ...]", file=sys.stderr, ) sys.exit(1) new_version_arg: str = sys.argv[1] file_paths_to_update: List[str] = sys.argv[2:] # Comprehensive Semantic Versioning 2.0.0 regex from https://semver.org/ # (slightly modified to handle common script usage) semver_pattern = r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" if not re.match(semver_pattern, new_version_arg): print( f"❌ Error: Invalid semantic version format provided: '{new_version_arg}'", file=sys.stderr, ) print( " Version must conform to Semantic Versioning 2.0.0 (e.g., 1.2.3, 1.0.0-alpha.1).", file=sys.stderr, ) sys.exit(1) all_successful: bool = True failed_files: List[str] = [] print(f"\n🚀 Attempting to update version to '{new_version_arg}' in files:") for proj_file in file_paths_to_update: print(f" - {proj_file}") print("-" * 30) # Separator for proj_file in file_paths_to_update: if not update_version(proj_file, new_version_arg): all_successful = False failed_files.append(proj_file) # Track failures print("-" * 30) # Separator if not all_successful: print("\n❌ Finished with errors. Failed to update:") for failed_file in failed_files: print(f" - {failed_file}") sys.exit(1) # Exit with error code print("\n✅ All specified pyproject.toml files processed successfully.")