Refactor yaml2pptx.py from a 1,245-line monolithic script into a modular architecture with clear separation of concerns. The entry point is now 127 lines, with business logic distributed across focused modules. Architecture: - core/: Domain models (elements, template, presentation) - loaders/: YAML loading and validation - renderers/: PPTX and HTML rendering - preview/: Flask preview server - utils.py: Shared utilities Key improvements: - Element abstraction layer using dataclass with validation - Renderer logic built into generator classes - Single-direction dependencies (no circular imports) - Each module 150-300 lines for better readability - Backward compatible CLI interface Documentation: - README.md: User-facing usage guide - README_DEV.md: Developer documentation OpenSpec: - Archive refactor-yaml2pptx-modular change (63/70 tasks complete) - Sync 5 delta specs to main specs (2 new + 3 updated)
8.8 KiB
8.8 KiB
Context
yaml2pptx.py 当前是一个 1,245 行的单文件脚本,包含了从 YAML 解析到 PPTX 生成的完整流程。虽然功能完整,但代码组织存在以下问题:
- 可读性差:单文件过大,开发者和 LLM 工具难以快速理解代码结构
- 维护困难:修改一个功能可能需要在文件中跳转多次,容易引入错误
- 扩展性受限:添加新元素类型需要在多处添加代码(验证、PPTX 渲染、HTML 渲染)
- 测试不便:无法对单个模块进行独立测试
当前约束:
- 必须保持
uv run yaml2pptx.py的单入口使用方式 - 所有依赖通过
/// script头部声明 - 不能污染主机环境的 Python 配置
Goals / Non-Goals
Goals:
- 将代码拆分为职责清晰的模块,每个文件控制在 150-300 行
- 引入元素抽象层,使添加新元素类型只需实现数据类和渲染方法
- 建立清晰的依赖关系,避免循环依赖
- 保持向后兼容,用户使用方式不变
- 为未来扩展(如添加 PDF 渲染器、Video 元素)奠定基础
Non-Goals:
- 不改变现有功能的行为(纯重构,不添加新功能)
- 不改变 YAML 文件格式或 CLI 参数
- 不迁移到标准 Python 包(保持单文件入口模式)
- 不添加单元测试(可以在后续单独添加)
Decisions
1. 四层架构设计
决策:采用 core/loaders/renderers/preview 四层架构
html2pptx/
├── yaml2pptx.py # 入口层:CLI + main
├── core/ # 核心层:领域模型
│ ├── elements.py
│ ├── template.py
│ └── presentation.py
├── loaders/ # 加载层:数据输入
│ └── yaml_loader.py
├── renderers/ # 渲染层:数据输出
│ ├── pptx_renderer.py
│ └── html_renderer.py
├── preview/ # 预览层:可选功能
│ └── server.py
└── utils.py # 工具层:通用函数
理由:
- 职责分离:每层有明确的职责,修改影响范围可控
- 依赖方向清晰:入口 → 渲染 → 核心 ← 加载,避免循环依赖
- 易于扩展:添加新渲染器(如 PDF)只需在 renderers/ 下新增文件
替代方案:
- 扁平结构(所有文件在同一目录):简单但缺乏层次,未来扩展会混乱
- 更细粒度的分层(如 domain/application/infrastructure):过度设计,不适合当前规模
2. 元素抽象层使用 dataclass
决策:使用 Python dataclass 定义元素数据类
from dataclasses import dataclass, field
@dataclass
class TextElement:
type: str = 'text'
content: str = ''
box: list[float] = field(default_factory=lambda: [1, 1, 8, 1])
font: dict = field(default_factory=dict)
def __post_init__(self):
# 创建时验证
if len(self.box) != 4:
raise ValueError("box 必须包含 4 个数字")
理由:
- 简洁性:dataclass 自动生成
__init__、__repr__等方法,减少样板代码 - 类型提示:支持类型注解,IDE 和 LLM 工具可以提供更好的补全
- 验证时机:
__post_init__在对象创建时自动调用,尽早发现错误 - 可扩展性:未来可以添加方法(如
to_dict()、validate_deep())
替代方案:
- TypedDict:运行时是字典,缺少验证能力
- 普通类:需要手写
__init__,代码冗长
3. 渲染器内置在生成器中
决策:将渲染逻辑内置在 PptxGenerator 和 HtmlRenderer 类中
class PptxGenerator:
def add_slide(self, slide_data, base_path=None):
# 渲染元素
for elem in slide_data['elements']:
self._render_element(slide, elem, base_path)
def _render_element(self, slide, elem, base_path):
if isinstance(elem, TextElement):
self._render_text(slide, elem)
elif isinstance(elem, ImageElement):
self._render_image(slide, elem, base_path)
# ...
理由:
- 封装性:渲染逻辑与生成器紧密相关,内置更自然
- 简单性:不需要额外的渲染器接口或依赖注入
- 性能:避免额外的方法调用开销
替代方案:
- 渲染器作为参数传入:增加复杂度,当前不需要运行时切换渲染器
- 统一渲染接口:过度抽象,PPTX 和 HTML 渲染差异较大
4. 创建时验证
决策:在元素对象创建时进行验证(__post_init__ 方法)
理由:
- 尽早失败:在数据进入系统时就发现错误,而不是等到渲染时
- 清晰的错误位置:验证失败时,堆栈指向元素创建处,易于定位
- 避免无效状态:确保元素对象始终处于有效状态
替代方案:
- 加载时验证:验证逻辑分散在 yaml_loader 中,难以维护
- 渲染时验证:错误发现太晚,可能已经生成了部分幻灯片
5. 元素工厂函数
决策:提供 create_element(elem_dict) 工厂函数,从字典创建元素对象
def create_element(elem_dict: dict):
elem_type = elem_dict.get('type')
if elem_type == 'text':
return TextElement(**elem_dict)
elif elem_type == 'image':
return ImageElement(**elem_dict)
# ...
理由:
- 统一入口:所有元素创建都通过工厂函数,便于添加通用逻辑(如日志、缓存)
- 类型安全:工厂函数可以进行类型检查,避免创建错误的元素类型
- 易于扩展:添加新元素类型只需在工厂函数中添加一个分支
6. 依赖管理策略
决策:所有依赖保留在 yaml2pptx.py 的 /// script 头部,预览依赖不再可选
# /// script
# requires-python = ">=3.8"
# dependencies = [
# "python-pptx",
# "pyyaml",
# "flask",
# "watchdog",
# ]
# ///
理由:
- 简化处理:不需要条件导入或可选依赖检查
- 一致性:所有用户都有相同的依赖环境
- 依赖轻量:flask 和 watchdog 都是轻量级库,不会显著增加安装时间
Risks / Trade-offs
风险 1:导入路径变化可能引入错误
风险:重构后内部导入路径会发生变化,可能遗漏某些导入或引入循环依赖
缓解措施:
- 按模块逐步迁移,每迁移一个模块就测试一次
- 使用 Python 的
import语句检查(运行脚本时会立即发现导入错误) - 保持清晰的依赖方向:入口 → 渲染 → 核心 ← 加载
风险 2:元素验证可能过于严格
风险:在 __post_init__ 中进行验证可能拒绝某些边缘情况的有效输入
缓解措施:
- 只验证关键约束(如 box 必须有 4 个元素),不验证业务逻辑
- 提供清晰的错误消息,帮助用户快速定位问题
- 如果发现验证过严,可以在后续放宽
风险 3:文件数量增加可能影响开发体验
权衡:从 1 个文件变成 8+ 个文件,开发者需要在多个文件间跳转
缓解措施:
- 每个文件职责清晰,命名直观(如
elements.py、pptx_renderer.py) - 文件大小适中(150-300 行),易于在单个屏幕内浏览
- 提供清晰的模块文档,说明每个文件的职责
风险 4:重构可能引入功能回归
风险:代码重组过程中可能无意中改变了某些行为
缓解措施:
- 保持函数逻辑不变,只改变组织方式
- 重构后运行完整的功能测试(手动测试所有 YAML 示例)
- 对比重构前后生成的 PPTX 文件,确保输出一致
Migration Plan
阶段 1:基础设施(utils + loaders)
- 创建目录结构
- 提取
utils.py(日志函数、颜色转换) - 提取
loaders/yaml_loader.py(YAML 加载和验证) - 更新 yaml2pptx.py 的导入,测试 YAML 加载功能
阶段 2:核心抽象(core)
- 实现
core/elements.py(元素数据类 + 工厂函数) - 提取
core/template.py(Template 类) - 提取
core/presentation.py(Presentation 类) - 测试模板渲染功能
阶段 3:渲染器(renderers)
- 实现
renderers/pptx_renderer.py(PptxGenerator + PPTX 渲染) - 实现
renderers/html_renderer.py(HtmlRenderer) - 测试 PPTX 生成和 HTML 预览功能
阶段 4:预览功能(preview)
- 提取
preview/server.py(Flask 服务器 + 文件监听) - 测试预览服务器功能
阶段 5:入口整合
- 重构 yaml2pptx.py(只保留 CLI + main)
- 运行完整的端到端测试
- 对比重构前后的输出,确保一致性
回滚策略
- 保留原始 yaml2pptx.py 的备份(如 yaml2pptx.py.backup)
- 如果发现严重问题,可以快速回滚到原始版本
- 使用 git 分支进行重构,确保可以随时回退
Open Questions
无待解决问题。设计方案已在 explore mode 中与用户充分讨论并确认。