GPConfigManager 类¶
GPConfigManager 是配置管理的核心类,负责配置文件夹解析、配置加载、类注册和对象创建。
导入¶
类定义¶
class GPConfigManager:
# 类级注册表
_config_classes: dict[str, Type[Any]] = {}
_configurable_classes: dict[str, type[GPConfigurable]] = {}
def __init__(self, project_name: str, cfg_folder: Optional[Path | str] = None):
"""Initialize the configuration manager."""
初始化¶
构造函数参数¶
| 参数 | 类型 | 说明 |
|---|---|---|
project_name |
str |
项目名称,用于环境变量和目录命名 |
cfg_folder |
Path \| str \| None |
可选的配置文件夹路径 |
配置文件夹搜索规则¶
按以下顺序搜索配置文件夹:
- 显式参数 -
cfg_folder参数 - 环境变量 -
{PROJECT_NAME}_CFG_PATH(大写) - 用户目录 -
~/.{project_name}/
示例¶
from pathlib import Path
from gpconfig import GPConfigManager
# 方式 1:显式指定路径
manager = GPConfigManager("myapp", cfg_folder=Path("/etc/myapp"))
# 方式 2:使用环境变量 MYAPP_CFG_PATH
# export MYAPP_CFG_PATH=/path/to/configs
manager = GPConfigManager("myapp")
# 方式 3:使用用户目录
# 配置文件夹: ~/.myapp/
manager = GPConfigManager("myapp")
配置文件夹要求¶
有效的配置文件夹必须:
- 存在且为目录
- 包含 global_env.yaml 文件
- 树内(递归)不存在 stem 含 . 的 .yaml 文件或名字含 . 的子目录
违反这些要求会在构造时抛出 ConfigFolderError(见下方构造函数抛出异常)。
约束:project_name 不能与配置子目录名冲突。
如果
cfg_folder包含一个与project_name同名的顶层子目录,GPConfigManager.__init__会抛出ConfigFolderError。这是因为可选的project_name路径前缀(例如get_config("myapp.x"))会遮蔽该子目录,使其无法通过点号表示法访问。该检查在构造时 执行一次(只扫描一层;空子目录也会触发)。如果遇到此错误,请重命名项目名或子目录。命名规则:
.是路径层级分隔符 —— 名字中的字面.写作@。点号表示法用
.作为层级分隔符(gpdc.core_domain→gpdc/core_domain.yaml)。 stem 含.的.yaml文件(如domain_name.com.yaml)会被list_configs()列出、 却永远无法寻址 ——get_config("domain_name.com")查找的是domain_name/com.yaml。 名字含.的子目录同理。自 v0.5.0 起,GPConfigManager.__init__会递归拒绝这类 名字并抛出ConfigFolderError(报错信息会给出@写法建议)。想使用含
.的名字(典型:互联网域名,如domain_name.com)时,把文件名中的 每个.写作@:
配置名 磁盘文件 寻址路径 domain_name.comgpdc/domain_name@com.yamlgpdc.domain_name@com
@在寻址层是普通透传字符(取其互联网地址user@host的含义),因此domain_name@com.yaml无需任何特殊处理即可使用。只要@在消费方自身的名称规则中 是非法字符,该映射就是单射的。面向消费方库: gpconfig 的
config.name始终返回原始文件 stem (domain_name@com)—— 不做任何转换。如果你的库需要含真实.的名字,请在客户端 自行实现「寻址时转义、加载后还原」(调用get_config/get_object前把.转义为@,消费config.name时把@还原为.)。
构造函数抛出异常:
当满足以下任一条件时,__init__ 抛出 ConfigFolderError:
| 触发条件 | 细节 |
|---|---|
| 找不到有效文件夹 | 三个搜索位置(显式参数、{PROJECT_NAME}_CFG_PATH、~/.{project_name}/)都未能给出有效的配置文件夹。 |
| 文件夹不存在 | 解析出的路径在磁盘上不存在。 |
| 路径不是目录 | 解析出的路径存在但是一个文件,不是目录。 |
缺少 global_env.yaml |
文件夹存在且是目录,但没有 global_env.yaml 文件。 |
| 项目名冲突 | cfg_folder 的某个顶层子目录与 project_name 同名(见上方约束)。 |
| 树内存在含点名字 | cfg_folder 内任何 .yaml 的 stem 或子目录名含 .(点号表示法无法寻址;应写作 @,见上方命名规则)。 |
属性¶
project_name¶
获取项目名称。
cfg_folder¶
获取配置文件夹的完整路径。
global_env¶
以只读的 MappingProxyType 视图(而非可变 dict)获取全局环境配置。
# global_env.yaml 内容:
# version: "1.0.0"
# debug: true
print(manager.global_env["version"]) # "1.0.0"
print(manager.global_env["debug"]) # True
只读 —— 不允许修改。 返回的视图用于保护 manager 的内部状态,并与缓存的快照模型保持一致(参见配置缓存与失效)。任何修改尝试都会抛出异常:
manager.global_env["new_key"] = "value" # TypeError: 'mappingproxy' object does not support item assignment
del manager.global_env["debug"] # TypeError: 'mappingproxy' object does not support item deletion
manager.global_env.pop("debug") # AttributeError: 'mappingproxy' object has no attribute 'pop'
manager.global_env.update({"x": 1}) # AttributeError: 'mappingproxy' object has no attribute 'update'
项操作([k] = v、del)抛出 TypeError;缺失的可变方法(.pop()、.update())抛出 AttributeError。
如需可变副本,请将其物化为普通 dict:
类方法¶
register_config_class()¶
注册配置类(不含可配置对象映射)。
@classmethod
def register_config_class(cls, config_cls: Type[Any]) -> None:
"""Register a config class by its cfg_class_name."""
示例:
from typing import ClassVar
from gpconfig import GPConfig, GPConfigManager
class AppConfig(GPConfig):
cfg_class_name: ClassVar[str] = "AppConfig"
debug: bool = False
# 注册配置类
GPConfigManager.register_config_class(AppConfig)
# 现在可以自动检测并加载
manager = GPConfigManager("myapp")
config = manager.get_config("app") # 自动使用 AppConfig 类
幂等性: 用已存在的 cfg_class_name 重复注册同一个类是静默无操作的。用已被占用的 cfg_class_name 注册不同的类会抛出 RegistrationError。
抛出异常:
| 异常 | 触发条件 |
|---|---|
RegistrationError |
cfg_class_name 已被注册给不同的类。 |
register_configurable_class()¶
注册可配置对象类。只需要传入可配置类本身,系统会通过类名进行查找。
@classmethod
def register_configurable_class(
cls,
configurable_cls: type[GPConfigurable]
) -> None:
"""Register a configurable class by its class name."""
示例:
from typing import ClassVar
from gpconfig import GPConfig, GPConfigurable, GPConfigManager
class DatabaseConfig(GPConfig):
cfg_class_name: ClassVar[str] = "DatabaseConfig"
host: str
port: int = 5432
class Database(GPConfigurable):
def __init__(self, config: DatabaseConfig) -> None:
super().__init__(config)
self.host = config.host
self.port = config.port
# 注册配置类
GPConfigManager.register_config_class(DatabaseConfig)
# 注册可配置类(只需传入类本身)
GPConfigManager.register_configurable_class(Database)
YAML 配置文件:
# database.yaml
cfg_class_name: "DatabaseConfig"
configured_class_name: "Database"
host: localhost
port: 5432
当调用 get_object("database") 时,系统会:
1. 加载配置,读取 cfg_class_name 和 configured_class_name
2. 通过 configured_class_name 在 _configurable_classes 中查找对应的类
3. 使用找到的类创建对象实例
幂等性: 重复注册同一个类是静默无操作的。注册一个与已占用 __name__ 同名的不同类会抛出 RegistrationError。
抛出异常:
| 异常 | 触发条件 |
|---|---|
RegistrationError |
configurable_cls 不是 GPConfigurable 子类,或其 __name__ 已被注册给不同的类。 |
make_new_project_config_folder()¶
创建新的项目配置文件夹。
@classmethod
def make_new_project_config_folder(
cls,
project_name: str,
cfgs: List["GPConfig"],
global_env: Optional[dict] = None,
cfg_folder_path: Optional[str | Path] = None,
) -> Path:
"""Create a new project configuration folder with initial configs."""
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
project_name |
str |
项目名称 |
cfgs |
List[GPConfig] |
GPConfig 实例列表,每个必须有 name |
global_env |
dict \| None |
global_env.yaml 的内容 |
cfg_folder_path |
str \| Path \| None |
可选的配置文件夹路径 |
返回值: 创建的配置文件夹路径
示例:
from typing import ClassVar
from gpconfig import GPConfig, GPConfigManager
class DatabaseConfig(GPConfig):
cfg_class_name: ClassVar[str] = "DatabaseConfig"
host: str = "localhost"
port: int = 5432
class CacheConfig(GPConfig):
cfg_class_name: ClassVar[str] = "CacheConfig"
default_cfg_path: ClassVar[str] = "cache"
host: str = "localhost"
port: int = 6379
# 创建配置实例
db_config = DatabaseConfig()
db_config.name = "database"
cache_config = CacheConfig()
cache_config.name = "redis"
# 创建项目配置文件夹
folder = GPConfigManager.make_new_project_config_folder(
project_name="myapp",
cfgs=[db_config, cache_config],
global_env={"version": "1.0.0", "debug": True}
)
# 结果:
# ~/.myapp/
# ├── global_env.yaml
# ├── database.yaml
# └── cache/
# └── redis.yaml
路径解析规则:
- 显式参数
cfg_folder_path - 环境变量
{PROJECT_NAME}_CFG_PATH - 用户主目录
~/.{project_name}/
异常:
| 异常 | 触发条件 |
|---|---|
ConfigFolderError |
配置文件夹已存在 |
TypeError |
cfgs 中存在非 GPConfig 实例的元素 |
ValueError |
cfgs 中某个配置的 name 为空(文件以 config.name 命名,因此空名字没有有效的保存目标) |
ConfigReadonlyError |
cfgs 中某个配置设置了 readonly=True |
IllegalPathError |
某个配置的 default_cfg_path 含 . 或有空段 |
实例方法¶
get_config()¶
获取配置对象或配置值。
def get_config(
self,
path: str,
config_cls: Optional[Type[T]] = None
) -> Union[T, Any]:
"""Get a config object or a specific config value."""
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
path |
str |
配置路径,支持点号分隔 |
config_cls |
Type[T] \| None |
可选的配置类 |
返回值:
- 如果路径解析到一个文件夹且未传入 config_cls,返回该子文件夹对应的 GPConfigFolder(文件夹优先——见下方文件夹与文件同名冲突)。
- 如果 config_cls 指定或自动检测到,返回配置对象实例
- 如果路径指向特定键,返回该键的值
- 否则返回原始字典
文件夹与文件同名冲突¶
当文件夹与 .yaml 文件同名(例如 services/ 和 services.yaml)时,默认行为是文件夹优先——get_config 返回 GPConfigFolder。有两种方式可以强制文件胜出:
get_config(path, config_cls)—— 传入config_cls即表示"我要的是配置对象,不是文件夹",因此会加载文件。get_object(path)—— 即使存在同名文件夹,也总是读取.yaml文件,因为它需要配置的configured_class_name来构造实例。这是通过get_config的一个私有仅关键字参数(_force_file)实现的。这是一个内部实现细节——不属于公共 API,也不可供用户代码使用;唯一支持的强制文件解析方式是传入config_cls。
抛出:
- 当路径格式错误或逃逸出 cfg_folder 时抛出 IllegalPathError。
- 当格式正确的路径无法解析到已存在的文件或键时抛出 ConfigNotFoundError。
- 当 YAML 文件存在语法错误、同一个 mapping 中包含重复显式键、顶层不是字典,或未通过 Pydantic 校验时抛出 ConfigValidationError。重复键诊断包含键名、磁盘文件路径,以及首次/重复定义的行列号;底层 YAML 错误同时保存在 .original_error 和 .__cause__ 上。
示例:
# 读取 global_env 中的值
debug = manager.get_config("global_env.debug")
version = manager.get_config("global_env.version")
# 读取整个配置文件(自动检测类)
config = manager.get_config("database")
# 读取配置文件(指定类)
config = manager.get_config("database", DatabaseConfig)
# 读取嵌套配置
config = manager.get_config("llm.openai", LLMConfig)
# 读取配置中的特定值
host = manager.get_config("database.host")
model = manager.get_config("llm.anthropic.model")
get_object()¶
从配置路径获取可配置对象实例。系统会从配置实例的 configured_class_name 字段读取要使用的类名,然后在注册表中查找对应的类。
def get_object(self, path: str) -> Any:
"""Get a configurable object instance from a config path."""
示例:
# 注册配置类和可配置类
GPConfigManager.register_config_class(DatabaseConfig)
GPConfigManager.register_configurable_class(Database)
# 创建对象 - 配置文件中需要有 configured_class_name: "Database"
db = manager.get_object("database")
print(db.host) # 配置中的值
# 嵌套配置
llm = manager.get_object("llm.openai")
配置文件示例:
# database.yaml
cfg_class_name: "DatabaseConfig"
configured_class_name: "Database" # 必须设置此项才能使用 get_object()
host: localhost
port: 5432
注意:
- 每次调用都创建新实例
- 配置文件必须包含 configured_class_name 字段
- configured_class_name 对应的类必须已通过 register_configurable_class() 注册
在内部,get_object() 会构造一个 GPConfigurableContext(manager=self, path=<canonical>)
——其中 <canonical> 是源 YAML 的点路径,去掉了 .yaml 后缀和可选的项目名前缀——然后
只调用一次 configurable_cls.from_config(config, context=context)。返回值会与已注册的类
校验;类型不匹配会抛出 ConfigurableConstructionError。默认的 from_config() 只是调用
cls(config),所以标准子类无需任何改动。如需自定义构造——例如从同一配置树中加载关联配置
——请参阅上下文感知构造。
当文件夹与同名 .yaml 文件同时存在时,get_object 总是读取文件(它需要 configured_class_name 来构造实例)。解析规则详见 get_config 下的文件夹与文件同名冲突。
抛出异常:
| 异常 | 触发条件 |
|---|---|
ConfigNotFoundError |
path 无法解析到已存在的配置文件。 |
RegistrationError |
配置没有 configured_class_name;或它被加载为原始 dict(没有注册的配置类);或 configured_class_name 指明的类未通过 register_configurable_class() 注册。 |
ConfigurableConstructionError |
from_config() 返回的对象不是已注册可配置类的实例。 |
list_configs()¶
列出文件夹中的所有配置对象。
示例:
# 列出根目录
items = manager.list_configs()
# ['cache', 'database', 'llm'] (已排序)
# 列出子目录
llm_items = manager.list_configs("llm")
# ['anthropic', 'openai']
# 使用点号分隔
llm_items = manager.list_configs("services.llm")
# ['anthropic', 'openai']
save()¶
保存配置到文件。
⚠️ 明文存储 —— 敏感信息请自行加密。
gpconfig将所有配置值以明文写入 YAML 文件,包括密码、API Key、令牌等字段。本库不提供加密、脱敏或SecretStr处理 —— 这是有意为之的设计,因为依赖 YAML 配置库来保护密钥并不能替代真正的密钥管理方案。如果需要存储敏感信息: - 在放入配置文件之前自行加密(例如使用来自密钥管理服务、环境变量或 KMS 的密钥),并在应用代码中于
gpconfig加载后解密。 - 或者完全不将密钥写入配置文件,改用环境变量或专用密钥存储注入。作为基本防护,请限制
cfg_folder的文件权限,但不要将明文配置文件视为安全的密钥存储。
def save(self, config: "GPConfig", path: Optional[str] = None) -> None:
"""Save a GPConfig instance to a config file."""
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
config |
GPConfig |
要保存的配置实例 |
path |
str \| None |
可选的相对文件夹路径(文件系统风格,以 / 或 \ 分隔)。文件始终命名为 {config.name}.yaml 并位于该文件夹内。不能包含 .。 |
抛出:
- 当 config 不是 GPConfig 实例时抛出 TypeError。
- 当配置设置了 readonly=True 时抛出 ConfigReadonlyError。
- 当 config.name 为空时抛出 ValueError(文件以 config.name 命名,因此空名字没有有效的保存目标)。
- 当路径格式错误或逃逸出 cfg_folder 时抛出 IllegalPathError(例如包含 .,包括 cfg_path 风格、.yaml 后缀或 .. 穿越)。
示例:
# 获取并修改配置
config = manager.get_config("database", DatabaseConfig)
config.port = 5433
# 保存(使用原有路径)
manager.save(config)
# 保存到新文件夹(文件系统风格;'.' 会被拒绝)
# manager.save(config, "backups/database.yaml") # IllegalPathError: 文件夹路径不能包含 '.'
manager.save(config, "backups/db_backups") # OK -> backups/db_backups/{config.name}.yaml
# 保存新配置
new_config = DatabaseConfig(
host="localhost",
username="admin",
password="secret",
database="test"
)
new_config.name = "test"
manager.save(new_config) # 保存到 default_cfg_path/test.yaml
invalidate_cache()¶
使内存中的配置缓存失效,强制下一次 get_config 调用重新从磁盘读取。
参数:
| 参数 | 类型 | 说明 |
|---|---|---|
path |
str \| None |
可选的点分配置路径,指定要失效的文件。如果为 None,则清空整个缓存。 |
示例:
# 清空整个缓存(下一次 get_config 会全部重新加载)
manager.invalidate_cache()
# 清除单个文件的缓存条目
manager.invalidate_cache("database")
# 格式正确但无法解析到文件的路径是空操作(不报错)
manager.invalidate_cache("does.not.exist")
注意: 格式错误的路径会抛出 IllegalPathError;只有格式正确但文件不存在的路径会被静默忽略。
配置缓存与失效¶
GPConfigManager 在内存中缓存配置对象,缓存的生命周期与该 manager 实例一致。一旦通过 get_config 加载了配置文件,后续调用会直接返回缓存的对象,而不会重新从磁盘读取。这适用于整对象访问(get_config("database"))和键路径访问(get_config("database.port"))——两者都会填充缓存。
这是一种**快照(snapshot)**模型:内存中的缓存不会自动检测配置文件的外部更改。
保存配置不会让缓存变旧,无需手动刷新。具体机制取决于调用哪个 save():
GPConfig.save()—— 对于通过GPConfigManager.get_config()获取的实例,你持有的对象就是缓存中的对象本身(同一引用),因此其内存状态早已反映在缓存中;save()只是把该状态落盘。GPConfigManager.save(config)—— 除了写文件之外,还会把该 config 注册进缓存。当你保存一个新构造的GPConfig实例时应使用此方法:它会同时把实例写入配置树并加载进缓存,立即可被get_config读取。
但是,如果配置文件被其他方式修改(手动编辑、其他进程或工具写入),manager 会继续返回过期的缓存值。
要在被外部修改后强制重新加载,请调用 manager.invalidate_cache()(清空整个缓存)或 manager.invalidate_cache(path)(清除单个文件的缓存条目)。下一次 get_config 调用会重新从磁盘读取。
# 第一次访问时填充缓存
config = manager.get_config("database") # 读取磁盘并缓存
# 后续调用命中缓存(无磁盘 I/O)
config2 = manager.get_config("database") # 命中缓存
assert config is config2
# 在缓存失效之前,外部修改不会被看到
manager.invalidate_cache()
config3 = manager.get_config("database") # 再次读取磁盘
GPConfigFolder¶
表示配置文件夹层次结构中的子文件夹。提供对特定文件夹内配置的便捷访问。通过 manager.get_config("<folder>") 获取的实例会被 manager 缓存,因此重复查询同一文件夹会返回同一个 GPConfigFolder 对象。
属性¶
| 属性 | 类型 | 说明 |
|---|---|---|
path |
str |
从 cfg_folder 开始的相对路径,使用点号表示法(如 "llm.providers") |
方法¶
__init__(manager, relative_path)¶
初始化 GPConfigFolder。
| 参数 | 类型 | 说明 |
|---|---|---|
manager |
GPConfigManager |
GPConfigManager 实例 |
relative_path |
str |
从 cfg_folder 开始的相对路径,使用点号表示法 |
get_config(path, config_cls=None)¶
从此文件夹获取配置对象或值。
| 参数 | 类型 | 说明 |
|---|---|---|
path |
str |
相对于此文件夹的配置路径 |
config_cls |
Type[T] |
可选的 GPConfig 子类,用于加载 |
返回值: GPConfig 实例、GPConfigFolder 或配置值。
get_object(path)¶
从此文件夹获取可配置对象实例。
| 参数 | 类型 | 说明 |
|---|---|---|
path |
str |
相对于此文件夹的配置路径 |
返回值: 配置的 GPConfigurable 子类的新实例。
list_configs()¶
列出此文件夹中的所有配置对象。
返回值: List[str] - 对象名称列表(配置名称和子文件夹名称)。
__repr__()¶
用于调试的简洁表示,例如 GPConfigFolder(relative_path='services.llm')。