From c310c5069f55f4f66005a28da11b16f76cbc4ddf Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Thu, 10 Jul 2025 16:31:32 +0800 Subject: [PATCH] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=88=B0=E4=BA=86pydantic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/manager.cpython-312.pyc | Bin 9189 -> 9863 bytes .../config/__pycache__/models.cpython-312.pyc | Bin 8832 -> 9415 bytes core/config/manager.py | 34 ++- core/config/models.py | 205 +++++++++--------- tests/test_config.py | 146 +++++++++++++ 5 files changed, 278 insertions(+), 107 deletions(-) create mode 100644 tests/test_config.py diff --git a/core/config/__pycache__/manager.cpython-312.pyc b/core/config/__pycache__/manager.cpython-312.pyc index 7e59efb02217fa1346d8f19ef4ede78055165b06..f76d67958060dc0b271b9bc8069c1c075f280971 100644 GIT binary patch delta 2558 zcmZ`*eQZKzq?lEUYHeb+6xOp9uwql}XHn9wI8+C- z>tqtAQqLsyu!BilOyW9aUCx`5*l%Ndi5-zk?L!Yr;uCbo-Bq?@G0+cLp=B5PIHph%|2nq2~24 ziXt6?&09Mnhh`+*Q=3otYopCxk+?s(<}YS0NbjI`<(f~7{{PupA30k=I%87sjC5L> z#?$De@Cus7aVvE@uHj+&N5|ILDaia6A7B03`HO%5@%hg`{vZ;Yg0GH6?wwoweCgV~ z_ugU>$SjjwTKeMG?>`gy6^c7}at&cz)k!y7H0m;1Ca3A6xwK07J*dm(6?HPDOl6Og zr$ED!O65kgYAR(|_n%Cw$MsA;N1p3oOC$yWIVTX5xcu0aF=Td?$1>waT~3`&ZH%$1 z$xNBuB-U6N3_7>ZhS_bMLzM>PX_yZcLu>A~6?a>yZ`Hl^oP5XTKDYNXyKBuJS+Pe- zzEa_~z3+~{Y5uk1Yis_F6@N!bU-fsLJFqSzS8$=D)OEYQyCT*(j^VrB(8AcIiQC@Z zzqD>!X&qdY*Lo5wJ&Dzx0XDmE=uT(fg~9ou;_OhVeZ?2M6N(iN{52RYT)wvx7hZpBwrtt*?{zEkh3+F0cy%?* z+l%&v)0dUYBa3^kx2z>bRuUuSeMibiM$1Rjv-*APx~-PUIKU-;JX088HSW$E;aH!Hm+TUS77l7zz+a==${&! zWlkZ7=+}+ym@c;l-Z0@t`io$0=ce@pD08lRm5mHjUNdBNXVu*6+`1u7W;ERpkEy3L z@I(iiU#eO?Oc$DCxJ0ive^jNXjW>l^LFgep{T4HN3epc+#u#4|1o~#ZkLD%I_8E~; z0=VUk5ssjUSEVZ(6!#Qj<^Pac7@;iLy@FVfs2+Ngy+8j9ZIfguc6!Slr7vu`ELTHE z|Fh-9>_KKRZs$*cQ8zL=IkmyH{6l%+ne}{1$)xqo$QmNA!p*3BBa6&nm7RHr_rjEi zmqc(C>!4lN&Ow>n{8$o$E^ba;pUFk0k80hS7PJ1o9w2|t+7-ab6^OX zPRJ8%MB)Gv05Aln9Kt`j6Q&SXatPosz!bpK08s#bOMqhu0MN5`n!eZiWU_*6Qt$P? z2hdf55h=W&RS@e~EuC13@!?0K4?uE%5QijBWv@-@E-9ejeLv`xj^awEPx6$6b;P== zGwx>xjkYChkDP$!+X7n)%=4$<9bTn5bl28Y7Mu94c!->2vT7M9^uxAq&k|7LoM?E` zd7^Hw(%yE?sv4)*>b>54wD}v({~+M0;qSU(WtaWM{P?&^2s~@my5k|_2mKMq;9QS| z%dlB?Gfz{K>Rxh&s?iQ{*BaVIe;)1q)(I2#e2F+tNXQeW;W=kT#5lMxw2oNEKj?qf C=}tcY delta 1980 zcmZ8hOKcNY6rG>PpNv1T<4J7Gu>(#VBl4l3CbSev0tyKv&=Rp2AsOSD7*l`38wUtA zfg(h^Xe;Pb3l%~QT~w(+Dl|n!VpE_Cq)JuNAO($Bw2Lm-RB1^U-Syt*04-1Qnd|%R zyT5ntdw=-%0nblvw~K=n*2;%QBNseP{4W=h{ik?N=XG15uBd2A(XF|Q9?etqYTlwx z^A+p0`l4U+7Xw;==LF6W54WR>|1TqWjjw4m6>OZkY$(_5*Mt>aqreSty5lUTJD=;C zz^>^U8bsiyg>;wEG$!bEXC*B>DWsH%Mo_!xRYC1~f>t9b-jeoYDu+^%C2h-RE0#1m zIc`{X#C;iJId+d%^5s&dK=<=s@l`4cJv+uEh$?9=$j7GJ4Ts?jfh*lE{!|dP@_5;-7{sz?%cY7@su0YNs0dg; zX#yBYIV>+Ra(T2&gW*^{M_4W%H1Q%R2rs1;1)Yi4Qp4}gmt?QT%%H%bd{qw`A8Ib0P_cU8H=c6w83pdTrH;i|Zo zt6-^Y>3o&H%;`c^IP9WjcQ7Dcu|4D686Is@63uw-(5Sb4PfBL_T4K(qSgyR8FPW80 zDQl1p@I_t$upH?$mYzmokEqPn|$H?V{hnE+w|Ip-mYK0 z!4IY$`5G2|iFsdQdUR&fLtpQsQ1pxF=h4N`n)%S0nGFk}?(_1J%=y$0+{+@@7@zLC zk+`AG^$b2(yEv4dA4<;+9+*3jnTsD>P_uKs>?3gMCfLN;YJIN+w7M0H#S@sa{aA*S z@}-<5*^NO;tdnKkHx@ZoFwh^ z6SafS&HkVcihDP~E_nlCGXRi|U_>&2umfQbVF&?b$Fm&bj_0ViQ@Qw`Z)|vYWK6!IW|tLeXMh=hkL=ulmXa{ ztvn;NvMMRh;LZbjbyb(DhA);cl&AIULknupqPk^X-Ljx=rB7CUxWVh`OLBLk;l7A? zw`VoT_ae5wg#BLJj&jo3uZs7&Tl(w8`}H=EM^g2qm(HwC?vI0eOEfElq(EZt2C<>! zLtyj6s%^)=IL@RWVHd(Uz}FljZ;fLC)FR+hW*()TiS?~dxjNgrrEtKeUNVlVTv16#ipyZ|G9Gk%3M8Vihrl`HGP3m6Yy6L*Mb*))}_x}mJ Z1st(){uqs&_AE&}AHFoa%)y5x`!D+U&XE8B diff --git a/core/config/__pycache__/models.cpython-312.pyc b/core/config/__pycache__/models.cpython-312.pyc index 9dd901419232d4e5120434a2cbd70de9c1db5c56..6e86875ba6c6068ce0e2b18ff23bef349736ff76 100644 GIT binary patch literal 9415 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 None: """ 注册一个配置类 @@ -91,14 +91,27 @@ class ConfigManager: 配置实例 """ config = self._configs.get(name) - if not isinstance(config, config_class): + if config is None: # 如果配置不存在,先注册一个默认实例 - if config is None: - self.register_config(name, config_class) - config = self._configs.get(name) - else: - raise TypeError(f"Configuration '{name}' is not of type '{config_class.__name__}'") - return config + self.register_config(name, config_class) + config = self._configs.get(name) + + # 确保配置是正确的类型 + if not isinstance(config, config_class): + # 尝试转换配置 + try: + if isinstance(config, BaseConfig): + # 将现有配置转换为请求的类型 + new_config = config_class(**config.model_dump()) + self._configs[name] = new_config + config = new_config + else: + raise TypeError(f"Configuration '{name}' is not of type '{config_class.__name__}'") + except Exception as e: + logger.error(f"转换配置 '{name}' 到类型 '{config_class.__name__}' 失败: {e}") + raise TypeError(f"Configuration '{name}' is not of type '{config_class.__name__}'") from e + + return cast(T, config) def _load_all_configs_from_dir(self): """动态加载目录中的所有.json文件""" @@ -176,7 +189,8 @@ class ConfigManager: raise ValueError("配置目录未设置,无法保存文件") path = self.config_dir / f"{name}.json" - config_data = self.get_config(name, BaseConfig).to_dict() + config = self.get_config(name, BaseConfig) + config_data = config.to_dict() try: with open(path, 'w', encoding='utf-8') as f: diff --git a/core/config/models.py b/core/config/models.py index 5afc4f0..7e9c217 100644 --- a/core/config/models.py +++ b/core/config/models.py @@ -4,18 +4,23 @@ """ 配置模型 定义了项目中所有配置的数据类模型 +使用pydantic进行数据验证和序列化 """ -from dataclasses import dataclass, field, fields, asdict -from typing import Dict, Any, List, Optional -from pydantic import BaseModel, Field +from typing import Dict, Any, List, Optional, Type, TypeVar, Union, ClassVar +from pydantic import BaseModel, Field, model_validator, ConfigDict +T = TypeVar('T', bound='BaseConfig') -# 基础配置类,提供通用方法 -class BaseConfig: +class BaseConfig(BaseModel): """可从字典更新的配置基类""" - def update(self, new_data: Dict[str, Any]): + model_config = ConfigDict( + extra="allow", # 允许额外字段 + arbitrary_types_allowed=True # 允许任意类型 + ) + + def update(self, new_data: Dict[str, Any]) -> None: """从字典递归更新配置""" for key, value in new_data.items(): if hasattr(self, key): @@ -24,20 +29,105 @@ class BaseConfig: if isinstance(current_attr, BaseConfig) and isinstance(value, dict): # 递归更新嵌套的配置对象 current_attr.update(value) - # 新增:处理ReferItem列表的特殊情况 - elif key == 'refer_list' and isinstance(value, list): - # 显式地从字典创建ReferItem对象 - setattr(self, key, [ReferItem(**item) for item in value]) else: # 否则,直接设置值 setattr(self, key, value) - + def to_dict(self) -> Dict[str, Any]: """将配置转换为字典""" - return asdict(self) + return self.model_dump() + + +class ReferItem(BaseConfig): + """单个Refer项""" + path: str = "" + sampling_rate: float = 1.0 + step: str = "" # 可选值: "topic", "content", "judge",表示在哪个阶段使用 + + +class ReferConfig(BaseConfig): + """Refer配置,现在是一个列表""" + refer_list: List[ReferItem] = Field(default_factory=list) + + def update(self, new_data: Dict[str, Any]) -> None: + """特殊处理refer_list的更新""" + if 'refer_list' in new_data and isinstance(new_data['refer_list'], list): + # 将字典列表转换为ReferItem对象列表 + new_list = [] + for item in new_data['refer_list']: + if isinstance(item, dict): + new_list.append(ReferItem(**item)) + elif isinstance(item, ReferItem): + new_list.append(item) + self.refer_list = new_list + + # 移除已处理的refer_list,避免重复处理 + new_data_copy = new_data.copy() + new_data_copy.pop('refer_list') + super().update(new_data_copy) + else: + super().update(new_data) + + +class PathConfig(BaseConfig): + """单个路径配置""" + path: str = "" + + +class PathListConfig(BaseConfig): + """路径列表配置""" + paths: List[str] = Field(default_factory=list) + + +class SamplingPathListConfig(BaseConfig): + """带采样率的路径列表配置""" + sampling_rate: float = 1.0 + paths: List[str] = Field(default_factory=list) + + +class OutputConfig(BaseConfig): + """输出配置""" + base_dir: str = "result" + image_dir: str = "images" + topic_dir: str = "topics" + content_dir: str = "contents" + + +class ResourceConfig(BaseConfig): + """资源配置""" + resource_dirs: List[str] = Field(default_factory=list) + style: PathListConfig = Field(default_factory=PathListConfig) + demand: PathListConfig = Field(default_factory=PathListConfig) + object: PathListConfig = Field(default_factory=PathListConfig) + product: PathListConfig = Field(default_factory=PathListConfig) + refer: ReferConfig = Field(default_factory=ReferConfig) + image: PathListConfig = Field(default_factory=PathListConfig) + output_dir: OutputConfig = Field(default_factory=OutputConfig) + + +class SystemConfig(BaseConfig): + """系统配置""" + debug: bool = False + log_level: str = "INFO" + parallel_processing: bool = True + max_workers: int = 4 + + +class TopicConfig(BaseConfig): + """选题配置""" + date: str = "" + num: int = 5 + variants: int = 1 + + +class GenerateTopicConfig(BaseConfig): + """主题生成配置""" + topic_system_prompt: str = "resource/prompt/generateTopics/system.txt" + topic_user_prompt: str = "resource/prompt/generateTopics/user.txt" + model: Dict[str, Any] = Field(default_factory=dict) + topic: TopicConfig = Field(default_factory=TopicConfig) -@dataclass class GenerateContentConfig(BaseConfig): """内容生成配置""" content_system_prompt: str = "resource/prompt/generateContent/contentSystem.txt" @@ -46,9 +136,10 @@ class GenerateContentConfig(BaseConfig): judger_user_prompt: str = "resource/prompt/judgeContent/user.txt" enable_content_judge: bool = True refer_sampling_rate: float = 1.0 + model: Dict[str, Any] = Field(default_factory=dict) + judger_model: Dict[str, Any] = Field(default_factory=dict) -@dataclass class AIModelConfig(BaseConfig): """AI模型配置""" model: str = "qwq-plus" @@ -63,99 +154,19 @@ class AIModelConfig(BaseConfig): topic_user_prompt: str = "resource/prompt/generateTopics/user.txt" refer_sampling_rate: float = Field(0.5, ge=0.0, le=1.0) - class Config: - pass - -@dataclass class PosterConfig(BaseConfig): """海报生成配置""" - target_size: List[int] = field(default_factory=lambda: [900, 1200]) + target_size: List[int] = Field(default_factory=lambda: [900, 1200]) additional_images_enabled: bool = True template_selection: str = "random" # random, business, vibrant, original - available_templates: List[str] = field(default_factory=lambda: ["original", "business", "vibrant"]) + available_templates: List[str] = Field(default_factory=lambda: ["original", "business", "vibrant"]) -@dataclass class ContentConfig(BaseConfig): """内容生成配置""" enable_content_judge: bool = True num: int = 5 variants_per_topic: int = 1 max_title_length: int = 30 - max_content_length: int = 500 - - -@dataclass -class PathConfig(BaseConfig): - """单个路径配置""" - path: str = "" - -@dataclass -class PathListConfig(BaseConfig): - """路径列表配置""" - paths: List[str] = field(default_factory=list) - -@dataclass -class SamplingPathListConfig(BaseConfig): - """带采样率的路径列表配置""" - sampling_rate: float = 1.0 - paths: List[str] = field(default_factory=list) - -@dataclass -class ReferItem: - """单个Refer项""" - path: str = "" - sampling_rate: float = 1.0 - step: str = "" # 可选值: "topic", "content", "judge",表示在哪个阶段使用 - -@dataclass -class ReferConfig(BaseConfig): - """Refer配置,现在是一个列表""" - refer_list: List[ReferItem] = field(default_factory=list) - -@dataclass -class OutputConfig(BaseConfig): - """输出配置""" - base_dir: str = "result" - image_dir: str = "images" - topic_dir: str = "topics" - content_dir: str = "contents" - - -@dataclass -class ResourceConfig(BaseConfig): - """资源配置""" - resource_dirs: List[str] = field(default_factory=list) - style: PathListConfig = field(default_factory=PathListConfig) - demand: PathListConfig = field(default_factory=PathListConfig) - object: PathListConfig = field(default_factory=PathListConfig) - product: PathListConfig = field(default_factory=PathListConfig) - refer: ReferConfig = field(default_factory=ReferConfig) - image: PathListConfig = field(default_factory=PathListConfig) - output_dir: OutputConfig = field(default_factory=OutputConfig) - - -@dataclass -class SystemConfig(BaseConfig): - """系统配置""" - debug: bool = False - log_level: str = "INFO" - parallel_processing: bool = True - max_workers: int = 4 - - -@dataclass -class TopicConfig(BaseConfig): - """选题配置""" - date: str = "" - num: int = 5 - variants: int = 1 - -@dataclass -class GenerateTopicConfig(BaseConfig): - """主题生成配置""" - topic_system_prompt: str = "resource/prompt/generateTopics/system.txt" - topic_user_prompt: str = "resource/prompt/generateTopics/user.txt" - model: Dict[str, Any] = field(default_factory=dict) - topic: TopicConfig = field(default_factory=TopicConfig) \ No newline at end of file + max_content_length: int = 500 \ No newline at end of file diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..79f6c6e --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测试配置管理器和pydantic模型的兼容性 +""" + +import os +import json +import tempfile +from pathlib import Path + +from core.config.manager import ConfigManager +from core.config.models import ( + BaseConfig, AIModelConfig, SystemConfig, GenerateTopicConfig, ResourceConfig, + GenerateContentConfig, PosterConfig, ContentConfig, ReferItem, ReferConfig +) + +def test_config_creation(): + """测试创建配置实例""" + print("测试创建配置实例...") + + # 创建各种配置实例 + ai_config = AIModelConfig(model="test-model", api_key="test-key") + print(f"AI配置: {ai_config.model}, {ai_config.api_key}") + + # 测试嵌套配置 + resource_config = ResourceConfig( + resource_dirs=["resource"], + refer=ReferConfig( + refer_list=[ + ReferItem(path="path1", sampling_rate=0.5, step="topic"), + ReferItem(path="path2", sampling_rate=0.8, step="content") + ] + ) + ) + + # 验证嵌套配置 + print(f"资源目录: {resource_config.resource_dirs}") + print(f"Refer项数量: {len(resource_config.refer.refer_list)}") + print(f"第一个Refer项: {resource_config.refer.refer_list[0].path}, {resource_config.refer.refer_list[0].sampling_rate}") + + # 测试序列化 + config_dict = resource_config.to_dict() + print(f"序列化后的字典: {json.dumps(config_dict, indent=2)}") + + return True + +def test_config_manager(): + """测试配置管理器""" + print("\n测试配置管理器...") + + # 创建临时目录 + with tempfile.TemporaryDirectory() as temp_dir: + # 创建测试配置文件 + ai_config_path = Path(temp_dir) / "ai_model.json" + ai_config = { + "model": "test-model", + "api_key": "test-key", + "temperature": 0.8 + } + + with open(ai_config_path, "w") as f: + json.dump(ai_config, f) + + # 创建嵌套配置文件 + resource_config_path = Path(temp_dir) / "resource.json" + resource_config = { + "resource_dirs": ["test_resource"], + "refer": { + "refer_list": [ + {"path": "test_path", "sampling_rate": 0.7, "step": "judge"} + ] + } + } + + with open(resource_config_path, "w") as f: + json.dump(resource_config, f) + + # 初始化配置管理器 + config_manager = ConfigManager() + config_manager.load_from_directory(temp_dir) + + # 获取并验证AI配置 + ai_model_config = config_manager.get_config("ai_model", AIModelConfig) + print(f"加载的AI配置: {ai_model_config.model}, {ai_model_config.api_key}, {ai_model_config.temperature}") + assert ai_model_config.model == "test-model" + assert ai_model_config.api_key == "test-key" + assert ai_model_config.temperature == 0.8 + + # 获取并验证资源配置 + resource_config = config_manager.get_config("resource", ResourceConfig) + print(f"加载的资源配置: {resource_config.resource_dirs}") + print(f"加载的Refer项: {resource_config.refer.refer_list[0].path}, {resource_config.refer.refer_list[0].sampling_rate}") + assert resource_config.resource_dirs == ["test_resource"] + assert len(resource_config.refer.refer_list) == 1 + assert resource_config.refer.refer_list[0].path == "test_path" + assert resource_config.refer.refer_list[0].sampling_rate == 0.7 + + # 测试更新配置 + ai_model_config.update({"model": "updated-model"}) + print(f"更新后的AI配置: {ai_model_config.model}") + assert ai_model_config.model == "updated-model" + + # 测试保存配置 + config_manager.save_config("ai_model") + + # 重新加载并验证 + with open(ai_config_path, "r") as f: + saved_config = json.load(f) + print(f"保存的配置: {saved_config['model']}") + assert saved_config["model"] == "updated-model" + + return True + +def test_config_type_conversion(): + """测试配置类型转换""" + print("\n测试配置类型转换...") + + # 创建配置管理器 + config_manager = ConfigManager() + + # 注册一个SystemConfig + config_manager.register_config("test_config", SystemConfig) + + # 尝试以不同类型获取 + system_config = config_manager.get_config("test_config", SystemConfig) + print(f"获取为SystemConfig: {type(system_config).__name__}") + + # 尝试转换类型 + try: + content_config = config_manager.get_config("test_config", ContentConfig) + print(f"成功转换为ContentConfig: {type(content_config).__name__}") + except TypeError as e: + print(f"类型转换失败,符合预期: {e}") + + return True + +if __name__ == "__main__": + print("开始测试pydantic配置模型和ConfigManager...") + + test_config_creation() + test_config_manager() + test_config_type_conversion() + + print("\n所有测试完成!") \ No newline at end of file