跳转至

异常类

gpconfig 定义了一组自定义异常类,用于处理配置管理过程中的各种错误情况。

导入

from gpconfig import (
    GPConfigError,
    ConfigFolderError,
    ConfigNotFoundError,
    IllegalPathError,
    ConfigReadonlyError,
    RegistrationError,
    ConfigurableConstructionError,
    ConfigValidationError,
)

异常层次

GPConfigError (基类)
├── ConfigFolderError
├── ConfigNotFoundError
├── IllegalPathError
├── ConfigReadonlyError
├── RegistrationError
├── ConfigurableConstructionError
└── ConfigValidationError

GPConfigError

所有 gpconfig 异常的基类。

class GPConfigError(Exception):
    """Base exception for all gpconfig errors."""
    pass

用法

捕获所有 gpconfig 相关异常:

from gpconfig import GPConfigError, GPConfigManager

try:
    manager = GPConfigManager("myapp")
    config = manager.get_config("database")
except GPConfigError as e:
    print(f"配置错误: {e}")

ConfigFolderError

配置文件夹未找到或无效时抛出。

class ConfigFolderError(GPConfigError):
    """Raised when the config folder cannot be found or is invalid."""
    pass

触发场景

  • 配置文件夹不存在
  • 配置路径不是目录
  • 配置文件夹缺少 global_env.yaml

示例

from gpconfig import GPConfigManager, ConfigFolderError
from pathlib import Path

try:
    # 文件夹不存在
    manager = GPConfigManager("myapp", cfg_folder=Path("/nonexistent"))
except ConfigFolderError as e:
    print(f"配置文件夹错误: {e}")
    # 输出: Config folder does not exist: /nonexistent (from explicit parameter)

try:
    # 缺少 global_env.yaml
    manager = GPConfigManager("myapp", cfg_folder=Path("/tmp"))
except ConfigFolderError as e:
    print(f"配置文件夹错误: {e}")
    # 输出: Config folder must contain global_env.yaml: /tmp (from explicit parameter)

ConfigNotFoundError

请求的配置路径不存在时抛出。

class ConfigNotFoundError(GPConfigError):
    """Raised when a requested config path doesn't exist."""

    def __init__(self, path: str, message: str = ""):
        self.path = path
        super().__init__(message or f"Config not found: {path}")

属性

属性 类型 说明
path str 未找到的配置路径

示例

from gpconfig import GPConfigManager, ConfigNotFoundError

manager = GPConfigManager("myapp")

try:
    config = manager.get_config("nonexistent")
except ConfigNotFoundError as e:
    print(f"未找到配置: {e.path}")
    print(f"错误信息: {e}")

try:
    value = manager.get_config("database.nonexistent_key")
except ConfigNotFoundError as e:
    print(f"未找到键: {e.path}")

IllegalPathError

配置路径格式错误或逃逸出 cfg_folder(纵深防御 containment)时抛出。

class IllegalPathError(GPConfigError):
    """Raised when a config path is malformed or escapes the cfg_folder."""

    def __init__(self, path: str, message: str = ""):
        self.path = path
        super().__init__(message or f"Illegal config path: {path}")

触发场景

当路径满足以下任一条件时被视为格式错误并抛出 IllegalPathError:

  • 为空 —— 例如 ""(以前被当作引用根文件夹的可容忍方式)。
  • 仅由点组成 —— 例如 "."、".."。
  • 包含连续的点 —— 例如 "a..b"。
  • 以点开头或结尾 —— 例如 ".x"、"x."(以点结尾以前会因 bug 而返回整个 global_env 字典)。
  • 在 cfg_path 风格中包含字面量 / 或 \ —— cfg_path 使用点号表示法;出现原始分隔符即表示路径格式错误。

此外,即使语法上合法,如果路径解析后位于 cfg_folder 之外,也会抛出 IllegalPathError。该 containment 检查是一项纵深防御保证,确保没有任何路径能逃逸出受管文件夹。

属性

属性 类型 说明
path str 被拒绝的违规路径字符串

抛出位置

当 GPConfigManager.get_config()、get_object()、list_configs() 和 save() 遇到格式错误或逃逸的路径时,都会抛出 IllegalPathError。

示例

from gpconfig import GPConfigManager, IllegalPathError

manager = GPConfigManager("myapp")

# 格式错误的路径
for bad in ["", ".", "a..b", ".hidden", "global_env.", "a/b"]:
    try:
        manager.get_config(bad)
    except IllegalPathError as e:
        print(f"拒绝 {bad!r}: {e.path} -> {e}")

