Coverage for src / rhiza_tools / commands / _shared.py: 74%

31 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-23 01:10 +0000

1"""Shared utilities for rhiza-tools commands. 

2 

3This module provides common helpers used across multiple command modules 

4(bump, release, rollback) to avoid duplication and ensure consistency. 

5 

6Utilities: 

7 - COOL_STYLE: Shared questionary styling for interactive prompts 

8 - run_git_command: Execute git commands with standard error handling 

9 - get_current_version: Read the project version from pyproject.toml 

10 - get_current_git_branch: Safely determine the current git branch 

11 - validate_pyproject_exists: Guard against missing pyproject.toml 

12""" 

13 

14import subprocess # nosec B404 - subprocess needed for git operations 

15from pathlib import Path 

16from typing import Any, cast 

17 

18import questionary as qs 

19import tomlkit 

20import typer 

21 

22from rhiza_tools import console 

23 

24COOL_STYLE = qs.Style( 

25 [ 

26 ("separator", "fg:#cc5454"), 

27 ("qmark", "fg:#2FA4A9 bold"), 

28 ("question", ""), 

29 ("selected", "fg:#2FA4A9 bold"), 

30 ("pointer", "fg:#2FA4A9 bold"), 

31 ("highlighted", "fg:#2FA4A9 bold"), 

32 ("answer", "fg:#2FA4A9 bold"), 

33 ("text", "fg:#ffffff"), 

34 ("disabled", "fg:#858585 italic"), 

35 ] 

36) 

37 

38 

39def run_git_command(command: list[str], check: bool = True) -> subprocess.CompletedProcess[str]: 

40 """Run a git command and return the result. 

41 

42 Args: 

43 command: The git command to run as a list of arguments. 

44 check: If True, raise an exception on non-zero exit code. 

45 

46 Returns: 

47 CompletedProcess instance with stdout, stderr, and returncode. 

48 

49 Raises: 

50 subprocess.CalledProcessError: If check=True and command fails. 

51 

52 Example: 

53 >>> result = run_git_command(["git", "status", "--porcelain"]) # doctest: +SKIP 

54 >>> print(result.stdout) # doctest: +SKIP 

55 """ 

56 result = subprocess.run(command, capture_output=True, text=True, check=False) # nosec B603 - git commands are trusted # noqa: S603 

57 if check and result.returncode != 0: 

58 console.error(f"Git command failed: {' '.join(command)}") 

59 console.error(f"Error: {result.stderr}") 

60 raise subprocess.CalledProcessError(result.returncode, command, result.stdout, result.stderr) 

61 return result 

62 

63 

64def get_current_version() -> str: 

65 """Read current version from pyproject.toml. 

66 

67 Returns: 

68 The current version string from the project.version field. 

69 

70 Raises: 

71 typer.Exit: If pyproject.toml cannot be read or parsed. 

72 

73 Example: 

74 >>> version = get_current_version() # doctest: +SKIP 

75 >>> print(version) # doctest: +SKIP 

76 0.1.0 

77 """ 

78 try: 

79 with open("pyproject.toml") as f: 

80 data = tomlkit.parse(f.read()) 

81 project = cast(dict[str, Any], data["project"]) 

82 return str(project["version"]) 

83 except Exception as e: 

84 console.error(f"Failed to read version from pyproject.toml: {e}") 

85 raise typer.Exit(code=1) from None 

86 

87 

88def get_current_git_branch() -> str: 

89 """Get the current git branch name. 

90 

91 This is the safe variant that returns ``"unknown"`` on failure, 

92 suitable for display purposes. For strict validation use 

93 :func:`run_git_command` directly. 

94 

95 Returns: 

96 Current branch name or "unknown" if unable to determine. 

97 """ 

98 result = run_git_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], check=False) 

99 return result.stdout.strip() if result.returncode == 0 else "unknown" 

100 

101 

102def validate_pyproject_exists() -> None: 

103 """Validate that pyproject.toml exists in the current directory. 

104 

105 Raises: 

106 typer.Exit: If pyproject.toml is not found. 

107 """ 

108 if not Path("pyproject.toml").exists(): 

109 console.error("pyproject.toml not found in current directory") 

110 raise typer.Exit(code=1)