# 开发文档 本文档说明 yaml2pptx 项目的代码结构、开发规范和技术决策。 ## 项目概述 yaml2pptx 是一个将 YAML 格式的演示文稿源文件转换为 PPTX 文件的工具,支持模板系统和浏览器预览功能。 ## 代码结构 项目采用模块化架构,按功能职责组织代码: ``` html2pptx/ ├── yaml2pptx.py (200 行) # 入口脚本,CLI + main 函数 ├── utils.py (74 行) # 工具函数(日志、颜色转换) ├── core/ # 核心领域模型 │ ├── elements.py (200 行) # 元素抽象层(dataclass + validate) │ ├── template.py (191 行) # 模板系统 │ ├── condition_evaluator.py # 条件表达式评估器(新增) │ └── presentation.py (91 行) # 演示文稿类 ├── loaders/ # 数据加载层 │ └── yaml_loader.py (113 行) # YAML 加载和验证 ├── validators/ # 验证层 │ ├── __init__.py # 导出主验证器 │ ├── result.py (70 行) # 验证结果数据结构 │ ├── validator.py (150 行) # 主验证器 │ ├── geometry.py (120 行) # 几何验证器 │ └── resource.py (110 行) # 资源验证器 ├── renderers/ # 渲染层 │ ├── pptx_renderer.py (292 行) # PPTX 渲染器 │ └── html_renderer.py (172 行) # HTML 渲染器(预览) └── preview/ # 预览功能 └── server.py (244 行) # Flask 服务器 + 文件监听 ``` ### 依赖关系 ``` yaml2pptx.py (入口) ↓ ├─→ utils (工具函数) ├─→ loaders.yaml_loader (YAML 加载) ├─→ validators.validator (验证器) │ ↓ │ ├─→ validators.result (验证结果) │ ├─→ validators.geometry (几何验证) │ ├─→ validators.resource (资源验证) │ └─→ core.elements (元素验证) ├─→ core.presentation (演示文稿) │ ↓ │ ├─→ core.template (模板) │ └─→ core.elements (元素) ├─→ renderers.pptx_renderer (PPTX 生成) │ ↓ │ └─→ core.elements └─→ preview.server (预览服务) ↓ └─→ renderers.html_renderer ↓ └─→ core.elements ``` **依赖原则**: - 单向依赖:入口 → 验证/渲染 → 核心 ← 加载 - 无循环依赖 - 核心层不依赖其他业务模块 - 验证层可以调用核心层的元素验证方法 ## 模块职责 ### 1. yaml2pptx.py(入口层) - **职责**:CLI 参数解析、流程编排 - **行数**:约 100-150 行 - **包含**: - `/// script` 依赖声明 - `parse_args()` - 命令行参数解析 - `main()` - 主流程编排 - `handle_convert()` - 转换流程,包含页面级 `enabled` 检查 - **不包含**:业务逻辑、数据处理 - **enabled 实现细节**: - 在主渲染循环中检查 `slide_data.get('enabled', True)` - 跳过 `enabled=false` 的幻灯片,不调用 `render_slide()` - 维护独立的 `slide_index` 计数器,只统计实际渲染的幻灯片 - 进度日志显示准确的渲染数量(不包括禁用的幻灯片) ### 2. utils/(工具层) - **职责**:通用工具函数 - **包含**: - `utils/__init__.py` - 日志和颜色工具 - 日志函数:`log_info()`, `log_success()`, `log_error()`, `log_progress()` - 颜色转换:`hex_to_rgb()`, `validate_color()` - **依赖**: - Pillow (PIL) - 保留用于未来可能的图片处理需求 ### 3. loaders/yaml_loader.py(加载层) - **职责**:YAML 文件加载和验证 - **包含**: - `YAMLError` - 自定义异常 - `load_yaml_file()` - 加载 YAML 文件 - `validate_presentation_yaml()` - 验证演示文稿结构,调用 `validate_templates_yaml()` 验证内联模板,验证幻灯片 `enabled` 字段 - `validate_template_yaml()` - 验证外部模板结构 - `validate_templates_yaml()` - 验证内联模板结构(templates 字段) - **特点**: - 内联模板验证包括:结构验证、元素验证、变量定义验证、默认值验证 - 检测默认值中引用不存在的变量 - 验证 `enabled` 字段必须是布尔值,拒绝字符串或条件表达式 ### 4. core/elements.py(核心层 - 元素抽象) - **职责**:定义元素数据类和工厂函数 - **包含**: - `_is_valid_color()` - 颜色格式验证工具函数 - `FontConfig` - 字体配置数据类(新增) - 支持所有字体属性:parent、family、size、bold、italic、underline、strikethrough、color、align、line_spacing、space_before、space_after、baseline、caps - 在 `__post_init__` 中验证 baseline 和 caps 枚举值 - `TextElement` - 文本元素(dataclass + validate) - `font` 字段类型:`FontConfig | str | dict` - `ImageElement` - 图片元素(dataclass + validate) - 新增字段:`fit` (适配模式), `background` (背景色) - 支持四种适配模式:stretch(默认)、contain、cover、center - `ShapeElement` - 形状元素(dataclass + validate) - `TableElement` - 表格元素(dataclass + validate) - 新增字段:`font`(表格数据单元格字体)、`header_font`(表头字体) - `create_element()` - 元素工厂函数 - 自动将字典形式的 font 转换为 FontConfig 对象 - **特点**: - 使用 `@dataclass` 装饰器 - 在 `__post_init__` 中进行创建时验证 - 在 `validate()` 方法中进行元素级验证 - 元素类负责自身属性的验证(颜色格式、字体大小、枚举值等) ### 4.3. utils/font_resolver.py(工具层 - 字体解析) - **职责**:字体引用解析、继承链处理和预设类别映射 - **包含**: - `PRESET_FONT_MAPPING` - 预设字体类别映射常量 - sans → Arial - serif → Times New Roman - mono → Courier New - cjk-sans → Microsoft YaHei - cjk-serif → SimSun - `FontResolver` 类 - `__init__(fonts, fonts_default)` - 初始化解析器 - `resolve_font(font_config)` - 解析字体配置(主入口) - `_resolve_reference(reference, visited)` - 解析字体引用 - `_resolve_font_dict(font_dict, visited)` - 解析字体字典 - `_get_default_config(visited)` - 获取默认字体配置 - `_merge_with_default(config, default)` - 合并配置与默认值 - **特点**: - 支持三种引用方式:整体引用(`"@xxx"`)、继承覆盖(`{parent: "@xxx"}`)、独立定义 - 实现属性继承链:parent → 当前 → fonts_default → 系统默认 - 引用循环检测:维护已访问集合,检测重复引用 - 最大引用深度限制:10 层 - 预设类别自动映射:在 family 字段中识别预设类别名称 - **字体引用解析逻辑**: 1. 如果 font_config 为 None,使用 fonts_default 2. 如果是字符串(`"@xxx"`),解析为整体引用 3. 如果是字典且包含 parent,先解析 parent 再覆盖当前属性 4. 如果是字典且不包含 parent,直接使用字典属性 5. 未定义的属性从 fonts_default 继承 6. 如果 family 是预设类别,映射到具体字体名称 7. 返回完整的 FontConfig 对象 ### 4.5. validators/(验证层) - **职责**:YAML 文件验证,在转换前检查问题 - **包含**: - `validators/result.py` - 验证结果数据结构 - `ValidationIssue` - 验证问题(level, message, location, code) - `ValidationResult` - 验证结果(errors, warnings, infos) - `validators/geometry.py` - 几何验证器 - `GeometryValidator` - 检查元素边界、页面范围 - 支持 0.1 英寸容忍度 - `validators/resource.py` - 资源验证器 - `ResourceValidator` - 检查图片、模板文件存在性 - 验证模板文件结构 - `validators/validator.py` - 主验证器 - `Validator` - 协调所有子验证器 - 集成元素级验证、几何验证、资源验证 - **特点**: - 分级错误报告(ERROR/WARNING/INFO) - ERROR 阻止转换,WARNING 不阻止 - 验证职责分层:元素级验证在元素类中,系统级验证在验证器中 ### 5. core/template.py(核心层 - 模板) - **职责**:模板加载和变量解析 - **包含**: - `Template` 类 - `from_data()` 类方法:从字典创建模板实例(用于内联模板) - 变量解析:`resolve_value()`, `resolve_element()` - 条件渲染:`evaluate_condition()` - 委托给 ConditionEvaluator - 模板渲染:`render()` - **特点**: - 支持外部模板(从文件加载)和内联模板(从字典创建) ### 5.5. core/condition_evaluator.py(核心层 - 条件评估) - **职责**:安全地评估条件表达式 - **包含**: - `ConditionEvaluator` 类 - `evaluate_condition()` - 主评估方法 - `_get_evaluator()` - 配置 simpleeval 实例 - `_extract_expression()` - 提取表达式内容 - **技术实现**: - 使用 simpleeval 库的 `EvalWithCompoundTypes` 进行安全评估 - 支持比较运算符(==, !=, >, <, >=, <=) - 支持逻辑运算符(and, or, not) - 支持成员测试(in, not in) - 支持列表/元组字面量 - 支持数学运算(+, -, *, /, %, **) - 支持内置函数(int, float, str, len, bool, abs, min, max) - **安全策略**: - 表达式最大长度限制:500 字符 - 禁止属性访问(obj.attr) - 禁止函数定义(lambda, def) - 禁止模块导入(import) - 白名单函数限制 - 详细的错误信息映射 - **错误处理**: - `NameNotDefined` → "条件表达式中的变量未定义" - `FunctionNotDefined` → "条件表达式中使用了不支持的函数" - `FeatureNotAvailable` → "条件表达式使用了不支持的语法特性" - `AttributeDoesNotExist` → "不支持属性访问" - `SyntaxError` → "条件表达式语法错误" - 内联模板通过 `from_data()` 类方法创建,避免修改现有 `__init__` - 禁止内联模板相互引用,在渲染时检测并报错 ### 6. core/presentation.py(核心层 - 演示文稿) - **职责**:演示文稿管理和幻灯片渲染 - **包含**: - `Presentation` 类 - 内联模板存储:`__init__` 中解析并保存 `templates` 字段到 `self.inline_templates` - 模板查找:`get_template()` 优先查找内联模板,然后回退到外部模板 - 同名检测:`_external_template_exists()` 检查外部模板是否存在,防止命名冲突 - 模板缓存:外部模板使用 `template_cache` 缓存 - 幻灯片渲染:`render_slide()` - 支持三种模式 - **特点**: - 将元素字典转换为元素对象 - 使用 `create_element()` 工厂函数 - 内联和外部模板同名时抛出 ERROR 错误 - 内联模板不需要缓存(已在内存中) #### 幻灯片渲染模式 `render_slide()` 方法支持三种渲染模式: 1. **纯模板模式**:只有 `template` 字段 - 渲染模板元素 - 向后兼容原有行为 2. **纯自定义模式**:只有 `elements` 字段 - 直接使用自定义元素 - 向后兼容原有行为 3. **混合模式**:同时有 `template` 和 `elements` 字段(新功能) - 先渲染模板元素 - 自定义元素使用模板变量解析(通过 `Template.resolve_element()`) - 合并策略:简单追加(`template_elements + custom_elements`) - z 轴顺序:模板元素在底层,自定义元素在上层 #### 元素合并策略 混合模式采用简单追加策略: ```python # 步骤1:渲染模板(如果有) elements_from_template = template.render(vars_values) # 步骤2:处理自定义元素(如果有) if has_custom_elements and has_template: # 使用模板变量解析自定义元素 elements_from_custom = [ template.resolve_element(elem, vars_values) for elem in custom_elements ] # 步骤3:合并元素(模板在前,自定义在后) final_elements = elements_from_template + elements_from_custom ``` **设计决策**: - 不支持按 key/id 合并(避免引入元素 ID 概念) - 不支持位置感知合并(保持简单) - 不检测元素重叠(用户通过预览模式查看效果) - 空 `elements: []` 等同于不指定 `elements` ### 7. renderers/pptx_renderer.py(渲染层 - PPTX) - **职责**:PPTX 文件生成 - **包含**: - `PptxGenerator` 类 - 渲染方法:`_render_text()`, `_render_image()`, `_render_shape()`, `_render_table()` - 元素分发:`_render_element()` - **特点**: - 渲染器内置在生成器中 - 使用 `isinstance()` 检查元素类型 - 通过元素对象的属性访问数据 ### 8. renderers/html_renderer.py(渲染层 - HTML) - **职责**:HTML 预览渲染 - **包含**: - `HtmlRenderer` 类 - 渲染方法:`render_text()`, `render_image()`, `render_shape()`, `render_table()` - **特点**: - 与 PptxRenderer 共享元素抽象层 - 使用固定 DPI (96) 进行单位转换 ### 9. preview/server.py(预览层) - **职责**:浏览器预览和热重载 - **包含**: - Flask 应用:`create_flask_app()` - 文件监听:`YAMLChangeHandler` - 预览服务器:`start_preview_server()` - HTML 模板:`HTML_TEMPLATE`, `ERROR_TEMPLATE` ## 开发规范 ### 1. Python 环境 **必须使用 uv 运行脚本**: ```bash # 正确 uv run yaml2pptx.py input.yaml output.pptx # 错误 - 严禁直接使用主机环境的 python python yaml2pptx.py input.yaml output.pptx ``` **依赖管理**: - 所有依赖在 `pyproject.toml` 的 `[project.dependencies]` 中声明 - uv 会自动安装依赖,无需手动 `pip install` ### 2. 命令行接口 **子命令架构**: ```bash # check - 验证 YAML 文件 uv run yaml2pptx.py check [--template-dir ] # convert - 转换为 PPTX uv run yaml2pptx.py convert [output] [--template-dir ] [--skip-validation] [--force] # preview - 启动预览服务器 uv run yaml2pptx.py preview [--template-dir ] [--port ] [--host ] [--no-browser] ``` **参数说明**: - `--template-dir`:所有命令通用,指定模板目录 - `--skip-validation`:convert 专用,跳过自动验证 - `--force/-f`:convert 专用,强制覆盖已存在文件 - `--port`:preview 专用,指定端口(默认随机 30000-40000) - `--host`:preview 专用,指定主机地址(默认 127.0.0.1) - `--no-browser`:preview 专用,不自动打开浏览器 ### 3. 文件组织 **代码文件**: - 每个模块文件控制在 150-300 行 - 入口脚本约 200 行 - 使用有意义的文件名和目录结构 **测试文件**: - 所有测试文件、临时文件必须放在 `temp/` 目录下 - 不污染项目根目录 ### 4. 代码风格 **导入顺序**: ```python # 1. 标准库 import sys from pathlib import Path # 2. 第三方库 import yaml from pptx import Presentation # 3. 本地模块 from utils import log_info from core.elements import TextElement ``` **文档字符串**: - 每个模块必须有模块级文档字符串 - 每个类和函数必须有文档字符串 - 使用中文编写注释和文档 **命名规范**: - 模块名:小写 + 下划线(如 `yaml_loader.py`) - 类名:大驼峰(如 `TextElement`) - 函数名:小写 + 下划线(如 `load_yaml_file()`) - 常量:大写 + 下划线(如 `HTML_TEMPLATE`) ## 技术决策 ### 1. 元素抽象层使用 dataclass **决策**:使用 Python dataclass 定义元素数据类 **理由**: - 简洁性:自动生成 `__init__`、`__repr__` 等方法 - 类型提示:支持类型注解,IDE 友好 - 验证时机:`__post_init__` 在创建时自动调用 - 可扩展性:未来可以添加方法 **示例**: ```python @dataclass class TextElement: type: str = 'text' content: str = '' box: list = 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 个数字") ``` ### 2. 渲染器内置在生成器中 **决策**:将渲染逻辑内置在 PptxGenerator 和 HtmlRenderer 类中 **理由**: - 封装性:渲染逻辑与生成器紧密相关 - 简单性:不需要额外的渲染器接口 - 性能:避免额外的方法调用开销 **示例**: ```python class PptxGenerator: 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) ``` ### 3. 创建时验证 **决策**:在元素对象创建时进行验证(`__post_init__` 方法) **理由**: - 尽早失败:在数据进入系统时就发现错误 - 清晰的错误位置:堆栈指向元素创建处 - 避免无效状态:确保元素对象始终有效 ### 4. 元素工厂函数 **决策**:提供 `create_element(elem_dict)` 工厂函数 **理由**: - 统一入口:所有元素创建都通过工厂函数 - 类型安全:进行类型检查 - 易于扩展:添加新元素类型只需添加一个分支 ### 5. 验证职责分层 **决策**:将验证逻辑分为两层 1. **元素级验证**:放在元素类本身(`core/elements.py`) 2. **系统级验证**:放在独立的验证器模块(`validators/`) **理由**: - 元素类最了解自己的约束,应该负责自身的完整性验证 - 系统级验证需要全局上下文(如页面尺寸、文件路径),适合集中处理 - 符合单一职责原则,便于扩展和维护 **元素级验证职责**: - 必需字段检查(在 `__post_init__` 中) - 数据类型检查(在 `__post_init__` 中) - 值的有效性检查(在 `validate()` 方法中) - 颜色格式验证 - 字体大小合理性 - 枚举值检查(如形状类型) - 表格数据一致性 **系统级验证职责**: - 几何验证(元素是否在页面范围内,需要知道页面尺寸) - 资源验证(文件是否存在,需要知道文件路径) - 跨元素验证(如果未来需要) **示例**: ```python # 元素级验证(在元素类中) @dataclass class TextElement: def validate(self) -> List[ValidationIssue]: issues = [] if self.font.get('color'): if not _is_valid_color(self.font['color']): issues.append(ValidationIssue( level="ERROR", message=f"无效的颜色格式: {self.font['color']}", code="INVALID_COLOR_FORMAT" )) return issues # 系统级验证(在验证器中) class GeometryValidator: def validate_element(self, element, slide_index, elem_index): # 需要页面尺寸信息 if element.box[0] + element.box[2] > self.slide_width: # 报告边界超出 ``` ### 6. 验证容忍度 **决策**:几何验证时,允许 0.1 英寸的容忍度 **理由**: - 浮点数计算可能有精度误差 - 0.1 英寸(约 2.54mm)在视觉上几乎不可见 - 避免误报,提升用户体验 **实现**: ```python TOLERANCE = 0.1 # 英寸 if right > slide_width + TOLERANCE: # 报告 WARNING ``` ### 7. 内联模板系统 **决策**:支持在 YAML 源文件中定义内联模板,与外部模板系统共存 **理由**: - 降低使用门槛:简单场景无需创建单独的模板文件 - 保持灵活性:复杂场景仍可使用外部模板 - 向后兼容:不影响现有外部模板功能 **实现要点**: 1. **模板定义**:在 YAML 顶层添加 `templates` 字段 ```yaml templates: my-template: vars: [...] elements: [...] ``` 2. **模板创建**:使用 `Template.from_data()` 类方法 ```python @classmethod def from_data(cls, template_data, template_name): """从字典创建模板(内联模板)""" obj = cls.__new__(cls) obj.data = template_data obj.vars_def = {var['name']: var for var in template_data.get('vars', [])} obj.elements = template_data.get('elements', []) return obj ``` 3. **模板查找**:`Presentation.get_template()` 优先查找内联模板 ```python def get_template(self, template_name): # 1. 检查内联模板 if hasattr(self, 'inline_templates') and template_name in self.inline_templates: # 2. 检查同名冲突 if self._external_template_exists(template_name): raise YAMLError(f"模板名称冲突: '{template_name}'") return Template.from_data(self.inline_templates[template_name], template_name) # 3. 回退到外部模板 return self._get_external_template(template_name) ``` 4. **同名检测**:禁止内联和外部模板同名 - 显式报错比隐式选择更安全 - 强制用户明确模板来源 - 降低调试难度 5. **限制**:禁止内联模板相互引用 - 降低实现复杂度 - 内联模板适合简单场景 - 复杂引用应使用外部模板 6. **验证**:`validate_templates_yaml()` 验证内联模板结构 - 检查 `templates` 是否为字典 - 检查每个模板是否有必需的 `elements` 字段 - 检查变量定义是否有必需的 `name` 字段 - 检测默认值中引用不存在的变量 ### 8. description 字段 **决策**:为 metadata、模板和幻灯片添加可选的 `description` 字段 **理由**: - 自文档化:提高模板和演示文稿的可读性和可维护性 - 备注支持:幻灯片 description 会写入 PPT 备注页,方便演讲者查看 - 完全向后兼容:字段为可选,现有文件无需修改 **实现要点**: 1. **数据模型**: - `Presentation.description`:从 `metadata.description` 读取,用于描述整个演示文稿 - `Template.description`:从模板文件的 `description` 字段读取,描述模板用途 - `Slide.description`:在 `render_slide()` 返回值中保留,会写入 PPT 备注页 2. **YAML 解析**: - 使用 `.get('description')` 自动处理缺失情况(返回 None) - YAML 原生支持多行文本格式 3. **PPT 备注功能**: - 仅幻灯片级别的 `description` 会写入 PPT 备注页 - 模板的 `description` 不继承到幻灯片备注 - `metadata.description` 不写入单个幻灯片备注 - 如果幻灯片没有 `description`,则不设置备注 4. **渲染实现**: - `PptxGenerator._set_notes()`:设置幻灯片备注的私有方法 - `PptxGenerator.add_slide()`:调用 `_set_notes()` 设置备注 **示例**: ```yaml # metadata description - 描述整个演示文稿 metadata: description: "2024年度项目进展总结" # 模板 description - 描述模板用途 templates: title-slide: description: "用于章节标题页的模板" elements: [...] # 幻灯片 description - 写入 PPT 备注页 slides: - description: "介绍项目背景和目标,包含以下要点:..." template: title-slide vars: title: "项目背景" ``` ## 扩展指南 ### 添加新元素类型 假设要添加 `VideoElement`: **1. 在 core/elements.py 中定义数据类**: ```python @dataclass class VideoElement: type: str = 'video' src: str = '' box: list = field(default_factory=lambda: [1, 1, 4, 3]) autoplay: bool = False def __post_init__(self): if not self.src: raise ValueError("视频元素必须指定 src") if len(self.box) != 4: raise ValueError("box 必须包含 4 个数字") ``` **2. 在工厂函数中添加分支**: ```python def create_element(elem_dict: dict): elem_type = elem_dict.get('type') # ... 其他类型 ... elif elem_type == 'video': return VideoElement(**elem_dict) ``` **3. 在 PptxGenerator 中实现渲染方法**: ```python def _render_element(self, slide, elem, base_path): # ... 其他类型 ... elif isinstance(elem, VideoElement): self._render_video(slide, elem, base_path) def _render_video(self, slide, elem: VideoElement, base_path): # 实现视频渲染逻辑 pass ``` **4. 在 HtmlRenderer 中实现渲染方法**: ```python def render_slide(self, slide_data, index, base_path): # ... 其他类型 ... elif isinstance(elem, VideoElement): elements_html += self.render_video(elem, base_path) def render_video(self, elem: VideoElement, base_path): # 实现 HTML 视频渲染 return f'' ``` ### 添加新渲染器 假设要添加 PDF 渲染器: **1. 创建 renderers/pdf_renderer.py**: ```python from core.elements import TextElement, ImageElement, ShapeElement, TableElement class PdfRenderer: def __init__(self): # 初始化 PDF 库 pass def add_slide(self, slide_data, base_path=None): # 添加页面 pass def _render_element(self, page, elem, base_path): if isinstance(elem, TextElement): self._render_text(page, elem) # ... 其他元素类型 ``` **2. 在 yaml2pptx.py 中添加 PDF 模式**: ```python from renderers.pdf_renderer import PdfRenderer def main(): # ... 解析参数 ... if args.pdf: # PDF 生成模式 generator = PdfRenderer() # ... 渲染逻辑 ``` ## 测试规范 ### 测试框架 项目使用 pytest 作为测试框架,测试代码位于 `tests/` 目录。 ### 测试结构 ``` tests/ ├── conftest.py # pytest 配置和共享 fixtures ├── conftest_pptx.py # PPTX 文件验证工具 ├── unit/ # 单元测试 │ ├── test_elements.py # 元素类测试 │ ├── test_template.py # 模板系统测试 │ ├── test_utils.py # 工具函数测试 │ ├── test_validators/ # 验证器测试 │ │ ├── test_geometry.py │ │ ├── test_resource.py │ │ ├── test_result.py │ │ └── test_validator.py │ └── test_loaders/ # 加载器测试 │ └── test_yaml_loader.py ├── integration/ # 集成测试 │ ├── test_presentation.py │ ├── test_rendering_flow.py │ └── test_validation_flow.py ├── e2e/ # 端到端测试 │ ├── test_convert_cmd.py │ ├── test_check_cmd.py │ └── test_preview_cmd.py └── fixtures/ # 测试数据 ├── yaml_samples/ # YAML 样本 ├── templates/ # 测试模板 └── images/ # 测试图片 ``` ### 运行测试 ```bash # 安装测试依赖 uv pip install -e ".[dev]" # 运行所有测试 uv run pytest # 运行特定类型的测试 uv run pytest tests/unit/ # 单元测试 uv run pytest tests/integration/ # 集成测试 uv run pytest tests/e2e/ # 端到端测试 # 运行特定测试文件 uv run pytest tests/unit/test_elements.py # 显示详细输出 uv run pytest -v # 显示测试覆盖率 uv run pytest --cov=. --cov-report=html ``` ### 编写测试 **测试类命名**:使用 `Test` 格式 **测试方法命名**:使用 `test_` 格式 ```python class TestTextElement: """TextElement 测试类""" def test_create_with_defaults(self): """测试使用默认值创建 TextElement""" elem = TextElement() assert elem.type == 'text' def test_invalid_color_raises_error(self): """测试无效颜色会引发错误""" with pytest.raises(ValueError, match="无效颜色"): TextElement(font={"color": "red"}) ``` ### Fixtures 共享 fixtures 定义在 `tests/conftest.py` 中: - `temp_dir`: 临时目录 - `sample_yaml`: 最小测试 YAML 文件 - `sample_image`: 测试图片 - `sample_template`: 测试模板 - `pptx_validator`: PPTX 验证器 ```python def test_with_fixture(sample_yaml): """使用 fixture 的测试""" assert sample_yaml.exists() ``` ### PPTX 验证 使用 `PptxFileValidator` 验证生成的 PPTX 文件: ```python def test_pptx_generation(temp_dir, pptx_validator): """测试 PPTX 生成""" # ... 生成 PPTX ... output_path = temp_dir / "output.pptx" # 验证文件 assert pptx_validator.validate_file(output_path) is True # 验证内容 prs = Presentation(str(output_path)) assert pptx_validator.validate_text_element( prs.slides[0], index=0, expected_content="Test" ) is True ``` ### 手动测试 ```bash # 验证 YAML 文件 uv run yaml2pptx.py check temp/test.yaml # 使用模板时验证 uv run yaml2pptx.py check temp/demo.yaml --template-dir temp/templates # 转换 YAML 为 PPTX uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx # 自动生成输出文件名 uv run yaml2pptx.py convert temp/test.yaml # 跳过自动验证 uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx --skip-validation # 强制覆盖已存在文件 uv run yaml2pptx.py convert temp/test.yaml temp/output.pptx --force # 使用模板 uv run yaml2pptx.py convert temp/demo.yaml temp/output.pptx --template-dir temp/templates # 启动预览服务器 uv run yaml2pptx.py preview temp/test.yaml # 指定端口 uv run yaml2pptx.py preview temp/test.yaml --port 8080 # 允许局域网访问 uv run yaml2pptx.py preview temp/test.yaml --host 0.0.0.0 # 不自动打开浏览器 uv run yaml2pptx.py preview temp/test.yaml --no-browser ``` ### 测试文件位置 - **自动化测试**:`tests/` 目录 - **手动测试文件**:`temp/` 目录 - `temp/*.yaml` - 手动测试用的 YAML 文件 - `temp/*.pptx` - 生成的 PPTX 文件 - `temp/templates/` - 手动测试用的模板文件 ## 常见问题 ### Q: 为什么不能直接使用 python 运行脚本? A: 项目使用 uv 和 pyproject.toml 来管理依赖。直接使用 python 会导致依赖缺失。必须使用 `uv run yaml2pptx.py`。 ### Q: 如何添加新的依赖? A: 在 `pyproject.toml` 的 `[project.dependencies]` 中添加: ```toml [project] dependencies = [ "python-pptx", "pyyaml", "flask", "watchdog", "new-dependency", # 添加新依赖 ] ``` ### Q: 为什么元素使用 dataclass 而不是普通字典? A: dataclass 提供: 1. 类型安全和 IDE 支持 2. 自动生成的方法(`__init__`, `__repr__`) 3. 创建时验证(`__post_init__`) 4. 更好的可维护性和可扩展性 ### Q: 如何调试渲染问题? A: 使用预览模式: ```bash uv run yaml2pptx.py preview temp/test.yaml ``` 在浏览器中查看渲染结果,支持热重载。 ## 项目约束 1. **面向中文开发者**:注释、文档、错误消息使用中文 2. **使用 uv 运行**:严禁直接使用主机环境的 python 3. **测试文件隔离**:所有测试文件放在 `temp/` 目录 4. **不污染主机环境**:不修改主机的 Python 配置 ## 维护指南 ### 代码审查要点 - [ ] 模块文件大小合理(150-300 行) - [ ] 无循环依赖 - [ ] 所有类和函数有文档字符串 - [ ] 使用中文注释 - [ ] 元素验证在 `__post_init__` 中完成 - [ ] 导入语句按标准库、第三方库、本地模块排序 - [ ] 测试文件在 `temp/` 目录下 ### 性能优化建议 1. **模板缓存**:Presentation 类已实现模板缓存 2. **元素验证**:只在创建时验证一次,渲染时不再验证 3. **文件监听**:预览模式使用 watchdog 高效监听文件变化