# save() 拒绝包含 '.' 的路径(cfg_path 风格、'.yaml' 后缀、'..' 穿越)
try:
    manager.save(config, "backups/database.yaml")  # 包含 '.'(.yaml 后缀)
except IllegalPathError as e:
    print(f"保存被拒绝: {e}")

ConfigReadonlyError

尝试保存只读配置时抛出。

class ConfigReadonlyError(GPConfigError):
    """Raised when trying to save a readonly config."""

    def __init__(self, config_name: str):
        super().__init__(f"Config '{config_name}' is readonly and cannot be saved")

示例

from gpconfig import GPConfig, GPConfigManager, ConfigReadonlyError
from typing import ClassVar

class SecureConfig(GPConfig):
    cfg_class_name: ClassVar[str] = "SecureConfig"
    api_key: str

# 获取配置
manager = GPConfigManager("myapp")
config = manager.get_config("secure", SecureConfig)

# 设置为只读
config.readonly = True

try:
    config.api_key = "new_key"
    config.save()
except ConfigReadonlyError as e:
    print(f"无法保存只读配置: {e}")
    # 输出: Config 'secure' is readonly and cannot be saved

使用场景

只读配置适用于: - 生产环境敏感配置 - 不应被修改的系统配置 - 防止意外覆盖

# 在生产环境中自动设置只读
import os

config = manager.get_config("production_db", DatabaseConfig)
if os.environ.get("ENV") == "production":
    config.readonly = True

RegistrationError

类注册相关问题时抛出。

class RegistrationError(GPConfigError):
    """Raised when there's an issue with class registration."""
    pass

触发场景

  • 用不同的类注册一个已被占用的 cfg_class_name(重新注册同一个类是幂等的,会静默成功)
  • 配置类未注册但调用了 get_object()
  • 配置类没有关联的 configured_class

示例

from gpconfig import GPConfig, GPConfigManager, RegistrationError
from typing import ClassVar

class MyConfig(GPConfig):
    cfg_class_name: ClassVar[str] = "MyConfig"
    value: str

class ConflictConfig(GPConfig):
    cfg_class_name: ClassVar[str] = "MyConfig"  # 相同的 cfg_class_name,不同的类
    other: str

# 第一次注册 - 成功
GPConfigManager.register_config_class(MyConfig)

# 用不同的类在相同名字下再次注册 - 失败
try:
    GPConfigManager.register_config_class(ConflictConfig)
except RegistrationError as e:
    print(f"注册错误: {e}")
    # 输出: Config class name 'MyConfig' is already registered with a different class

ConfigurableConstructionError

可配置对象的构造钩子返回了无效的对象类型时抛出。

class ConfigurableConstructionError(GPConfigError):
    """Raised when a construction hook returns an invalid object type.

    Attributes:
        path: Canonical dotted path used for object construction.
        expected_type: Registered configurable class that should be returned.
        actual_type: Type actually returned by the construction hook.
    """

    def __init__(
        self,
        path: str,
        expected_type: type,
        actual_type: type,
    ):
        self.path = path
        self.expected_type = expected_type
        self.actual_type = actual_type

触发场景

ConfigurableConstructionError 由 GPConfigManager.get_object()(以及委托给它的 GPConfigFolder.get_object())在可配置类的 from_config() 钩子返回值不是已注册可配置类的实例时抛出:

  • 钩子返回了无关类型(例如 dict、None 或普通对象)。
  • 钩子返回的实例既不是已注册类,也不是其子类。

返回已注册类的子类实例是允许的,不会抛出。只有返回值无法通过 isinstance(result, configurable_cls) 时才会触发此错误。

属性

属性 类型 说明
path str 用于构造的源 YAML 文件规范点路径(不含 .yaml 后缀,不含项目名前缀)
expected_type type 应当返回的已注册可配置类
actual_type type 构造钩子实际返回的类型

由谁抛出

由 GPConfigManager.get_object() 和 GPConfigFolder.get_object() 在调用 configurable_cls.from_config(config, context=context) 之后抛出。钩子内部抛出的异常(业务异常,或签名不兼容导致的 TypeError)会原样传播,不会变成 ConfigurableConstructionError —— 此错误专用于返回类型错误。

示例

from typing import ClassVar

from gpconfig import (
    GPConfig,
    GPConfigurable,
    GPConfigManager,
    ConfigurableConstructionError,
)
from gpconfig.configurable import GPConfigurableContext


