Skip to content

Commit 0b20ecf

Browse files
committed
Support passing parameters by overriding default values through environment variables
1 parent 9385fb4 commit 0b20ecf

3 files changed

Lines changed: 144 additions & 37 deletions

File tree

test/common/config_utils.py

Lines changed: 139 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,189 @@
11
import os
2+
import re
23
import threading
3-
from typing import Any, Dict
4+
import logging
5+
from typing import Any, Dict, Optional, Union
46

57
import yaml
68

9+
logger = logging.getLogger(__name__)
10+
11+
12+
def _parse_string_type(value: Any) -> Any:
13+
"""Convert string values to appropriate Python types (bool, int, float)."""
14+
if not isinstance(value, str):
15+
return value
16+
17+
stripped = value.strip()
18+
if not stripped:
19+
return stripped
20+
21+
lower_val = stripped.lower()
22+
23+
# Boolean conversion (support multiple formats)
24+
if lower_val == "true":
25+
return True
26+
if lower_val == "false":
27+
return False
28+
29+
# Numeric conversion
30+
try:
31+
if "." in stripped and stripped.count(".") == 1:
32+
if not stripped.startswith(".") and not stripped.endswith("."):
33+
return float(stripped)
34+
return int(stripped)
35+
except ValueError:
36+
return stripped
37+
738

839
class ConfigUtils:
940
"""
10-
Singleton Configuration Utility
11-
Provides methods to read and access YAML configuration files.
41+
Thread-safe singleton configuration utility.
42+
43+
Features:
44+
- YAML config with environment variable substitution ${VAR:-default}
45+
- Automatic type conversion for string values
46+
- Nested key access using dot notation
1247
"""
1348

14-
_instance = None
15-
_lock = threading.Lock() # Ensure thread-safe singleton creation
49+
_instance: Optional["ConfigUtils"] = None
50+
_lock = threading.Lock()
51+
_init_lock = threading.Lock()
1652

17-
def __init__(self):
18-
self._config = None
53+
ENV_PATTERN = re.compile(r"^\$\{(\w+)(?::-([^}]*))?\}$")
1954

20-
def __new__(cls, config_file: str = None):
21-
# Double-checked locking
55+
def __new__(cls, config_file: Optional[str] = None, **kwargs):
56+
"""Double-checked locking for thread-safe singleton"""
2257
if cls._instance is None:
2358
with cls._lock:
2459
if cls._instance is None:
2560
instance = super().__new__(cls)
26-
instance._init_config(config_file)
61+
instance._config = None
62+
instance.config_file = None
2763
cls._instance = instance
64+
instance._init_config(config_file)
65+
2866
return cls._instance
2967

30-
def _init_config(self, config_file: str = None):
31-
"""Initialize configuration file path and load config"""
68+
def __init__(self):
69+
self._config = None
70+
71+
@classmethod
72+
def get_instance(cls, config_file: Optional[str] = None) -> "ConfigUtils":
73+
"""Get singleton instance"""
74+
return cls(config_file)
75+
76+
def _init_config(self, config_file: Optional[str] = None):
77+
"""Initialize config file path"""
3278
if config_file is None:
3379
current_dir = os.path.dirname(os.path.abspath(__file__))
3480
config_file = os.path.join(current_dir, "..", "config.yaml")
3581

3682
self.config_file = os.path.abspath(config_file)
37-
self._config = None # Lazy load
83+
logger.info(f"Configuration file path set to: {self.config_file}")
84+
85+
def _substitute_env_vars(self, data: Any) -> Any:
86+
"""Recursively substitute environment variables"""
87+
if isinstance(data, dict):
88+
return {k: self._substitute_env_vars(v) for k, v in data.items()}
89+
elif isinstance(data, list):
90+
return [self._substitute_env_vars(item) for item in data]
91+
elif isinstance(data, str):
92+
return self._process_string_value(data)
93+
return data
94+
95+
def _process_string_value(self, value: str) -> Any:
96+
"""Process string: check for env var pattern, then type conversion"""
97+
match = self.ENV_PATTERN.fullmatch(value.strip())
98+
99+
if match:
100+
var_name = match.group(1)
101+
default_val = match.group(2)
102+
103+
env_value = os.getenv(var_name)
104+
if env_value is not None:
105+
return _parse_string_type(env_value.strip())
106+
elif default_val is not None:
107+
return _parse_string_type(default_val.strip())
108+
else:
109+
logger.warning(f"Env var '{var_name}' not found, no default. Keeping: {value}")
110+
return value
111+
else:
112+
return _parse_string_type(value)
38113

