# core/config.py
import os
import json
import logging
from pathlib import Path
from typing import Dict, Any, Optional, Union, List, Tuple
import hashlib
import sys
import configparser
import yaml
class CoreConfig:
"""修复递归错误的核心配置管理器"""
_instance = None # 单例实例
def __new__(cls, *args, **kwargs):
"""实现单例模式"""
if cls._instance is None:
cls._instance = super().__new__(cls)
# 在创建实例时初始化关键属性
cls._instance._initialized = False
cls._instance._config = {}
return cls._instance
def __init__(
self,
config_path: Union[str, Path] = None,
env_prefix: str = "AI_SYSTEM_",
default_config: Dict = None,
log_level: str = "INFO"
):
"""初始化配置管理器
修复方案:
1. 在__new__中初始化关键属性
2. 添加明确的初始化状态检查
3. 重构__getattr__方法避免递归
"""
# 确保只初始化一次
if self._initialized:
return
# 标记为已初始化
self._initialized = True
# 1. 存储基本属性
self.env_prefix = env_prefix
self.config_path = Path(config_path) if config_path else None
self.default_config = default_config or {}
# 2. 确定基础目录
self.BASE_DIR = self._determine_base_dir()
# 3. 初始化基本配置
self._config = {
'BASE_DIR': str(self.BASE_DIR),
'LOG_DIR': str(self.BASE_DIR / "logs"),
'CONFIG_DIR': str(self.BASE_DIR / "config"),
'CACHE_DIR': str(self.BASE_DIR / "cache"),
'DATA_DIR': str(self.BASE_DIR / "data"),
'MODEL_DIR': str(self.BASE_DIR / "models"),
'TEMP_DIR': str(self.BASE_DIR / "temp"),
}
# 4. 初始化日志器
self.logger = self._create_safe_logger(log_level)
# 5. 完成初始化
self._initialize()
self.logger.info("[OK] 配置管理器初始化完成 | 环境前缀: %s | 基础目录: %s",
self.env_prefix, self.BASE_DIR)
def _determine_base_dir(self) -> Path:
"""确定基础目录"""
# 1. 尝试环境变量
base_dir_env = os.getenv(f"{self.env_prefix}BASE_DIR")
if base_dir_env:
return Path(base_dir_env).resolve()
# 2. 尝试文件位置
try:
return Path(__file__).parent.parent.resolve()
except Exception:
pass
# 3. 默认当前目录
return Path.cwd().resolve()
def _create_safe_logger(self, log_level: str) -> logging.Logger:
"""创建安全的跨平台日志器"""
logger = logging.getLogger('CoreConfig')
# 设置日志级别
level = getattr(logging, log_level.upper(), logging.INFO)
logger.setLevel(level)
# 移除所有现有处理器
for handler in logger.handlers[:]:
logger.removeHandler(handler)
# 安全控制台处理器
class SafeConsoleHandler(logging.StreamHandler):
def __init__(self, stream=None):
super().__init__(stream)
self.encoding = self._detect_safe_encoding()
def _detect_safe_encoding(self) -> str:
"""检测安全的控制台编码"""
# 尝试UTF-8
if sys.stdout and hasattr(sys.stdout, 'encoding'):
if sys.stdout.encoding and sys.stdout.encoding.lower().startswith('utf'):
return 'utf-8'
# Windows上尝试设置UTF-8代码页
if sys.platform == 'win32':
try:
os.system('chcp 65001 > nul')
return 'utf-8'
except:
pass
# 回退到ASCII
return 'ascii'
def emit(self, record):
try:
msg = self.format(record)
# 替换表情符号为文本
msg = self._sanitize_message(msg)
# 确保正确编码
if isinstance(msg, str):
msg = msg.encode(self.encoding, errors='replace').decode(self.encoding)
self.stream.write(msg + self.terminator)
self.flush()
except Exception:
self.handleError(record)
def _sanitize_message(self, msg: str) -> str:
"""替换或移除不支持的字符"""
replacements = {
'🔄': '[RELOAD]',
'✅': '[OK]',
'⚠️': '[WARN]',
'❌': '[ERROR]',
'📁': '[FOLDER]'
}
for char, replacement in replacements.items():
msg = msg.replace(char, replacement)
return msg
# 创建处理器
console_handler = SafeConsoleHandler()
# 纯文本格式器
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
'%Y-%m-%d %H:%M:%S'
)
console_handler.setFormatter(formatter)
# 添加处理器并禁用传播
logger.addHandler(console_handler)
logger.propagate = False
return logger
def _initialize(self):
"""初始化配置流程"""
try:
self.logger.info("[RELOAD] 加载环境变量...")
self._load_environment()
if self.config_path and self.config_path.exists():
self.logger.info("加载配置文件: %s", self.config_path)
self._load_config_file()
elif self.config_path:
self.logger.warning("[WARN] 配置文件不存在: %s", self.config_path)
self.logger.info("[RELOAD] 合并默认配置...")
self._merge_defaults()
self.logger.info("创建必要目录...")
self._create_directories()
self.logger.info("[OK] 配置加载完成 | 条目数: %d", len(self._config))
except Exception as e:
self.logger.error("[ERROR] 配置初始化失败: %s", str(e), exc_info=True)
def _load_environment(self):
"""加载环境变量到配置"""
for key, value in os.environ.items():
if key.startswith(self.env_prefix):
config_key = key[len(self.env_prefix):]
# 处理嵌套键 (如 DB_HOST → db.host)
if '__' in config_key:
parts = config_key.split('__')
current_level = self._config
for part in parts[:-1]:
part = part.lower()
if part not in current_level:
current_level[part] = {}
current_level = current_level[part]
current_level[parts[-1].lower()] = value
self.logger.debug("加载环境变量: %s = %s", config_key, value)
else:
self._config[config_key.lower()] = value
self.logger.debug("加载环境变量: %s = %s", config_key, value)
def _load_config_file(self):
"""加载配置文件"""
if not self.config_path or not self.config_path.exists():
return
file_type = self.config_path.suffix.lower()
try:
if file_type == '.json':
with open(self.config_path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
elif file_type in ('.yaml', '.yml'):
with open(self.config_path, 'r', encoding='utf-8') as f:
config_data = yaml.safe_load(f)
elif file_type == '.ini':
parser = configparser.ConfigParser()
parser.read(self.config_path, encoding='utf-8')
config_data = {}
for section in parser.sections():
config_data[section] = dict(parser.items(section))
else:
raise ValueError(f"不支持的配置文件类型: {file_type}")
# 合并配置
self._deep_merge(self._config, config_data)
self._last_hash = self._calculate_hash()
self.logger.info("[OK] 配置文件加载成功")
except Exception as e:
self.logger.error("[ERROR] 加载配置文件失败: %s", str(e), exc_info=True)
def _merge_defaults(self):
"""合并默认配置"""
self._deep_merge(self._config, self.default_config)
def _deep_merge(self, target: Dict, source: Dict):
"""深度合并字典"""
for key, value in source.items():
if (key in target and
isinstance(target[key], dict) and
isinstance(value, dict)):
self._deep_merge(target[key], value)
else:
target[key] = value
def _calculate_hash(self) -> str:
"""计算配置哈希值"""
return hashlib.md5(
json.dumps(self._config, sort_keys=True).encode('utf-8')
).hexdigest()
def _create_directories(self):
"""创建配置中指定的目录"""
dir_keys = ['LOG_DIR', 'CACHE_DIR', 'MODEL_DIR', 'DATA_DIR', 'TEMP_DIR']
created_dirs = []
for key in dir_keys:
path_str = self._config.get(key)
if not path_str:
continue
path = Path(path_str)
if not path.is_absolute():
path = self.BASE_DIR / path
if not path.exists():
try:
path.mkdir(parents=True, exist_ok=True)
created_dirs.append(str(path))
except Exception as e:
self.logger.error("[ERROR] 创建目录失败: %s | 错误: %s", path, str(e))
if created_dirs:
self.logger.info("[FOLDER] 创建了 %d 个目录: %s",
len(created_dirs), ", ".join(created_dirs))
def get(self, key: str, default: Any = None) -> Any:
"""安全获取配置值
Args:
key: 配置键 (支持点号分隔的嵌套键)
default: 默认值 (如果键不存在)
Returns:
配置值或默认值
"""
keys = key.split('.')
current = self._config
for k in keys:
if isinstance(current, dict) and k in current:
current = current[k]
else:
return default
return current
def get_path(self, key: str, default: Optional[Union[str, Path]] = None) -> Path:
"""获取路径配置项
Args:
key: 配置键
default: 默认路径 (如果键不存在)
Returns:
绝对路径对象
"""
path_str = self.get(key, default)
if path_str is None:
return self.BASE_DIR / "default"
path = Path(path_str)
if not path.is_absolute():
return self.BASE_DIR / path
return path.resolve()
def get_int(self, key: str, default: int = 0) -> int:
"""获取整数配置值"""
value = self.get(key, default)
try:
return int(value)
except (TypeError, ValueError):
return default
def get_float(self, key: str, default: float = 0.0) -> float:
"""获取浮点数配置值"""
value = self.get(key, default)
try:
return float(value)
except (TypeError, ValueError):
return default
def get_bool(self, key: str, default: bool = False) -> bool:
"""获取布尔值配置值"""
value = self.get(key, default)
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in ('true', 'yes', '1', 'on')
return bool(value)
def get_list(self, key: str, default: list = None, separator: str = ',') -> list:
"""获取列表配置值"""
if default is None:
default = []
value = self.get(key, default)
if isinstance(value, list):
return value
if isinstance(value, str):
return [item.strip() for item in value.split(separator)]
return default
def reload(self):
"""重新加载配置"""
self.logger.info("[RELOAD] 重新加载配置...")
try:
# 保留初始化状态
initialized = self._initialized
self._initialized = False
# 重置配置
self._config = {}
self._initialize()
# 恢复初始化状态
self._initialized = initialized
self.logger.info("[OK] 配置重新加载成功")
return True
except Exception as e:
self.logger.error("[ERROR] 配置重载失败: %s", str(e))
return False
def dump_config(self, file_path: Union[str, Path] = None) -> str:
"""导出当前配置为JSON字符串或文件"""
config_json = json.dumps(self._config, indent=2, ensure_ascii=False)
if file_path:
try:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(config_json)
self.logger.info("[OK] 配置已导出到: %s", file_path)
except Exception as e:
self.logger.error("[ERROR] 导出配置失败: %s", str(e))
return config_json
def __getattr__(self, name: str) -> Any:
"""安全属性访问方法 - 修复递归问题"""
# 1. 检查是否在初始化过程中
if name == '_config' and not hasattr(self, '_config'):
raise AttributeError("_config 属性尚未初始化")
# 2. 检查是否在初始化过程中访问其他属性
if not self._initialized and name != '_initialized':
raise AttributeError(f"配置尚未初始化,无法访问属性: {name}")
# 3. 检查配置字典
if name in self._config:
return self._config[name]
# 4. 检查嵌套属性 (key.subkey)
if '.' in name:
parts = name.split('.')
current = self._config
try:
for part in parts:
if isinstance(current, dict) and part in current:
current = current[part]
else:
break
else: # 所有部分都存在
return current
except TypeError:
pass # 当前值不是字典
# 5. 尝试直接访问实例属性
try:
# 使用object.__getattribute__避免递归
return object.__getattribute__(self, name)
except AttributeError:
# 6. 抛出明确异常
raise AttributeError(
f"配置项 '{name}' 在 {type(self).__name__} 中不存在"
) from None
def __contains__(self, key: str) -> bool:
"""检查配置项是否存在"""
return key in self._config
def __str__(self) -> str:
"""返回配置摘要"""
return f"CoreConfig(entries={len(self._config)}, base_dir={self.BASE_DIR})"
def __len__(self) -> int:
"""返回配置项数量"""
return len(self._config)
def keys(self) -> list:
"""返回所有配置键"""
return list(self._config.keys())
def items(self) -> list:
"""返回所有配置项 (键值对)"""
return list(self._config.items())
最新发布