class WorkerConfig(GPConfig):
    cfg_class_name: ClassVar[str] = "WorkerConfig"
    worker_name: str


class Worker(GPConfigurable):
    @classmethod
    def from_config(
        cls,
        config: WorkerConfig,
        *,
        context: GPConfigurableContext,
    ) -> "Worker":
        # BUG:返回了一个 dict,而不是 Worker 实例
        return {"worker_name": config.worker_name}


# 假设 worker.yaml 中有 configured_class_name: "Worker"
GPConfigManager.register_config_class(WorkerConfig)
GPConfigManager.register_configurable_class(Worker)

manager = GPConfigManager("myapp")

try:
    worker = manager.get_object("worker")
except ConfigurableConstructionError as e:
    print(f"{e.path} 返回了错误类型")        # e.path == "worker"
    print(f"期望: {e.expected_type.__name__}")    # "Worker"
    print(f"实际: {e.actual_type.__name__}")      # "dict"
    print(e)
    # Configurable 'Worker' from path 'worker' returned dict; expected an instance of Worker

ConfigValidationError

配置文件验证失败时抛出。

class ConfigValidationError(GPConfigError):
    """Raised when a config file fails validation."""

    def __init__(self, path: str, original_error: Exception):
        self.path = path
        self.original_error = original_error
        super().__init__(f"Validation failed for '{path}': {original_error}")

触发条件

当配置文件无法被转成合法的配置对象时,GPConfigManager.get_config()(及其 YAML 加载路径)会抛出 ConfigValidationError:

  • YAML 语法/解析错误 —— 畸形 YAML(缩进错误、引号/括号未闭合、使用了 Tab 等)。原始错误为 yaml.YAMLError,其消息带有文件路径、行号和列号(例如 in ".../server.yaml", line 3, column 5)。
  • YAML 显式键重复 —— 同一个 mapping 中的两个键解析成相同的 Python 字典键,包括 1/true、null/~ 等写法不同但解析结果相等的键。原始错误为 yaml.constructor.ConstructorError;其消息会指出重复键、文件路径、首次定义的行列号,以及重复定义的行列号。同名键仍可分别出现在不同 mapping 中,显式键也仍可覆盖合法 merge key(<<)提供的值。
  • 顶层非字典 —— YAML 能解析,但根节点是列表或标量而非映射。消息中嵌入磁盘上的文件路径。
  • Pydantic 校验失败 —— YAML 解析成字典,但某字段未通过 schema 校验(类型错误、缺少必填字段、或在 extra="forbid" 下出现多余字段)。原始错误为 Pydantic 的 ValidationError,其消息会指出出错的字段名。

无论哪种情况,异常消息都携带点分配置路径(.path)、磁盘上的文件路径,以及底层错误的详细信息。重复键错误还会携带可读的键名、首次定义的行列号和重复定义的行列号。YAML 错误会同时保存在 .original_error 与 .__cause__ 上,调用方无需解析包装异常的完整文本即可检查原始 yaml.YAMLError。

属性

属性 类型 说明
path str 验证失败的点分配置路径
original_error Exception 底层错误:Pydantic 的 ValidationError、yaml.YAMLError,或 TypeError(顶层非字典)

示例

from gpconfig import GPConfig, GPConfigManager, ConfigValidationError
from typing import ClassVar

class ServerConfig(GPConfig):
    cfg_class_name: ClassVar[str] = "ServerConfig"
    host: str      # 必需
    port: int      # 必须是整数

# 假设 server.yaml 内容为:
# host: localhost
# port: "not_a_number"  # 类型错误

manager = GPConfigManager("myapp")

try:
    config = manager.get_config("server", ServerConfig)
except ConfigValidationError as e:
    print(f"配置验证失败: {e.path}")
    # e.path 是点分配置路径,例如 "server"。
    # str(e) 同时会嵌入磁盘上的文件路径和出错字段名。
    print(f"原始错误: {e.original_error}")

最佳实践

  1. 使用特定的异常类型:根据具体场景捕获特定的异常
  2. 提供有用的错误信息:在异常处理中提供用户友好的错误信息
  3. 记录结构化属性:多个异常携带值得随消息一起记录的上下文——ConfigValidationError.original_error、ConfigurableConstructionError(path、expected_type、actual_type)以及 IllegalPathError.path。
  4. 使用基类作为兜底:最后使用 GPConfigError 捕获所有未处理的配置异常