Coverage for /builds/svesch/cikit/src/cikit/env/common.py: 100%

40 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-10-09 14:17 +0000

1# SPDX-License-Identifier: GPL-3.0-or-later 

2"""Common building blocks for the env package. 

3 

4This module contains shared primitives used across the environment package: 

5- EnvVar: environment variable definition and resolver 

6- current Python version helper 

7- project root and project name discovery helpers 

8 

9Author: 

10 Sven Schlender 

11Date: 

12 2026-02-18 

13""" 

14 

15import sys 

16from dataclasses import dataclass 

17from pathlib import Path 

18from string import Template 

19from typing import Any, Mapping 

20 

21from cikit.errors import CiToolsError 

22from cikit.project_meta import find_project_file, load_project_file 

23 

24 

25@dataclass(frozen=True) 

26class EnvVar: 

27 """Definition of one environment variable. 

28 

29 Resolution behavior: 

30 - Uses the OS environment value if present. 

31 - Otherwise uses the configured default value. 

32 - Evaluates callable defaults at resolution time. 

33 - Expands ``${VAR}`` placeholders from already resolved variables. 

34 - Converts the final value using ``value_type``. 

35 

36 Args: 

37 key: Environment variable name. 

38 doc: Human-readable variable description. 

39 default: Default value or callable returning a default value. 

40 value_type: Callable or type used to convert the resolved value. 

41 secret: Whether the value should be hidden in user-facing output. 

42 """ 

43 key: str 

44 doc: str 

45 default: Any = None 

46 value_type: type[Any] = str 

47 secret: bool = False 

48 

49 def resolve(self, raw_env: Mapping[str, str], current_env: dict[str, Any]) -> Any: 

50 """Resolve this variable from OS environment and already resolved variables. 

51 

52 The resolution order is: 

53 

54 1. If the variable is explicitly set in ``raw_env``, that value is used. 

55 2. Otherwise, the variable's default value is used. 

56 3. String values are expanded using ``string.Template``. 

57 4. The expanded value is converted using ``value_type``. 

58 

59 Args: 

60 raw_env: Raw OS environment mapping. 

61 current_env: Variables already resolved earlier in the definition order. 

62 

63 Returns: 

64 The resolved and type-converted value. 

65 

66 Raises: 

67 CiToolsError: If template substitution fails or type conversion raises 

68 an error. 

69 """ 

70 try: 

71 raw_value = raw_env[self.key] 

72 except KeyError: 

73 raw_value = self.default() if callable(self.default) else self.default 

74 

75 if self.default is None and raw_value == "": 

76 raw_value = None 

77 

78 if isinstance(raw_value, str): 

79 try: 

80 value = Template(raw_value).substitute(current_env) 

81 except KeyError as exp: 

82 missing_key = exp.args[0] if exp.args else "<unknown>" 

83 raise CiToolsError( 

84 f"{self.key}: unresolved template variable {missing_key!r} in {raw_value!r}" 

85 ) from exp 

86 else: 

87 value = raw_value 

88 

89 try: 

90 return None if value is None else self.value_type(value) 

91 except (TypeError, ValueError) as exp: 

92 raise CiToolsError(f"{self.key}: invalid value {value!r}") from exp 

93 

94 

95def current_python_version() -> str: 

96 """Return the current interpreter version as 'major.minor'. 

97 

98 Returns: 

99 The Python version string, for example ``"3.12"``. 

100 """ 

101 return f"{sys.version_info.major}.{sys.version_info.minor}" 

102 

103 

104def find_project_root(start: Path = None) -> Path: 

105 """Return the project root directory for the current workspace. 

106 

107 The project root is defined as the directory containing the nearest 

108 supported project metadata file. Supported project files are 

109 ``cikit.toml`` and ``pyproject.toml``. 

110 

111 Args: 

112 start: Optional start path for the upward search. 

113 

114 Returns: 

115 Path to the detected project root directory. 

116 

117 Raises: 

118 CiToolsError: If no supported project metadata file could be found. 

119 """ 

120 return find_project_file(start).parent 

121 

122 

123def read_project_name(start: Path = None) -> str: 

124 """Return the configured project name from the project metadata file. 

125 

126 The function resolves the active project metadata file using the same 

127 search rules as ``find_project_root()`` and then loads the normalized 

128 project metadata through ``cikit.project_meta``. 

129 

130 Args: 

131 start: Optional start path for the upward search. 

132 

133 Returns: 

134 Configured project name from the resolved project metadata file. 

135 

136 Raises: 

137 CiToolsError: If no supported project metadata file could be found 

138 or the metadata does not contain a valid project name. 

139 """ 

140 project_file = find_project_file(start) 

141 meta = load_project_file(project_file) 

142 

143 return meta["project_name"]