添加可选的description字段用于文档目的,不影响渲染输出。 主要更改: - core/presentation.py: 添加metadata.description属性 - core/template.py: 添加template.description属性 - tests: 添加16个新测试用例验证description功能 - docs: 更新README.md和README_DEV.md文档 - specs: 新增page-description规范文件
31 KiB
开发文档
本文档说明 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
依赖原则:
- 单向依赖:入口 → 验证/渲染 → 核心 ← 加载
- 无循环依赖
- 核心层不依赖其他业务模块
- 验证层可以调用核心层的元素验证方法
图片处理架构
图片适配模式功能采用分层设计,将图片处理逻辑与渲染逻辑分离:
ImageElement (核心层)
↓ 定义 fit/background 字段
utils/image_utils.py (工具层)
↓ 提供图片处理函数
├─→ apply_fit_mode() - 应用适配模式
├─→ create_canvas_with_background() - 创建背景画布
└─→ inches_to_pixels() / pixels_to_inches() - 单位转换
↓ 使用 Pillow (PIL)
renderers/pptx_renderer.py (渲染层)
↓ 调用 image_utils 处理图片
└─→ 将处理后的图片添加到 PPTX
renderers/html_renderer.py (渲染层)
└─→ 使用 CSS object-fit 实现相同效果
设计要点:
-
Pillow 依赖:
- 用于高质量图片处理(缩放、裁剪、画布创建)
- 使用 LANCZOS 重采样算法确保最佳质量
- 支持 RGB 模式和透明度处理
-
适配模式实现:
- stretch:直接使用
Image.resize() - contain:使用
ImageOps.contain()保持宽高比 - cover:使用
ImageOps.cover()+ 中心裁剪 - center:不缩放,使用
Image.crop()裁剪超出部分
- stretch:直接使用
-
DPI 配置:
- 在
metadata.dpi配置,默认 96 - 通过 Presentation → Renderer 传递
- 用于像素与英寸的转换计算
- 在
-
向后兼容性:
fit和background参数可选- 未指定
fit时默认使用stretch模式 - 现有 YAML 文件无需修改即可正常工作
模块职责
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()
- 日志函数:
utils/image_utils.py- 图片处理工具- 单位转换:
inches_to_pixels(),pixels_to_inches() - 图片适配:
apply_fit_mode()- 支持 stretch/contain/cover/center 四种模式 - 画布创建:
create_canvas_with_background()- 创建带背景色的画布
- 单位转换:
- 依赖:
- Pillow (PIL) - 用于高质量图片处理
- 使用 LANCZOS 重采样算法确保最佳质量
- 设计原则:
- 图片处理与渲染逻辑分离
- 支持 DPI 配置,灵活控制图片分辨率
- 所有图片操作返回 PIL Image 对象,便于后续处理
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()- 颜色格式验证工具函数TextElement- 文本元素(dataclass + validate)ImageElement- 图片元素(dataclass + validate)- 新增字段:
fit(适配模式),background(背景色) - 支持四种适配模式:stretch(默认)、contain、cover、center
- 新增字段:
ShapeElement- 形状元素(dataclass + validate)TableElement- 表格元素(dataclass + validate)create_element()- 元素工厂函数
- 特点:
- 使用
@dataclass装饰器 - 在
__post_init__中进行创建时验证 - 在
validate()方法中进行元素级验证 - 元素类负责自身属性的验证(颜色格式、字体大小、枚举值等)
- 使用
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/image_config.py- 图片配置验证器validate_fit_value()- 验证 fit 参数值validate_background_color()- 验证背景色格式validate_dpi_value()- 验证 DPI 值范围
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)
- 使用 simpleeval 库的
- 安全策略:
- 表达式最大长度限制: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() 方法支持三种渲染模式:
-
纯模板模式:只有
template字段- 渲染模板元素
- 向后兼容原有行为
-
纯自定义模式:只有
elements字段- 直接使用自定义元素
- 向后兼容原有行为
-
混合模式:同时有
template和elements字段(新功能)- 先渲染模板元素
- 自定义元素使用模板变量解析(通过
Template.resolve_element()) - 合并策略:简单追加(
template_elements + custom_elements) - z 轴顺序:模板元素在底层,自定义元素在上层
元素合并策略
混合模式采用简单追加策略:
# 步骤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
- Flask 应用:
开发规范
1. Python 环境
必须使用 uv 运行脚本:
# 正确
uv run yaml2pptx.py input.yaml output.pptx
# 错误 - 严禁直接使用主机环境的 python
python yaml2pptx.py input.yaml output.pptx
依赖管理:
- 所有依赖在
pyproject.toml的[project.dependencies]中声明 - uv 会自动安装依赖,无需手动
pip install
2. 命令行接口
子命令架构:
# check - 验证 YAML 文件
uv run yaml2pptx.py check <input> [--template-dir <dir>]
# convert - 转换为 PPTX
uv run yaml2pptx.py convert <input> [output] [--template-dir <dir>] [--skip-validation] [--force]
# preview - 启动预览服务器
uv run yaml2pptx.py preview <input> [--template-dir <dir>] [--port <port>] [--host <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. 代码风格
导入顺序:
# 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__在创建时自动调用 - 可扩展性:未来可以添加方法
示例:
@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 类中
理由:
- 封装性:渲染逻辑与生成器紧密相关
- 简单性:不需要额外的渲染器接口
- 性能:避免额外的方法调用开销
示例:
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. 验证职责分层
决策:将验证逻辑分为两层
- 元素级验证:放在元素类本身(
core/elements.py) - 系统级验证:放在独立的验证器模块(
validators/)
理由:
- 元素类最了解自己的约束,应该负责自身的完整性验证
- 系统级验证需要全局上下文(如页面尺寸、文件路径),适合集中处理
- 符合单一职责原则,便于扩展和维护
元素级验证职责:
- 必需字段检查(在
__post_init__中) - 数据类型检查(在
__post_init__中) - 值的有效性检查(在
validate()方法中)- 颜色格式验证
- 字体大小合理性
- 枚举值检查(如形状类型)
- 表格数据一致性
系统级验证职责:
- 几何验证(元素是否在页面范围内,需要知道页面尺寸)
- 资源验证(文件是否存在,需要知道文件路径)
- 跨元素验证(如果未来需要)
示例:
# 元素级验证(在元素类中)
@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)在视觉上几乎不可见
- 避免误报,提升用户体验
实现:
TOLERANCE = 0.1 # 英寸
if right > slide_width + TOLERANCE:
# 报告 WARNING
7. 内联模板系统
决策:支持在 YAML 源文件中定义内联模板,与外部模板系统共存
理由:
- 降低使用门槛:简单场景无需创建单独的模板文件
- 保持灵活性:复杂场景仍可使用外部模板
- 向后兼容:不影响现有外部模板功能
实现要点:
-
模板定义:在 YAML 顶层添加
templates字段templates: my-template: vars: [...] elements: [...] -
模板创建:使用
Template.from_data()类方法@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 -
模板查找:
Presentation.get_template()优先查找内联模板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) -
同名检测:禁止内联和外部模板同名
- 显式报错比隐式选择更安全
- 强制用户明确模板来源
- 降低调试难度
-
限制:禁止内联模板相互引用
- 降低实现复杂度
- 内联模板适合简单场景
- 复杂引用应使用外部模板
-
验证:
validate_templates_yaml()验证内联模板结构- 检查
templates是否为字典 - 检查每个模板是否有必需的
elements字段 - 检查变量定义是否有必需的
name字段 - 检测默认值中引用不存在的变量
- 检查
8. description 字段
决策:为 metadata、模板和幻灯片添加可选的 description 字段
理由:
- 自文档化:提高模板和演示文稿的可读性和可维护性
- 不影响渲染:description 仅用于文档目的,不参与 PPTX 生成
- 完全向后兼容:字段为可选,现有文件无需修改
实现要点:
-
数据模型:
Presentation.description:从metadata.description读取Template.description:从模板文件的description字段读取Slide.description:在render_slide()返回值中保留
-
YAML 解析:
- 使用
.get('description')自动处理缺失情况(返回 None) - YAML 原生支持多行文本格式
- 使用
-
不参与渲染:
- description 字段不传递给渲染器
- 不写入最终的 PPTX 文件
示例:
# metadata description
metadata:
description: "2024年度项目进展总结"
# 模板 description
templates:
title-slide:
description: "用于章节标题页的模板"
elements: [...]
# 幻灯片 description
slides:
- description: "介绍项目背景"
template: title-slide
扩展指南
添加新元素类型
假设要添加 VideoElement:
1. 在 core/elements.py 中定义数据类:
@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. 在工厂函数中添加分支:
def create_element(elem_dict: dict):
elem_type = elem_dict.get('type')
# ... 其他类型 ...
elif elem_type == 'video':
return VideoElement(**elem_dict)
3. 在 PptxGenerator 中实现渲染方法:
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 中实现渲染方法:
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'<video src="{elem.src}" ...></video>'
添加新渲染器
假设要添加 PDF 渲染器:
1. 创建 renderers/pdf_renderer.py:
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 模式:
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/ # 测试图片
运行测试
# 安装测试依赖
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<ClassName> 格式
测试方法命名:使用 test_<what_is_being_tested> 格式
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 验证器
def test_with_fixture(sample_yaml):
"""使用 fixture 的测试"""
assert sample_yaml.exists()
PPTX 验证
使用 PptxFileValidator 验证生成的 PPTX 文件:
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
手动测试
# 验证 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] 中添加:
[project]
dependencies = [
"python-pptx",
"pyyaml",
"flask",
"watchdog",
"new-dependency", # 添加新依赖
]
Q: 为什么元素使用 dataclass 而不是普通字典?
A: dataclass 提供:
- 类型安全和 IDE 支持
- 自动生成的方法(
__init__,__repr__) - 创建时验证(
__post_init__) - 更好的可维护性和可扩展性
Q: 如何调试渲染问题?
A: 使用预览模式:
uv run yaml2pptx.py preview temp/test.yaml
在浏览器中查看渲染结果,支持热重载。
项目约束
- 面向中文开发者:注释、文档、错误消息使用中文
- 使用 uv 运行:严禁直接使用主机环境的 python
- 测试文件隔离:所有测试文件放在
temp/目录 - 不污染主机环境:不修改主机的 Python 配置
维护指南
代码审查要点
- 模块文件大小合理(150-300 行)
- 无循环依赖
- 所有类和函数有文档字符串
- 使用中文注释
- 元素验证在
__post_init__中完成 - 导入语句按标准库、第三方库、本地模块排序
- 测试文件在
temp/目录下
性能优化建议
- 模板缓存:Presentation 类已实现模板缓存
- 元素验证:只在创建时验证一次,渲染时不再验证
- 文件监听:预览模式使用 watchdog 高效监听文件变化