39114
def _load_config(self) -> Dict[str, Any]:
40-
"""Internal method to read configuration from file"""
115+
"""Load config from file"""
116+
if not self.config_file:
117+
logger.error("Config file path not initialized")
118+
return {}
119+
120+
if not os.path.exists(self.config_file):
121+
logger.warning(f"Configuration file not found: {self.config_file}")
122+
return {}
123+
41124
try:
42125
with open(self.config_file, "r", encoding="utf-8") as f:
43-
return yaml.safe_load(f) or {}
44-
except FileNotFoundError:
45-
print(f"[WARN] Config file not found: {self.config_file}")
46-
return {}
126+
raw_config = yaml.safe_load(f) or {}
127+
return self._substitute_env_vars(raw_config)
47128
except yaml.YAMLError as e:
48-
print(f"[ERROR] Failed to parse YAML config: {e}")
129+
logger.error(f"YAML parsing error: {e}")
130+
return {}
131+
except Exception as e:
132+
logger.error(f"Error loading config: {e}")
49133
return {}
50134

51135
def read_config(self) -> Dict[str, Any]:
52-
"""Read configuration file (lazy load)"""
136+
"""Lazy load configuration"""
53137
if self._config is None:
54-
self._config = self._load_config()
138+
with self._init_lock:
139+
if self._config is None:
140+
self._config = self._load_config()
55141
return self._config
56142

57-
def reload_config(self):
58-
"""Force reload configuration file"""
59-
self._config = self._load_config()
143+
def reload_config(self) -> Dict[str, Any]:
144+
"""Force reload configuration"""
145+
with self._init_lock:
146+
self._config = self._load_config()
147+
logger.info(f"Configuration reloaded successfully")
148+
return self._config
60149

61150
def get_config(self, key: str, default: Any = None) -> Any:
62-
"""Get top-level configuration item"""
63-
config = self.read_config()
64-
return config.get(key, default)
151+
"""Get top-level config item"""
152+
return self.read_config().get(key, default)
65153

66-
def get_nested_config(self, key_path: str, default: Any = None) -> Any:
67-
"""Get nested configuration, e.g., 'influxdb.host'"""
154+
def get_nested_config(self, key_path: str, default: Any = None, separator: str = ".") -> Any:
155+
"""Get nested config using dot notation (e.g., 'database.host')"""
68156
config = self.read_config()
69-
keys = key_path.split(".")
157+
keys = key_path.split(separator)
70158
value = config
159+
71160
try:
72-
for k in keys:
73-
value = value[k]
161+
for key in keys:
162+
if not isinstance(value, dict):
163+
return default
164+
value = value[key]
74165
return value
75166
except (KeyError, TypeError):
76167
return default
77168

169+
def __getitem__(self, key: str) -> Any:
170+
"""Support config_utils['key']"""
171+
return self.get_config(key)
172+
173+
def __contains__(self, key: str) -> bool:
174+
"""Support 'key' in config_utils"""
175+
return key in self.read_config()
176+
78177

79178
# Global instance
80179
config_utils = ConfigUtils()
81180

82181
if __name__ == "__main__":
83-
print("DataBase config:", config_utils.get_config("database"))
84-
print(
85-
"DataBase host:", config_utils.get_nested_config("database.host", "localhost")
86-
)
182+
print("=== Configuration Test ===")
183+
print(f"Config file path: {config_utils.config_file}")
184+
print(f"LLM Connection: {config_utils.get_config('llm_connection')}")
185+
print(f"EXTRA_INFO: {config_utils.get_nested_config('llm_connection.extra_info')}")
186+
os.environ["LLM_EX_INFO"] = "prefix cache and gsa"
187+
config_utils.reload_config()
188+
print(f"EXTRA_INFO: {config_utils.get_nested_config('llm_connection.extra_info')}")
189+
print(f"Timeout: {config_utils.get_nested_config('llm_connection.timeout')} (type: {type(config_utils.get_nested_config('llm_connection.timeout'))})")

test/config.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ llm_connection:
3131
stream: true # stream output
3232
ignore_eos: true # Ignore the returned terminator
3333
timeout: 180 # request time out
34-
extra_info: "vllm_qwen3-32b_pc-gsa" # extra info, Used to mark different service pull-up parameters
34+
# extra_info: "" # extra info, Used to mark different service pull-up parameters
35+
extra_info: ${LLM_EX_INFO:-only-prefix-cache} # Support passing parameters by overriding default values through environment variables
36+
3537

3638
# Environment Pre-Check Configuration
3739
Env_preCheck:

test/conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def pytest_addoption(parser):
3131
"--platform", action="store", default="", help="Filter by platform marker"
3232
)
3333

34+
def pytest_sessionstart(session):
35+
config_instance.reload_config()
3436

3537
# ---------------- Test Filtering ----------------
3638
def pytest_collection_modifyitems(config, items):

0 commit comments

Comments
 (0)