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
« 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.
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
9Author:
10 Sven Schlender
11Date:
12 2026-02-18
13"""
15import sys
16from dataclasses import dataclass
17from pathlib import Path
18from string import Template
19from typing import Any, Mapping
21from cikit.errors import CiToolsError
22from cikit.project_meta import find_project_file, load_project_file
25@dataclass(frozen=True)
26class EnvVar:
27 """Definition of one environment variable.
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``.
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
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.
52 The resolution order is:
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``.
59 Args:
60 raw_env: Raw OS environment mapping.
61 current_env: Variables already resolved earlier in the definition order.
63 Returns:
64 The resolved and type-converted value.
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
75 if self.default is None and raw_value == "":
76 raw_value = None
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
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
95def current_python_version() -> str:
96 """Return the current interpreter version as 'major.minor'.
98 Returns:
99 The Python version string, for example ``"3.12"``.
100 """
101 return f"{sys.version_info.major}.{sys.version_info.minor}"
104def find_project_root(start: Path = None) -> Path:
105 """Return the project root directory for the current workspace.
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``.
111 Args:
112 start: Optional start path for the upward search.
114 Returns:
115 Path to the detected project root directory.
117 Raises:
118 CiToolsError: If no supported project metadata file could be found.
119 """
120 return find_project_file(start).parent
123def read_project_name(start: Path = None) -> str:
124 """Return the configured project name from the project metadata file.
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``.
130 Args:
131 start: Optional start path for the upward search.
133 Returns:
134 Configured project name from the resolved project metadata file.
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)
143 return meta["project_name"]