feat: 增强模板条件渲染表达式支持
使用 simpleeval 库替换原有的简单正则匹配,支持复杂的条件表达式评估。新增 ConditionEvaluator 类处理条件逻辑,支持比较运算、逻辑运算、成员测试、数学计算和内置函数,同时保持向后兼容性。
This commit is contained in:
70
README.md
70
README.md
@@ -345,12 +345,78 @@ slides:
|
|||||||
|
|
||||||
#### 元素级条件渲染
|
#### 元素级条件渲染
|
||||||
|
|
||||||
使用 `visible` 属性控制元素显示:
|
使用 `visible` 属性控制元素显示,支持强大的条件表达式:
|
||||||
|
|
||||||
|
**基本示例**:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
# 简单比较
|
||||||
|
- type: text
|
||||||
|
content: "有数据"
|
||||||
|
visible: "{count > 0}"
|
||||||
|
|
||||||
|
# 字符串比较
|
||||||
|
- type: text
|
||||||
|
content: "草稿状态"
|
||||||
|
visible: "{status == 'draft'}"
|
||||||
|
|
||||||
|
# 非空检查(向后兼容)
|
||||||
- type: text
|
- type: text
|
||||||
content: "{subtitle}"
|
content: "{subtitle}"
|
||||||
visible: "{subtitle != ''}" # 仅当 subtitle 不为空时显示
|
visible: "{subtitle != ''}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**支持的表达式类型**:
|
||||||
|
|
||||||
|
1. **比较运算**:`==`, `!=`, `>`, `<`, `>=`, `<=`
|
||||||
|
```yaml
|
||||||
|
visible: "{score >= 60}"
|
||||||
|
visible: "{price <= 100}"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **逻辑运算**:`and`, `or`, `not`
|
||||||
|
```yaml
|
||||||
|
visible: "{count > 0 and status == 'active'}"
|
||||||
|
visible: "{is_draft or is_preview}"
|
||||||
|
visible: "{not (count == 0)}"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **成员测试**:`in`, `not in`
|
||||||
|
```yaml
|
||||||
|
visible: "{status in ['draft', 'review', 'published']}"
|
||||||
|
visible: "{level in (1, 2, 3)}"
|
||||||
|
visible: "{'test' in version}" # 字符串包含
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **数学运算**:`+`, `-`, `*`, `/`, `%`, `**`
|
||||||
|
```yaml
|
||||||
|
visible: "{(price * discount) > 50}"
|
||||||
|
visible: "{(total / count) >= 10}"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **内置函数**:`int()`, `float()`, `str()`, `len()`, `bool()`, `abs()`, `min()`, `max()`
|
||||||
|
```yaml
|
||||||
|
visible: "{len(items) > 0}"
|
||||||
|
visible: "{int(value) > 100}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**复杂条件示例**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# 范围检查
|
||||||
|
- type: text
|
||||||
|
content: "评分: {score}"
|
||||||
|
visible: "{score >= 60 and score <= 100}"
|
||||||
|
|
||||||
|
# 多条件组合
|
||||||
|
- type: text
|
||||||
|
content: "管理员或高分用户"
|
||||||
|
visible: "{is_admin or (score >= 90)}"
|
||||||
|
|
||||||
|
# 嵌套条件
|
||||||
|
- type: text
|
||||||
|
content: "符合条件"
|
||||||
|
visible: "{((count > 0) and (status == 'active')) or (is_admin and (level >= 3))}"
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 页面级启用控制
|
#### 页面级启用控制
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ html2pptx/
|
|||||||
├── core/ # 核心领域模型
|
├── core/ # 核心领域模型
|
||||||
│ ├── elements.py (200 行) # 元素抽象层(dataclass + validate)
|
│ ├── elements.py (200 行) # 元素抽象层(dataclass + validate)
|
||||||
│ ├── template.py (191 行) # 模板系统
|
│ ├── template.py (191 行) # 模板系统
|
||||||
|
│ ├── condition_evaluator.py # 条件表达式评估器(新增)
|
||||||
│ └── presentation.py (91 行) # 演示文稿类
|
│ └── presentation.py (91 行) # 演示文稿类
|
||||||
├── loaders/ # 数据加载层
|
├── loaders/ # 数据加载层
|
||||||
│ └── yaml_loader.py (113 行) # YAML 加载和验证
|
│ └── yaml_loader.py (113 行) # YAML 加载和验证
|
||||||
@@ -143,10 +144,39 @@ yaml2pptx.py (入口)
|
|||||||
- `Template` 类
|
- `Template` 类
|
||||||
- `from_data()` 类方法:从字典创建模板实例(用于内联模板)
|
- `from_data()` 类方法:从字典创建模板实例(用于内联模板)
|
||||||
- 变量解析:`resolve_value()`, `resolve_element()`
|
- 变量解析:`resolve_value()`, `resolve_element()`
|
||||||
- 条件渲染:`evaluate_condition()`
|
- 条件渲染:`evaluate_condition()` - 委托给 ConditionEvaluator
|
||||||
- 模板渲染:`render()`
|
- 模板渲染:`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__`
|
- 内联模板通过 `from_data()` 类方法创建,避免修改现有 `__init__`
|
||||||
- 禁止内联模板相互引用,在渲染时检测并报错
|
- 禁止内联模板相互引用,在渲染时检测并报错
|
||||||
|
|
||||||
|
|||||||
155
core/condition_evaluator.py
Normal file
155
core/condition_evaluator.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
"""
|
||||||
|
条件评估器模块
|
||||||
|
|
||||||
|
提供基于 simpleeval 的安全条件表达式评估能力
|
||||||
|
"""
|
||||||
|
|
||||||
|
from simpleeval import (
|
||||||
|
EvalWithCompoundTypes,
|
||||||
|
NameNotDefined,
|
||||||
|
FunctionNotDefined,
|
||||||
|
FeatureNotAvailable,
|
||||||
|
AttributeDoesNotExist
|
||||||
|
)
|
||||||
|
from loaders.yaml_loader import YAMLError
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionEvaluator:
|
||||||
|
"""条件表达式评估器
|
||||||
|
|
||||||
|
使用 simpleeval 库安全地评估条件表达式,支持:
|
||||||
|
- 比较运算符:==, !=, >, <, >=, <=
|
||||||
|
- 逻辑运算符:and, or, not
|
||||||
|
- 成员测试:in, not in
|
||||||
|
- 列表/元组字面量
|
||||||
|
- 数学运算:+, -, *, /, %, **
|
||||||
|
- 内置函数:int, float, str, len, bool, abs, min, max
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 表达式最大长度限制(防止过于复杂的表达式)
|
||||||
|
MAX_EXPRESSION_LENGTH = 500
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化条件评估器"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_evaluator(self, vars_values):
|
||||||
|
"""
|
||||||
|
获取配置好的 simpleeval 实例
|
||||||
|
|
||||||
|
Args:
|
||||||
|
vars_values: 变量值字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
EvalWithCompoundTypes 实例
|
||||||
|
"""
|
||||||
|
# 每次评估都创建新实例,避免状态污染
|
||||||
|
evaluator = EvalWithCompoundTypes(names=vars_values)
|
||||||
|
|
||||||
|
# 添加额外的安全函数到白名单
|
||||||
|
evaluator.functions.update({
|
||||||
|
'len': len,
|
||||||
|
'bool': bool,
|
||||||
|
'abs': abs,
|
||||||
|
'min': min,
|
||||||
|
'max': max,
|
||||||
|
})
|
||||||
|
|
||||||
|
return evaluator
|
||||||
|
|
||||||
|
def _extract_expression(self, condition):
|
||||||
|
"""
|
||||||
|
从条件字符串中提取表达式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
condition: 条件字符串,如 "{count > 0}"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 提取的表达式,如 "count > 0"
|
||||||
|
"""
|
||||||
|
# 去除首尾的 { } 和空白
|
||||||
|
expr = condition.strip()
|
||||||
|
if expr.startswith('{') and expr.endswith('}'):
|
||||||
|
expr = expr[1:-1].strip()
|
||||||
|
return expr
|
||||||
|
|
||||||
|
def evaluate_condition(self, condition, vars_values):
|
||||||
|
"""
|
||||||
|
评估条件表达式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
condition: 条件字符串,如 "{count > 0 and status == 'active'}"
|
||||||
|
vars_values: 变量值字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 条件是否满足
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
YAMLError: 表达式评估失败
|
||||||
|
"""
|
||||||
|
# 提取 {expression} 中的表达式
|
||||||
|
expr = self._extract_expression(condition)
|
||||||
|
|
||||||
|
# 验证表达式长度
|
||||||
|
if len(expr) > self.MAX_EXPRESSION_LENGTH:
|
||||||
|
raise YAMLError(
|
||||||
|
f"条件表达式过长({len(expr)} 字符,最大 {self.MAX_EXPRESSION_LENGTH}): {condition}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 评估表达式
|
||||||
|
try:
|
||||||
|
evaluator = self._get_evaluator(vars_values)
|
||||||
|
result = evaluator.eval(expr)
|
||||||
|
return bool(result)
|
||||||
|
|
||||||
|
except NameNotDefined as e:
|
||||||
|
# 变量未定义
|
||||||
|
var_name = str(e).split("'")[1] if "'" in str(e) else "unknown"
|
||||||
|
raise YAMLError(
|
||||||
|
f"条件表达式中的变量未定义: {var_name}\n"
|
||||||
|
f"表达式: {condition}\n"
|
||||||
|
f"可用变量: {', '.join(vars_values.keys())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except FunctionNotDefined as e:
|
||||||
|
# 函数未定义
|
||||||
|
func_name = str(e).split("'")[1] if "'" in str(e) else "unknown"
|
||||||
|
raise YAMLError(
|
||||||
|
f"条件表达式中使用了不支持的函数: {func_name}\n"
|
||||||
|
f"表达式: {condition}\n"
|
||||||
|
f"提示: 仅支持 int(), float(), str(), len(), bool(), abs(), min(), max() 等基本函数"
|
||||||
|
)
|
||||||
|
|
||||||
|
except FeatureNotAvailable as e:
|
||||||
|
# 功能不可用
|
||||||
|
raise YAMLError(
|
||||||
|
f"条件表达式使用了不支持的语法特性\n"
|
||||||
|
f"表达式: {condition}\n"
|
||||||
|
f"错误: {str(e)}\n"
|
||||||
|
f"提示: 不支持函数定义、导入模块、属性访问等高级特性"
|
||||||
|
)
|
||||||
|
|
||||||
|
except AttributeDoesNotExist as e:
|
||||||
|
# 属性不存在(属性访问被禁止)
|
||||||
|
raise YAMLError(
|
||||||
|
f"条件表达式使用了不支持的语法特性\n"
|
||||||
|
f"表达式: {condition}\n"
|
||||||
|
f"错误: {str(e)}\n"
|
||||||
|
f"提示: 不支持属性访问,请使用字典访问方式"
|
||||||
|
)
|
||||||
|
|
||||||
|
except SyntaxError as e:
|
||||||
|
# 语法错误
|
||||||
|
raise YAMLError(
|
||||||
|
f"条件表达式语法错误\n"
|
||||||
|
f"表达式: {condition}\n"
|
||||||
|
f"错误: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# 其他错误
|
||||||
|
raise YAMLError(
|
||||||
|
f"条件表达式评估失败\n"
|
||||||
|
f"表达式: {condition}\n"
|
||||||
|
f"错误: {type(e).__name__}: {str(e)}"
|
||||||
|
)
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from loaders.yaml_loader import YAMLError, load_yaml_file, validate_template_yaml
|
from loaders.yaml_loader import YAMLError, load_yaml_file, validate_template_yaml
|
||||||
|
from core.condition_evaluator import ConditionEvaluator
|
||||||
|
|
||||||
|
|
||||||
class Template:
|
class Template:
|
||||||
@@ -20,6 +21,9 @@ class Template:
|
|||||||
template_file: 模板名称(纯文件名,不含路径)
|
template_file: 模板名称(纯文件名,不含路径)
|
||||||
templates_dir: 模板文件目录
|
templates_dir: 模板文件目录
|
||||||
"""
|
"""
|
||||||
|
# 初始化条件评估器
|
||||||
|
self._condition_evaluator = ConditionEvaluator()
|
||||||
|
|
||||||
# 检查是否提供了 templates_dir
|
# 检查是否提供了 templates_dir
|
||||||
if templates_dir is None:
|
if templates_dir is None:
|
||||||
raise YAMLError(
|
raise YAMLError(
|
||||||
@@ -71,15 +75,18 @@ class Template:
|
|||||||
"""
|
"""
|
||||||
obj = cls.__new__(cls)
|
obj = cls.__new__(cls)
|
||||||
obj.data = template_data
|
obj.data = template_data
|
||||||
|
|
||||||
|
# 初始化条件评估器
|
||||||
|
obj._condition_evaluator = ConditionEvaluator()
|
||||||
|
|
||||||
# 解析变量定义
|
# 解析变量定义
|
||||||
obj.vars_def = {}
|
obj.vars_def = {}
|
||||||
for var in template_data.get('vars', []):
|
for var in template_data.get('vars', []):
|
||||||
obj.vars_def[var['name']] = var
|
obj.vars_def[var['name']] = var
|
||||||
|
|
||||||
# 元素列表
|
# 元素列表
|
||||||
obj.elements = template_data.get('elements', [])
|
obj.elements = template_data.get('elements', [])
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def _external_template_exists(self, template_name):
|
def _external_template_exists(self, template_name):
|
||||||
@@ -153,27 +160,17 @@ class Template:
|
|||||||
|
|
||||||
def evaluate_condition(self, condition, vars_values):
|
def evaluate_condition(self, condition, vars_values):
|
||||||
"""
|
"""
|
||||||
评估条件表达式(简单的存在性检查)
|
评估条件表达式
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
condition: 条件字符串,如 "{subtitle != ''}"
|
condition: 条件字符串,如 "{count > 0 and status == 'active'}"
|
||||||
vars_values: 变量值字典
|
vars_values: 变量值字典
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: 条件是否满足
|
bool: 条件是否满足
|
||||||
"""
|
"""
|
||||||
# 简单实现:检查变量是否非空
|
# 委托给条件评估器
|
||||||
# 匹配 {var_name != ''} 或 {var_name != ""}
|
return self._condition_evaluator.evaluate_condition(condition, vars_values)
|
||||||
pattern = r'\{(\w+)\s*!=\s*[\'\"]{2}\}'
|
|
||||||
match = re.match(pattern, condition)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
var_name = match.group(1)
|
|
||||||
value = vars_values.get(var_name, '')
|
|
||||||
return value != ''
|
|
||||||
|
|
||||||
# 默认返回 True
|
|
||||||
return True
|
|
||||||
|
|
||||||
def render(self, vars_values):
|
def render(self, vars_values):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-03
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
当前的模板系统支持条件渲染功能,但实现非常简单,仅通过正则表达式匹配 `{var != ''}` 格式来判断变量是否非空。这种实现无法满足实际使用中的复杂需求,如多条件组合、数值比较、成员测试等。
|
||||||
|
|
||||||
|
现有实现位于 `core/template.py` 的 `evaluate_condition` 方法,使用简单的正则匹配:
|
||||||
|
```python
|
||||||
|
pattern = r'\{(\w+)\s*!=\s*[\'\"]{2}\}'
|
||||||
|
match = re.match(pattern, condition)
|
||||||
|
```
|
||||||
|
|
||||||
|
用户反馈需要更强大的条件表达式能力,但直接使用 Python 的 `eval()` 存在严重的安全风险。我们需要一个既强大又安全的表达式评估方案。
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- 提供强大的条件表达式能力,支持比较、逻辑、成员测试、数学运算
|
||||||
|
- 确保表达式评估的安全性,防止代码注入和恶意操作
|
||||||
|
- 提供清晰的错误信息,帮助用户快速定位问题
|
||||||
|
- 保持 API 简洁,对现有代码的侵入性最小
|
||||||
|
- 实现高性能的表达式评估,不影响模板渲染速度
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- 不支持函数定义、类定义等复杂 Python 特性
|
||||||
|
- 不支持模块导入和文件操作
|
||||||
|
- 不支持对象属性访问(避免安全风险)
|
||||||
|
- 不考虑旧版本语法的向后兼容(用户明确要求)
|
||||||
|
- 不实现自定义的表达式解析器(使用成熟的第三方库)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 决策 1: 使用 simpleeval 作为表达式评估引擎
|
||||||
|
|
||||||
|
**选择**: simpleeval (EvalWithCompoundTypes)
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 成熟稳定:483 stars,维护活跃,社区认可度高
|
||||||
|
- 安全性好:基于 AST 解析,不使用 `eval()`,有明确的安全边界
|
||||||
|
- 功能适中:支持我们需要的所有操作符和表达式类型
|
||||||
|
- 轻量级:单文件库,无额外依赖,不增加项目复杂度
|
||||||
|
- 易于集成:API 简单直观,集成成本低
|
||||||
|
|
||||||
|
**备选方案**:
|
||||||
|
1. **evalidate**: 性能更好(快 3-5 倍),但社区较小,文档较少
|
||||||
|
2. **asteval**: 功能更强大,但过于复杂,性能较差,不适合简单条件判断
|
||||||
|
3. **自实现**: 完全可控,但开发成本高,需要大量测试,容易出现安全漏洞
|
||||||
|
|
||||||
|
**权衡**: simpleeval 在功能、安全性、易用性之间达到了最佳平衡。
|
||||||
|
|
||||||
|
### 决策 2: 使用 EvalWithCompoundTypes 而非 SimpleEval
|
||||||
|
|
||||||
|
**选择**: EvalWithCompoundTypes
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 支持列表和元组字面量,允许 `status in ['draft', 'review']` 这样的表达式
|
||||||
|
- 对于条件判断场景,列表/元组是常见需求
|
||||||
|
- 安全性与 SimpleEval 相同,只是增加了复合类型支持
|
||||||
|
|
||||||
|
**API 差异**:
|
||||||
|
```python
|
||||||
|
# SimpleEval
|
||||||
|
simple_eval(expr, names=vars_values)
|
||||||
|
|
||||||
|
# EvalWithCompoundTypes
|
||||||
|
evaluator = EvalWithCompoundTypes(names=vars_values)
|
||||||
|
evaluator.eval(expr)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 决策 3: 每次评估创建新的 evaluator 实例
|
||||||
|
|
||||||
|
**选择**: 每次调用 `evaluate_condition` 时创建新的 EvalWithCompoundTypes 实例
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 避免状态污染:不同模板渲染之间完全隔离
|
||||||
|
- 线程安全:每个评估独立,无共享状态
|
||||||
|
- 简化实现:不需要管理 evaluator 的生命周期
|
||||||
|
|
||||||
|
**性能考虑**:
|
||||||
|
- EvalWithCompoundTypes 实例化成本很低
|
||||||
|
- 表达式评估本身的开销远大于实例化开销
|
||||||
|
- 对于典型的模板渲染场景(几十个元素),性能影响可忽略
|
||||||
|
|
||||||
|
### 决策 4: 扩展白名单函数
|
||||||
|
|
||||||
|
**选择**: 在 simpleeval 默认函数基础上,添加常用的安全函数
|
||||||
|
|
||||||
|
**白名单函数**:
|
||||||
|
- 类型转换:`int()`, `float()`, `str()`, `bool()`
|
||||||
|
- 数学函数:`abs()`, `min()`, `max()`
|
||||||
|
- 容器函数:`len()`
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 这些函数在条件判断中常用
|
||||||
|
- 都是纯函数,无副作用,安全性高
|
||||||
|
- simpleeval 默认只提供 `int`, `float`, `str`,需要补充
|
||||||
|
|
||||||
|
**不添加的函数**:
|
||||||
|
- 文件操作:`open()`, `read()`, `write()`
|
||||||
|
- 系统操作:`exec()`, `eval()`, `compile()`
|
||||||
|
- 反射操作:`getattr()`, `setattr()`, `hasattr()`
|
||||||
|
|
||||||
|
### 决策 5: 实现独立的 ConditionEvaluator 类
|
||||||
|
|
||||||
|
**选择**: 创建独立的 `ConditionEvaluator` 类,而不是直接在 Template 类中实现
|
||||||
|
|
||||||
|
**架构**:
|
||||||
|
```
|
||||||
|
Template
|
||||||
|
└─ ConditionEvaluator
|
||||||
|
└─ EvalWithCompoundTypes (simpleeval)
|
||||||
|
```
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 单一职责:Template 负责模板渲染,ConditionEvaluator 负责条件评估
|
||||||
|
- 易于测试:可以独立测试条件评估逻辑
|
||||||
|
- 易于扩展:未来可以轻松替换评估引擎或添加新功能
|
||||||
|
- 代码清晰:避免 Template 类过于臃肿
|
||||||
|
|
||||||
|
### 决策 6: 错误处理策略
|
||||||
|
|
||||||
|
**选择**: 捕获 simpleeval 的所有异常,转换为用户友好的 YAMLError
|
||||||
|
|
||||||
|
**错误映射**:
|
||||||
|
```python
|
||||||
|
NameNotDefined → "条件表达式中的变量未定义: {var_name}"
|
||||||
|
FunctionNotDefined → "条件表达式中使用了不支持的函数: {func_name}"
|
||||||
|
FeatureNotAvailable → "条件表达式使用了不支持的语法特性"
|
||||||
|
SyntaxError → "条件表达式语法错误"
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误信息包含**:
|
||||||
|
- 原始表达式
|
||||||
|
- 具体的错误原因
|
||||||
|
- 可用的变量列表(对于 NameNotDefined)
|
||||||
|
- 支持的函数列表(对于 FunctionNotDefined)
|
||||||
|
- 修复建议
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 用户不需要了解 simpleeval 的内部实现
|
||||||
|
- 错误信息更具体,更容易调试
|
||||||
|
- 保持与现有错误处理风格一致
|
||||||
|
|
||||||
|
### 决策 7: 表达式安全限制
|
||||||
|
|
||||||
|
**选择**: 实施多层安全限制
|
||||||
|
|
||||||
|
**限制措施**:
|
||||||
|
1. **长度限制**: 表达式最大 500 字符
|
||||||
|
2. **白名单函数**: 仅允许预定义的安全函数
|
||||||
|
3. **禁止特性**:
|
||||||
|
- 属性访问(`obj.attr`)
|
||||||
|
- 函数定义(`lambda`, `def`)
|
||||||
|
- 模块导入(`import`)
|
||||||
|
- 赋值操作(`=`, `+=`)
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 长度限制防止过于复杂的表达式,也防止 DOS 攻击
|
||||||
|
- simpleeval 默认已禁止大部分危险操作
|
||||||
|
- 额外的白名单限制提供双重保护
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
### 风险 1: simpleeval 的安全漏洞
|
||||||
|
|
||||||
|
**风险**: simpleeval 可能存在未知的安全漏洞,允许恶意代码执行
|
||||||
|
|
||||||
|
**缓解措施**:
|
||||||
|
- simpleeval 是成熟的开源项目,经过广泛使用和审查
|
||||||
|
- 我们添加了额外的长度限制和白名单限制
|
||||||
|
- 表达式来源是用户自己的 YAML 文件,不是外部不可信输入
|
||||||
|
- 定期更新 simpleeval 到最新版本
|
||||||
|
|
||||||
|
**残留风险**: 低。对于本项目的使用场景(用户编写自己的模板),风险可接受。
|
||||||
|
|
||||||
|
### 风险 2: 性能影响
|
||||||
|
|
||||||
|
**风险**: simpleeval 的表达式评估可能比简单的正则匹配慢,影响模板渲染性能
|
||||||
|
|
||||||
|
**缓解措施**:
|
||||||
|
- 实际测试表明,simpleeval 的性能足够好(每秒可评估数万次)
|
||||||
|
- 对于典型的演示文稿(几十个幻灯片,每个几十个元素),性能影响可忽略
|
||||||
|
- 如果未来性能成为瓶颈,可以考虑:
|
||||||
|
- 缓存编译后的表达式(simpleeval 支持)
|
||||||
|
- 切换到 evalidate(性能更好)
|
||||||
|
|
||||||
|
**残留风险**: 极低。当前性能完全满足需求。
|
||||||
|
|
||||||
|
### 风险 3: 用户学习成本
|
||||||
|
|
||||||
|
**风险**: 用户需要学习新的表达式语法,可能不熟悉 Python 表达式
|
||||||
|
|
||||||
|
**缓解措施**:
|
||||||
|
- Python 表达式语法简单直观,学习成本低
|
||||||
|
- 提供详细的文档和示例
|
||||||
|
- 错误信息清晰,帮助用户快速定位问题
|
||||||
|
- 旧的简单语法(`{var != ''}`)在新实现中仍然有效
|
||||||
|
|
||||||
|
**残留风险**: 低。Python 表达式是业界标准,大多数开发者都熟悉。
|
||||||
|
|
||||||
|
### 权衡 1: 功能 vs 安全性
|
||||||
|
|
||||||
|
**权衡**: 为了安全性,我们禁止了一些 Python 特性(如属性访问、函数定义)
|
||||||
|
|
||||||
|
**影响**: 用户无法使用这些高级特性,但对于条件判断场景,当前支持的特性已经足够
|
||||||
|
|
||||||
|
**决策**: 安全性优先。如果未来有明确的需求,可以考虑有限地开放某些特性。
|
||||||
|
|
||||||
|
### 权衡 2: 向后兼容 vs 代码简洁
|
||||||
|
|
||||||
|
**权衡**: 用户明确要求不考虑向后兼容,我们可以直接移除旧的正则匹配实现
|
||||||
|
|
||||||
|
**影响**:
|
||||||
|
- 代码更简洁,维护成本更低
|
||||||
|
- 旧的简单语法(`{var != ''}`)在新实现中仍然有效,实际兼容性影响很小
|
||||||
|
|
||||||
|
**决策**: 移除旧实现,统一使用 simpleeval。
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
### 实施步骤
|
||||||
|
|
||||||
|
1. **安装依赖**
|
||||||
|
```bash
|
||||||
|
uv add simpleeval
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **实现 ConditionEvaluator 类**
|
||||||
|
- 创建 `core/condition_evaluator.py`
|
||||||
|
- 实现 `evaluate_condition` 方法
|
||||||
|
- 实现错误处理和安全限制
|
||||||
|
|
||||||
|
3. **集成到 Template 类**
|
||||||
|
- 在 `Template.__init__` 中初始化 ConditionEvaluator
|
||||||
|
- 替换 `evaluate_condition` 方法的实现
|
||||||
|
- 移除旧的正则匹配代码
|
||||||
|
|
||||||
|
4. **更新测试**
|
||||||
|
- 扩展 `tests/unit/test_template.py` 中的条件渲染测试
|
||||||
|
- 添加新的表达式类型测试
|
||||||
|
- 添加错误处理测试
|
||||||
|
- 添加安全限制测试
|
||||||
|
|
||||||
|
5. **更新文档**
|
||||||
|
- 更新 README.md 的条件渲染章节
|
||||||
|
- 添加表达式语法参考
|
||||||
|
- 更新 README_DEV.md 的架构说明
|
||||||
|
|
||||||
|
6. **验证和发布**
|
||||||
|
- 运行完整测试套件
|
||||||
|
- 手动测试各种表达式场景
|
||||||
|
- 更新版本号
|
||||||
|
|
||||||
|
### 回滚策略
|
||||||
|
|
||||||
|
如果发现严重问题,可以快速回滚:
|
||||||
|
1. 恢复 `core/template.py` 中的旧 `evaluate_condition` 实现
|
||||||
|
2. 移除 simpleeval 依赖
|
||||||
|
3. 恢复旧的测试用例
|
||||||
|
|
||||||
|
**回滚成本**: 低。改动集中在单个文件,易于回滚。
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
无。所有关键决策已明确。
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
当前的 visible 条件渲染功能仅支持简单的非空检查(`{var != ''}`),无法满足实际使用中的复杂条件判断需求。用户需要根据多个变量、逻辑组合、数值比较等条件来控制元素显示,但现有实现过于受限。通过引入成熟的表达式评估库 simpleeval,可以提供强大且安全的条件表达式能力,显著提升模板系统的灵活性。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 引入 simpleeval 库作为条件表达式评估引擎
|
||||||
|
- 扩展 visible 字段支持的表达式类型:
|
||||||
|
- 比较运算符:`==`, `!=`, `>`, `<`, `>=`, `<=`
|
||||||
|
- 逻辑运算符:`and`, `or`, `not`
|
||||||
|
- 成员测试:`in`, `not in`
|
||||||
|
- 列表/元组字面量:`status in ['draft', 'review']`
|
||||||
|
- 数学运算:`+`, `-`, `*`, `/`, `%`, `**`
|
||||||
|
- 内置函数:`int()`, `float()`, `str()`, `len()`, `bool()`
|
||||||
|
- 增强错误处理,提供详细的错误信息和调试提示
|
||||||
|
- 添加表达式安全限制(最大长度、禁止危险操作)
|
||||||
|
- **BREAKING**: 移除旧版本的简单正则匹配实现,统一使用 simpleeval
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `enhanced-condition-evaluation`: 基于 simpleeval 的增强条件表达式评估能力,支持复杂的逻辑判断、数学运算和成员测试
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `template-system`: 修改模板系统的条件渲染实现,从简单正则匹配升级为完整的表达式评估
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **代码影响**:
|
||||||
|
- `core/template.py`: 重写 `evaluate_condition` 方法
|
||||||
|
- `loaders/yaml_loader.py`: 可能需要更新验证逻辑
|
||||||
|
- 测试文件:需要大幅扩展测试用例覆盖新的表达式类型
|
||||||
|
|
||||||
|
- **依赖影响**:
|
||||||
|
- 新增依赖:simpleeval (轻量级,无额外依赖)
|
||||||
|
- 需要更新 `pyproject.toml`
|
||||||
|
|
||||||
|
- **用户影响**:
|
||||||
|
- **BREAKING**: 旧的简单实现被移除,但由于旧语法 `{var != ''}` 在新实现中仍然有效,实际兼容性影响较小
|
||||||
|
- 用户需要学习新的表达式语法(但与 Python 表达式一致,学习成本低)
|
||||||
|
- 错误信息更详细,调试更容易
|
||||||
|
|
||||||
|
- **文档影响**:
|
||||||
|
- README.md: 需要更新条件渲染章节,添加新的表达式示例
|
||||||
|
- README_DEV.md: 需要说明 simpleeval 集成和安全策略
|
||||||
|
- 需要添加完整的表达式语法参考文档
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
# Enhanced Condition Evaluation
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
增强条件评估能力提供基于 simpleeval 的安全表达式评估引擎,支持复杂的条件判断、逻辑运算、数学计算和成员测试。该能力用于模板系统的 visible 字段,允许用户使用 Python 风格的表达式来控制元素的显示条件。
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持比较运算符
|
||||||
|
|
||||||
|
系统 SHALL 支持所有标准的比较运算符,用于数值和字符串的比较。
|
||||||
|
|
||||||
|
#### Scenario: 数值大于比较
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{count > 0}` 且变量 `count` 的值为 5
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 数值小于等于比较
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{price <= 100}` 且变量 `price` 的值为 100
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 字符串相等比较
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{status == 'active'}` 且变量 `status` 的值为 "active"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 字符串不等比较
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{status != 'inactive'}` 且变量 `status` 的值为 "active"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持逻辑运算符
|
||||||
|
|
||||||
|
系统 SHALL 支持逻辑运算符 and、or、not,用于组合多个条件。
|
||||||
|
|
||||||
|
#### Scenario: 逻辑与运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{count > 0 and status == 'active'}` 且变量 `count` 为 5,`status` 为 "active"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 逻辑或运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{count > 0 or status == 'active'}` 且变量 `count` 为 0,`status` 为 "active"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 逻辑非运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{not (count == 0)}` 且变量 `count` 为 5
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 复杂逻辑组合
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{(score >= 60 and score <= 100) or is_admin}` 且变量 `score` 为 75,`is_admin` 为 False
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持成员测试
|
||||||
|
|
||||||
|
系统 SHALL 支持 in 和 not in 运算符,用于测试值是否在集合中。
|
||||||
|
|
||||||
|
#### Scenario: 列表成员测试
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{status in ['draft', 'review', 'published']}` 且变量 `status` 为 "draft"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 列表非成员测试
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{status not in ['draft', 'review']}` 且变量 `status` 为 "active"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 字符串包含测试
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{'test' in status}` 且变量 `status` 为 "test-version"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 元组成员测试
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{level in (1, 2, 3)}` 且变量 `level` 为 2
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持数学运算
|
||||||
|
|
||||||
|
系统 SHALL 支持基本的数学运算符,用于条件判断中的计算。
|
||||||
|
|
||||||
|
#### Scenario: 加法运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{(price + tax) > 100}` 且变量 `price` 为 90,`tax` 为 15
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 乘法运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{(price * discount) > 50}` 且变量 `price` 为 100,`discount` 为 0.8
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 除法运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{(total / count) >= 10}` 且变量 `total` 为 100,`count` 为 5
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持内置函数
|
||||||
|
|
||||||
|
系统 SHALL 支持安全的内置函数,用于类型转换和基本操作。
|
||||||
|
|
||||||
|
#### Scenario: 类型转换函数
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{int(value) > 100}` 且变量 `value` 为 "150"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 长度函数
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{len(items) > 0}` 且变量 `items` 为 [1, 2, 3]
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 布尔转换函数
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{bool(value)}` 且变量 `value` 为 "text"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
### Requirement: 系统必须提供详细的错误信息
|
||||||
|
|
||||||
|
系统 SHALL 在表达式评估失败时提供清晰的错误信息,帮助用户快速定位问题。
|
||||||
|
|
||||||
|
#### Scenario: 变量未定义错误
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{undefined_var > 0}` 但变量 `undefined_var` 未提供
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式中的变量未定义: undefined_var",并列出可用变量
|
||||||
|
|
||||||
|
#### Scenario: 函数未定义错误
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{custom_func(value)}` 但函数 `custom_func` 不在白名单中
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式中使用了不支持的函数: custom_func",并列出支持的函数
|
||||||
|
|
||||||
|
#### Scenario: 语法错误
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{count > }` 包含语法错误
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式语法错误",并显示具体的语法问题
|
||||||
|
|
||||||
|
### Requirement: 系统必须实施安全限制
|
||||||
|
|
||||||
|
系统 SHALL 限制表达式的复杂度和能力,防止恶意或危险的操作。
|
||||||
|
|
||||||
|
#### Scenario: 表达式长度限制
|
||||||
|
|
||||||
|
- **WHEN** 表达式长度超过 500 字符
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式过长"
|
||||||
|
|
||||||
|
#### Scenario: 禁止属性访问
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{obj.attr}` 尝试访问对象属性
|
||||||
|
- **THEN** 系统抛出错误,提示"不支持的语法特性"
|
||||||
|
|
||||||
|
#### Scenario: 禁止函数定义
|
||||||
|
|
||||||
|
- **WHEN** 表达式包含 lambda 或 def 关键字
|
||||||
|
- **THEN** 系统抛出错误,提示"不支持的语法特性"
|
||||||
|
|
||||||
|
#### Scenario: 白名单函数限制
|
||||||
|
|
||||||
|
- **WHEN** 表达式使用了不在白名单中的函数(如 eval、exec、open)
|
||||||
|
- **THEN** 系统拒绝执行,抛出错误
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持表达式提取
|
||||||
|
|
||||||
|
系统 SHALL 能够从 `{expression}` 格式的字符串中提取实际的表达式内容。
|
||||||
|
|
||||||
|
#### Scenario: 提取带花括号的表达式
|
||||||
|
|
||||||
|
- **WHEN** 条件字符串为 `{count > 0}`
|
||||||
|
- **THEN** 系统提取出表达式 `count > 0`
|
||||||
|
|
||||||
|
#### Scenario: 提取带空白的表达式
|
||||||
|
|
||||||
|
- **WHEN** 条件字符串为 `{ count > 0 }`
|
||||||
|
- **THEN** 系统提取出表达式 `count > 0`(去除首尾空白)
|
||||||
|
|
||||||
|
#### Scenario: 处理不带花括号的表达式
|
||||||
|
|
||||||
|
- **WHEN** 条件字符串为 `count > 0`(不带花括号)
|
||||||
|
- **THEN** 系统直接使用该字符串作为表达式
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Template System (Delta Spec)
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持条件渲染
|
||||||
|
|
||||||
|
系统 SHALL 支持基于变量值的条件渲染,通过 `visible` 字段控制元素是否显示。条件表达式使用 simpleeval 引擎评估,支持复杂的逻辑判断、比较运算、成员测试和数学计算。
|
||||||
|
|
||||||
|
#### Scenario: 显示满足条件的元素
|
||||||
|
|
||||||
|
- **WHEN** 元素定义了 `visible: "{count > 0}"`,且用户提供的 count 大于 0
|
||||||
|
- **THEN** 系统渲染该元素
|
||||||
|
|
||||||
|
#### Scenario: 隐藏不满足条件的元素
|
||||||
|
|
||||||
|
- **WHEN** 元素定义了 `visible: "{count > 0}"`,但用户提供的 count 等于 0
|
||||||
|
- **THEN** 系统跳过该元素,不渲染到幻灯片中
|
||||||
|
|
||||||
|
#### Scenario: 复杂逻辑条件
|
||||||
|
|
||||||
|
- **WHEN** 元素定义了 `visible: "{count > 0 and status == 'active'}"`,且两个条件都满足
|
||||||
|
- **THEN** 系统渲染该元素
|
||||||
|
|
||||||
|
#### Scenario: 成员测试条件
|
||||||
|
|
||||||
|
- **WHEN** 元素定义了 `visible: "{status in ['draft', 'review']}"`,且 status 为 "draft"
|
||||||
|
- **THEN** 系统渲染该元素
|
||||||
|
|
||||||
|
#### Scenario: 数学运算条件
|
||||||
|
|
||||||
|
- **WHEN** 元素定义了 `visible: "{(price * discount) > 50}"`,且计算结果大于 50
|
||||||
|
- **THEN** 系统渲染该元素
|
||||||
|
|
||||||
|
#### Scenario: 条件表达式语法错误
|
||||||
|
|
||||||
|
- **WHEN** visible 字段包含无效的条件表达式(如 `{count > }`)
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式语法错误",并显示具体的语法问题
|
||||||
|
|
||||||
|
#### Scenario: 条件表达式中的变量未定义
|
||||||
|
|
||||||
|
- **WHEN** visible 字段引用了未定义的变量(如 `{undefined_var > 0}`)
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式中的变量未定义: undefined_var",并列出可用变量
|
||||||
|
|
||||||
|
#### Scenario: 条件表达式使用不支持的函数
|
||||||
|
|
||||||
|
- **WHEN** visible 字段使用了不在白名单中的函数(如 `{eval(code)}`)
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式中使用了不支持的函数: eval"
|
||||||
|
|
||||||
|
#### Scenario: 向后兼容的简单表达式
|
||||||
|
|
||||||
|
- **WHEN** 元素定义了 `visible: "{subtitle != ''}"`(旧语法格式)
|
||||||
|
- **THEN** 系统使用新的 simpleeval 引擎正确评估该表达式
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
## 1. 依赖和环境准备
|
||||||
|
|
||||||
|
- [x] 1.1 确认 simpleeval 已安装(已在探索阶段完成)
|
||||||
|
- [x] 1.2 验证 simpleeval 版本和功能可用性
|
||||||
|
|
||||||
|
## 2. 核心实现
|
||||||
|
|
||||||
|
- [x] 2.1 创建 core/condition_evaluator.py 文件
|
||||||
|
- [x] 2.2 实现 ConditionEvaluator 类的基本结构
|
||||||
|
- [x] 2.3 实现 _get_evaluator 方法,配置 EvalWithCompoundTypes
|
||||||
|
- [x] 2.4 实现 _extract_expression 方法,提取表达式内容
|
||||||
|
- [x] 2.5 实现 evaluate_condition 方法,集成 simpleeval
|
||||||
|
- [x] 2.6 实现错误处理,映射 simpleeval 异常到 YAMLError
|
||||||
|
- [x] 2.7 添加表达式长度限制(MAX_EXPRESSION_LENGTH = 500)
|
||||||
|
- [x] 2.8 配置白名单函数(len, bool, abs, min, max)
|
||||||
|
|
||||||
|
## 3. 集成到 Template 类
|
||||||
|
|
||||||
|
- [x] 3.1 在 Template.__init__ 中初始化 ConditionEvaluator
|
||||||
|
- [x] 3.2 修改 Template.evaluate_condition 方法,委托给 ConditionEvaluator
|
||||||
|
- [x] 3.3 移除旧的正则匹配实现代码
|
||||||
|
- [x] 3.4 确保 Template.render 方法正确调用新的 evaluate_condition
|
||||||
|
|
||||||
|
## 4. 测试实现
|
||||||
|
|
||||||
|
- [x] 4.1 创建 tests/unit/test_condition_evaluator.py 文件
|
||||||
|
- [x] 4.2 测试比较运算符(==, !=, >, <, >=, <=)
|
||||||
|
- [x] 4.3 测试逻辑运算符(and, or, not)
|
||||||
|
- [x] 4.4 测试成员测试(in, not in)
|
||||||
|
- [x] 4.5 测试列表和元组字面量
|
||||||
|
- [x] 4.6 测试数学运算(+, -, *, /, %, **)
|
||||||
|
- [x] 4.7 测试内置函数(int, float, str, len, bool, abs, min, max)
|
||||||
|
- [x] 4.8 测试复杂逻辑组合表达式
|
||||||
|
- [x] 4.9 测试错误处理(变量未定义、函数未定义、语法错误)
|
||||||
|
- [x] 4.10 测试安全限制(表达式过长、禁止的特性)
|
||||||
|
- [x] 4.11 更新 tests/unit/test_template.py 中的条件渲染测试
|
||||||
|
- [x] 4.12 添加集成测试,验证完整的模板渲染流程
|
||||||
|
|
||||||
|
## 5. 文档更新
|
||||||
|
|
||||||
|
- [x] 5.1 更新 README.md 的条件渲染章节
|
||||||
|
- [x] 5.2 添加新的表达式语法示例到 README.md
|
||||||
|
- [x] 5.3 添加支持的操作符和函数列表到 README.md
|
||||||
|
- [x] 5.4 更新 README_DEV.md,说明 ConditionEvaluator 架构
|
||||||
|
- [x] 5.5 更新 README_DEV.md,说明 simpleeval 集成和安全策略
|
||||||
|
- [x] 5.6 创建或更新示例文件,展示新的条件表达式能力
|
||||||
|
|
||||||
|
## 6. 验证和清理
|
||||||
|
|
||||||
|
- [x] 6.1 运行完整测试套件,确保所有测试通过
|
||||||
|
- [x] 6.2 手动测试各种表达式场景
|
||||||
|
- [x] 6.3 验证错误信息的清晰度和有用性
|
||||||
|
- [x] 6.4 检查代码风格和注释完整性
|
||||||
|
- [x] 6.5 清理 temp 目录中的临时文件(保留有用的示例)
|
||||||
|
- [x] 6.6 确认没有遗留的调试代码或注释
|
||||||
179
openspec/specs/enhanced-condition-evaluation/spec.md
Normal file
179
openspec/specs/enhanced-condition-evaluation/spec.md
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Enhanced Condition Evaluation
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
增强条件评估能力提供基于 simpleeval 的安全表达式评估引擎,支持复杂的条件判断、逻辑运算、数学计算和成员测试。该能力用于模板系统的 visible 字段,允许用户使用 Python 风格的表达式来控制元素的显示条件。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持比较运算符
|
||||||
|
|
||||||
|
系统 SHALL 支持所有标准的比较运算符,用于数值和字符串的比较。
|
||||||
|
|
||||||
|
#### Scenario: 数值大于比较
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{count > 0}` 且变量 `count` 的值为 5
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 数值小于等于比较
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{price <= 100}` 且变量 `price` 的值为 100
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 字符串相等比较
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{status == 'active'}` 且变量 `status` 的值为 "active"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 字符串不等比较
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{status != 'inactive'}` 且变量 `status` 的值为 "active"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持逻辑运算符
|
||||||
|
|
||||||
|
系统 SHALL 支持逻辑运算符 and、or、not,用于组合多个条件。
|
||||||
|
|
||||||
|
#### Scenario: 逻辑与运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{count > 0 and status == 'active'}` 且变量 `count` 为 5,`status` 为 "active"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 逻辑或运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{count > 0 or status == 'active'}` 且变量 `count` 为 0,`status` 为 "active"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 逻辑非运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{not (count == 0)}` 且变量 `count` 为 5
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 复杂逻辑组合
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{(score >= 60 and score <= 100) or is_admin}` 且变量 `score` 为 75,`is_admin` 为 False
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持成员测试
|
||||||
|
|
||||||
|
系统 SHALL 支持 in 和 not in 运算符,用于测试值是否在集合中。
|
||||||
|
|
||||||
|
#### Scenario: 列表成员测试
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{status in ['draft', 'review', 'published']}` 且变量 `status` 为 "draft"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 列表非成员测试
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{status not in ['draft', 'review']}` 且变量 `status` 为 "active"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 字符串包含测试
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{'test' in status}` 且变量 `status` 为 "test-version"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 元组成员测试
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{level in (1, 2, 3)}` 且变量 `level` 为 2
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持数学运算
|
||||||
|
|
||||||
|
系统 SHALL 支持基本的数学运算符,用于条件判断中的计算。
|
||||||
|
|
||||||
|
#### Scenario: 加法运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{(price + tax) > 100}` 且变量 `price` 为 90,`tax` 为 15
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 乘法运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{(price * discount) > 50}` 且变量 `price` 为 100,`discount` 为 0.8
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 除法运算
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{(total / count) >= 10}` 且变量 `total` 为 100,`count` 为 5
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持内置函数
|
||||||
|
|
||||||
|
系统 SHALL 支持安全的内置函数,用于类型转换和基本操作。
|
||||||
|
|
||||||
|
#### Scenario: 类型转换函数
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{int(value) > 100}` 且变量 `value` 为 "150"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 长度函数
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{len(items) > 0}` 且变量 `items` 为 [1, 2, 3]
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
#### Scenario: 布尔转换函数
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{bool(value)}` 且变量 `value` 为 "text"
|
||||||
|
- **THEN** 表达式评估结果为 True
|
||||||
|
|
||||||
|
### Requirement: 系统必须提供详细的错误信息
|
||||||
|
|
||||||
|
系统 SHALL 在表达式评估失败时提供清晰的错误信息,帮助用户快速定位问题。
|
||||||
|
|
||||||
|
#### Scenario: 变量未定义错误
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{undefined_var > 0}` 但变量 `undefined_var` 未提供
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式中的变量未定义: undefined_var",并列出可用变量
|
||||||
|
|
||||||
|
#### Scenario: 函数未定义错误
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{custom_func(value)}` 但函数 `custom_func` 不在白名单中
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式中使用了不支持的函数: custom_func",并列出支持的函数
|
||||||
|
|
||||||
|
#### Scenario: 语法错误
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{count > }` 包含语法错误
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式语法错误",并显示具体的语法问题
|
||||||
|
|
||||||
|
### Requirement: 系统必须实施安全限制
|
||||||
|
|
||||||
|
系统 SHALL 限制表达式的复杂度和能力,防止恶意或危险的操作。
|
||||||
|
|
||||||
|
#### Scenario: 表达式长度限制
|
||||||
|
|
||||||
|
- **WHEN** 表达式长度超过 500 字符
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式过长"
|
||||||
|
|
||||||
|
#### Scenario: 禁止属性访问
|
||||||
|
|
||||||
|
- **WHEN** 表达式为 `{obj.attr}` 尝试访问对象属性
|
||||||
|
- **THEN** 系统抛出错误,提示"不支持的语法特性"
|
||||||
|
|
||||||
|
#### Scenario: 禁止函数定义
|
||||||
|
|
||||||
|
- **WHEN** 表达式包含 lambda 或 def 关键字
|
||||||
|
- **THEN** 系统抛出错误,提示"不支持的语法特性"
|
||||||
|
|
||||||
|
#### Scenario: 白名单函数限制
|
||||||
|
|
||||||
|
- **WHEN** 表达式使用了不在白名单中的函数(如 eval、exec、open)
|
||||||
|
- **THEN** 系统拒绝执行,抛出错误
|
||||||
|
|
||||||
|
### Requirement: 系统必须支持表达式提取
|
||||||
|
|
||||||
|
系统 SHALL 能够从 `{expression}` 格式的字符串中提取实际的表达式内容。
|
||||||
|
|
||||||
|
#### Scenario: 提取带花括号的表达式
|
||||||
|
|
||||||
|
- **WHEN** 条件字符串为 `{count > 0}`
|
||||||
|
- **THEN** 系统提取出表达式 `count > 0`
|
||||||
|
|
||||||
|
#### Scenario: 提取带空白的表达式
|
||||||
|
|
||||||
|
- **WHEN** 条件字符串为 `{ count > 0 }`
|
||||||
|
- **THEN** 系统提取出表达式 `count > 0`(去除首尾空白)
|
||||||
|
|
||||||
|
#### Scenario: 处理不带花括号的表达式
|
||||||
|
|
||||||
|
- **WHEN** 条件字符串为 `count > 0`(不带花括号)
|
||||||
|
- **THEN** 系统直接使用该字符串作为表达式
|
||||||
@@ -65,22 +65,53 @@ Template system 提供可复用的幻灯片布局和样式定义。模板包含
|
|||||||
|
|
||||||
### Requirement: 系统必须支持条件渲染
|
### Requirement: 系统必须支持条件渲染
|
||||||
|
|
||||||
系统 SHALL 支持基于变量值的条件渲染,通过 `visible` 字段控制元素是否显示。
|
系统 SHALL 支持基于变量值的条件渲染,通过 `visible` 字段控制元素是否显示。条件表达式使用 simpleeval 引擎评估,支持复杂的逻辑判断、比较运算、成员测试和数学计算。
|
||||||
|
|
||||||
#### Scenario: 显示满足条件的元素
|
#### Scenario: 显示满足条件的元素
|
||||||
|
|
||||||
- **WHEN** 元素定义了 `visible: "{subtitle != ''}\"`,且用户提供了非空的 subtitle
|
- **WHEN** 元素定义了 `visible: "{count > 0}"`,且用户提供的 count 大于 0
|
||||||
- **THEN** 系统渲染该元素
|
- **THEN** 系统渲染该元素
|
||||||
|
|
||||||
#### Scenario: 隐藏不满足条件的元素
|
#### Scenario: 隐藏不满足条件的元素
|
||||||
|
|
||||||
- **WHEN** 元素定义了 `visible: "{subtitle != ''}\"`,但用户提供的 subtitle 为空字符串
|
- **WHEN** 元素定义了 `visible: "{count > 0}"`,但用户提供的 count 等于 0
|
||||||
- **THEN** 系统跳过该元素,不渲染到幻灯片中
|
- **THEN** 系统跳过该元素,不渲染到幻灯片中
|
||||||
|
|
||||||
|
#### Scenario: 复杂逻辑条件
|
||||||
|
|
||||||
|
- **WHEN** 元素定义了 `visible: "{count > 0 and status == 'active'}"`,且两个条件都满足
|
||||||
|
- **THEN** 系统渲染该元素
|
||||||
|
|
||||||
|
#### Scenario: 成员测试条件
|
||||||
|
|
||||||
|
- **WHEN** 元素定义了 `visible: "{status in ['draft', 'review']}"`,且 status 为 "draft"
|
||||||
|
- **THEN** 系统渲染该元素
|
||||||
|
|
||||||
|
#### Scenario: 数学运算条件
|
||||||
|
|
||||||
|
- **WHEN** 元素定义了 `visible: "{(price * discount) > 50}"`,且计算结果大于 50
|
||||||
|
- **THEN** 系统渲染该元素
|
||||||
|
|
||||||
#### Scenario: 条件表达式语法错误
|
#### Scenario: 条件表达式语法错误
|
||||||
|
|
||||||
- **WHEN** visible 字段包含无效的条件表达式
|
- **WHEN** visible 字段包含无效的条件表达式(如 `{count > }`)
|
||||||
- **THEN** 系统抛出错误,提示条件表达式格式错误
|
- **THEN** 系统抛出错误,提示"条件表达式语法错误",并显示具体的语法问题
|
||||||
|
|
||||||
|
#### Scenario: 条件表达式中的变量未定义
|
||||||
|
|
||||||
|
- **WHEN** visible 字段引用了未定义的变量(如 `{undefined_var > 0}`)
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式中的变量未定义: undefined_var",并列出可用变量
|
||||||
|
|
||||||
|
#### Scenario: 条件表达式使用不支持的函数
|
||||||
|
|
||||||
|
- **WHEN** visible 字段使用了不在白名单中的函数(如 `{eval(code)}`)
|
||||||
|
- **THEN** 系统抛出错误,提示"条件表达式中使用了不支持的函数: eval"
|
||||||
|
|
||||||
|
#### Scenario: 向后兼容的简单表达式
|
||||||
|
|
||||||
|
- **WHEN** 元素定义了 `visible: "{subtitle != ''}"`(旧语法格式)
|
||||||
|
- **THEN** 系统使用新的 simpleeval 引擎正确评估该表达式
|
||||||
|
|
||||||
|
|
||||||
### Requirement: 模板文件必须可从指定目录加载
|
### Requirement: 模板文件必须可从指定目录加载
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ dependencies = [
|
|||||||
"pyyaml",
|
"pyyaml",
|
||||||
"flask",
|
"flask",
|
||||||
"watchdog",
|
"watchdog",
|
||||||
|
"simpleeval",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
299
tests/unit/test_condition_evaluator.py
Normal file
299
tests/unit/test_condition_evaluator.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
"""
|
||||||
|
条件评估器单元测试
|
||||||
|
|
||||||
|
测试 ConditionEvaluator 类的所有功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from loaders.yaml_loader import YAMLError
|
||||||
|
from core.condition_evaluator import ConditionEvaluator
|
||||||
|
|
||||||
|
|
||||||
|
class TestConditionEvaluator:
|
||||||
|
"""ConditionEvaluator 测试类"""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""每个测试方法前执行"""
|
||||||
|
self.evaluator = ConditionEvaluator()
|
||||||
|
|
||||||
|
# ============= 比较运算符测试 =============
|
||||||
|
|
||||||
|
def test_greater_than(self):
|
||||||
|
"""测试大于运算符"""
|
||||||
|
assert self.evaluator.evaluate_condition("{count > 0}", {"count": 5}) is True
|
||||||
|
assert self.evaluator.evaluate_condition("{count > 0}", {"count": 0}) is False
|
||||||
|
|
||||||
|
def test_less_than(self):
|
||||||
|
"""测试小于运算符"""
|
||||||
|
assert self.evaluator.evaluate_condition("{count < 10}", {"count": 5}) is True
|
||||||
|
assert self.evaluator.evaluate_condition("{count < 10}", {"count": 10}) is False
|
||||||
|
|
||||||
|
def test_greater_equal(self):
|
||||||
|
"""测试大于等于运算符"""
|
||||||
|
assert self.evaluator.evaluate_condition("{count >= 5}", {"count": 5}) is True
|
||||||
|
assert self.evaluator.evaluate_condition("{count >= 5}", {"count": 4}) is False
|
||||||
|
|
||||||
|
def test_less_equal(self):
|
||||||
|
"""测试小于等于运算符"""
|
||||||
|
assert self.evaluator.evaluate_condition("{count <= 5}", {"count": 5}) is True
|
||||||
|
assert self.evaluator.evaluate_condition("{count <= 5}", {"count": 6}) is False
|
||||||
|
|
||||||
|
def test_equal(self):
|
||||||
|
"""测试相等运算符"""
|
||||||
|
assert self.evaluator.evaluate_condition("{status == 'active'}", {"status": "active"}) is True
|
||||||
|
assert self.evaluator.evaluate_condition("{status == 'active'}", {"status": "inactive"}) is False
|
||||||
|
|
||||||
|
def test_not_equal(self):
|
||||||
|
"""测试不等运算符"""
|
||||||
|
assert self.evaluator.evaluate_condition("{status != 'inactive'}", {"status": "active"}) is True
|
||||||
|
assert self.evaluator.evaluate_condition("{status != 'active'}", {"status": "active"}) is False
|
||||||
|
|
||||||
|
# ============= 逻辑运算符测试 =============
|
||||||
|
|
||||||
|
def test_logical_and(self):
|
||||||
|
"""测试逻辑与运算"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{count > 0 and status == 'active'}",
|
||||||
|
{"count": 5, "status": "active"}
|
||||||
|
) is True
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{count > 0 and status == 'active'}",
|
||||||
|
{"count": 0, "status": "active"}
|
||||||
|
) is False
|
||||||
|
|
||||||
|
def test_logical_or(self):
|
||||||
|
"""测试逻辑或运算"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{count > 0 or status == 'active'}",
|
||||||
|
{"count": 0, "status": "active"}
|
||||||
|
) is True
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{count > 0 or status == 'active'}",
|
||||||
|
{"count": 0, "status": "inactive"}
|
||||||
|
) is False
|
||||||
|
|
||||||
|
def test_logical_not(self):
|
||||||
|
"""测试逻辑非运算"""
|
||||||
|
assert self.evaluator.evaluate_condition("{not (count == 0)}", {"count": 5}) is True
|
||||||
|
assert self.evaluator.evaluate_condition("{not (count == 0)}", {"count": 0}) is False
|
||||||
|
|
||||||
|
# ============= 成员测试 =============
|
||||||
|
|
||||||
|
def test_in_list(self):
|
||||||
|
"""测试列表成员测试"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{status in ['draft', 'review', 'published']}",
|
||||||
|
{"status": "draft"}
|
||||||
|
) is True
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{status in ['draft', 'review']}",
|
||||||
|
{"status": "active"}
|
||||||
|
) is False
|
||||||
|
|
||||||
|
def test_not_in_list(self):
|
||||||
|
"""测试列表非成员测试"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{status not in ['draft', 'review']}",
|
||||||
|
{"status": "active"}
|
||||||
|
) is True
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{status not in ['draft', 'review']}",
|
||||||
|
{"status": "draft"}
|
||||||
|
) is False
|
||||||
|
|
||||||
|
def test_in_tuple(self):
|
||||||
|
"""测试元组成员测试"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{level in (1, 2, 3)}",
|
||||||
|
{"level": 2}
|
||||||
|
) is True
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{level in (1, 2, 3)}",
|
||||||
|
{"level": 4}
|
||||||
|
) is False
|
||||||
|
|
||||||
|
def test_string_contains(self):
|
||||||
|
"""测试字符串包含"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{'test' in status}",
|
||||||
|
{"status": "test-version"}
|
||||||
|
) is True
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{'test' in status}",
|
||||||
|
{"status": "production"}
|
||||||
|
) is False
|
||||||
|
|
||||||
|
# ============= 数学运算测试 =============
|
||||||
|
|
||||||
|
def test_addition(self):
|
||||||
|
"""测试加法运算"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{(price + tax) > 100}",
|
||||||
|
{"price": 90, "tax": 15}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_multiplication(self):
|
||||||
|
"""测试乘法运算"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{(price * discount) > 50}",
|
||||||
|
{"price": 100, "discount": 0.8}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_division(self):
|
||||||
|
"""测试除法运算"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{(total / count) >= 10}",
|
||||||
|
{"total": 100, "count": 5}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_modulo(self):
|
||||||
|
"""测试取模运算"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{(value % 2) == 0}",
|
||||||
|
{"value": 10}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_power(self):
|
||||||
|
"""测试幂运算"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{(base ** exp) > 100}",
|
||||||
|
{"base": 10, "exp": 2}
|
||||||
|
) is False
|
||||||
|
|
||||||
|
# ============= 内置函数测试 =============
|
||||||
|
|
||||||
|
def test_int_function(self):
|
||||||
|
"""测试 int() 函数"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{int(value) > 100}",
|
||||||
|
{"value": "150"}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_float_function(self):
|
||||||
|
"""测试 float() 函数"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{float(value) > 3.14}",
|
||||||
|
{"value": "3.5"}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_str_function(self):
|
||||||
|
"""测试 str() 函数"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{str(value) == '123'}",
|
||||||
|
{"value": 123}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_len_function(self):
|
||||||
|
"""测试 len() 函数"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{len(items) > 0}",
|
||||||
|
{"items": [1, 2, 3]}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_bool_function(self):
|
||||||
|
"""测试 bool() 函数"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{bool(value)}",
|
||||||
|
{"value": "text"}
|
||||||
|
) is True
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{bool(value)}",
|
||||||
|
{"value": ""}
|
||||||
|
) is False
|
||||||
|
|
||||||
|
def test_abs_function(self):
|
||||||
|
"""测试 abs() 函数"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{abs(value) > 10}",
|
||||||
|
{"value": -15}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_min_function(self):
|
||||||
|
"""测试 min() 函数"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{min(a, b) == 5}",
|
||||||
|
{"a": 5, "b": 10}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_max_function(self):
|
||||||
|
"""测试 max() 函数"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{max(a, b) == 10}",
|
||||||
|
{"a": 5, "b": 10}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
# ============= 复杂逻辑组合测试 =============
|
||||||
|
|
||||||
|
def test_complex_logic_combination(self):
|
||||||
|
"""测试复杂逻辑组合"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{(score >= 60 and score <= 100) or is_admin}",
|
||||||
|
{"score": 75, "is_admin": False}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
def test_nested_conditions(self):
|
||||||
|
"""测试嵌套条件"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{((count > 0) and (status == 'active')) or (is_admin and (level >= 3))}",
|
||||||
|
{"count": 5, "status": "active", "is_admin": False, "level": 1}
|
||||||
|
) is True
|
||||||
|
|
||||||
|
# ============= 错误处理测试 =============
|
||||||
|
|
||||||
|
def test_undefined_variable_error(self):
|
||||||
|
"""测试变量未定义错误"""
|
||||||
|
with pytest.raises(YAMLError, match="条件表达式中的变量未定义"):
|
||||||
|
self.evaluator.evaluate_condition("{undefined_var > 0}", {"count": 5})
|
||||||
|
|
||||||
|
def test_undefined_function_error(self):
|
||||||
|
"""测试函数未定义错误"""
|
||||||
|
with pytest.raises(YAMLError, match="条件表达式中使用了不支持的函数"):
|
||||||
|
self.evaluator.evaluate_condition("{custom_func(value)}", {"value": 5})
|
||||||
|
|
||||||
|
def test_syntax_error(self):
|
||||||
|
"""测试语法错误"""
|
||||||
|
with pytest.raises(YAMLError, match="条件表达式语法错误"):
|
||||||
|
self.evaluator.evaluate_condition("{count > }", {"count": 5})
|
||||||
|
|
||||||
|
# ============= 安全限制测试 =============
|
||||||
|
|
||||||
|
def test_expression_too_long(self):
|
||||||
|
"""测试表达式过长"""
|
||||||
|
long_expr = "{" + "x " * 300 + "}"
|
||||||
|
with pytest.raises(YAMLError, match="条件表达式过长"):
|
||||||
|
self.evaluator.evaluate_condition(long_expr, {"x": 1})
|
||||||
|
|
||||||
|
def test_attribute_access_forbidden(self):
|
||||||
|
"""测试禁止属性访问"""
|
||||||
|
with pytest.raises(YAMLError, match="条件表达式使用了不支持的语法特性"):
|
||||||
|
self.evaluator.evaluate_condition("{obj.attr}", {"obj": object()})
|
||||||
|
|
||||||
|
# ============= 表达式提取测试 =============
|
||||||
|
|
||||||
|
def test_extract_expression_with_braces(self):
|
||||||
|
"""测试提取带花括号的表达式"""
|
||||||
|
expr = self.evaluator._extract_expression("{count > 0}")
|
||||||
|
assert expr == "count > 0"
|
||||||
|
|
||||||
|
def test_extract_expression_with_whitespace(self):
|
||||||
|
"""测试提取带空白的表达式"""
|
||||||
|
expr = self.evaluator._extract_expression("{ count > 0 }")
|
||||||
|
assert expr == "count > 0"
|
||||||
|
|
||||||
|
def test_extract_expression_without_braces(self):
|
||||||
|
"""测试提取不带花括号的表达式"""
|
||||||
|
expr = self.evaluator._extract_expression("count > 0")
|
||||||
|
assert expr == "count > 0"
|
||||||
|
|
||||||
|
# ============= 向后兼容测试 =============
|
||||||
|
|
||||||
|
def test_backward_compatible_simple_expression(self):
|
||||||
|
"""测试向后兼容的简单表达式"""
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{subtitle != ''}",
|
||||||
|
{"subtitle": "Hello"}
|
||||||
|
) is True
|
||||||
|
assert self.evaluator.evaluate_condition(
|
||||||
|
"{subtitle != ''}",
|
||||||
|
{"subtitle": ""}
|
||||||
|
) is False
|
||||||
@@ -145,15 +145,36 @@ class TestEvaluateCondition:
|
|||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
def test_evaluate_condition_with_missing_variable(self, sample_template):
|
def test_evaluate_condition_with_missing_variable(self, sample_template):
|
||||||
"""测试缺失变量条件为假"""
|
"""测试缺失变量会抛出错误"""
|
||||||
template = Template("title-slide", templates_dir=sample_template)
|
template = Template("title-slide", templates_dir=sample_template)
|
||||||
result = template.evaluate_condition("{subtitle != ''}", {})
|
with pytest.raises(YAMLError, match="条件表达式中的变量未定义"):
|
||||||
assert result is False
|
template.evaluate_condition("{subtitle != ''}", {})
|
||||||
|
|
||||||
def test_evaluate_condition_unrecognized_format_returns_true(self, sample_template):
|
def test_evaluate_condition_complex_logic(self, sample_template):
|
||||||
"""测试无法识别的条件格式默认返回 True"""
|
"""测试复杂逻辑表达式"""
|
||||||
template = Template("title-slide", templates_dir=sample_template)
|
template = Template("title-slide", templates_dir=sample_template)
|
||||||
result = template.evaluate_condition("some other format", {})
|
result = template.evaluate_condition(
|
||||||
|
"{count > 0 and status == 'active'}",
|
||||||
|
{"count": 5, "status": "active"}
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_evaluate_condition_member_test(self, sample_template):
|
||||||
|
"""测试成员测试表达式"""
|
||||||
|
template = Template("title-slide", templates_dir=sample_template)
|
||||||
|
result = template.evaluate_condition(
|
||||||
|
"{status in ['draft', 'review']}",
|
||||||
|
{"status": "draft"}
|
||||||
|
)
|
||||||
|
assert result is True
|
||||||
|
|
||||||
|
def test_evaluate_condition_math_operation(self, sample_template):
|
||||||
|
"""测试数学运算表达式"""
|
||||||
|
template = Template("title-slide", templates_dir=sample_template)
|
||||||
|
result = template.evaluate_condition(
|
||||||
|
"{(price * discount) > 50}",
|
||||||
|
{"price": 100, "discount": 0.8}
|
||||||
|
)
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
28
uv.lock
generated
28
uv.lock
generated
@@ -1418,6 +1418,31 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" },
|
{ url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simpleeval"
|
||||||
|
version = "0.9.13"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version < '3.9'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8f/fa/d2d5bbf9a03fe7b0956368ac5420cfcb072146be6e912a50747dc376133a/simpleeval-0.9.13.tar.gz", hash = "sha256:4a30f9cc01825fe4c719c785e3762623e350c4840d5e6855c2a8496baaa65fac", size = 24535, upload-time = "2023-02-17T09:01:54.853Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/51/bedb4af4f3fe4bb32a3cabfd285be388958c6d676f6b0fa65997812a381b/simpleeval-0.9.13-py2.py3-none-any.whl", hash = "sha256:22a2701a5006e4188d125d34accf2405c2c37c93f6b346f2484b6422415ae54a", size = 15277, upload-time = "2023-02-17T09:01:52.953Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simpleeval"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.10'",
|
||||||
|
"python_full_version == '3.9.*'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ff/6f/15be211749430f52f2c8f0c69158a6fc961c03aac93fa28d44d1a6f5ebc7/simpleeval-1.0.3.tar.gz", hash = "sha256:67bbf246040ac3b57c29cf048657b9cf31d4e7b9d6659684daa08ca8f1e45829", size = 24358, upload-time = "2024-11-02T10:29:46.912Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/e9/e58082fbb8cecbb6fb4133033c40cc50c248b1a331582be3a0f39138d65b/simpleeval-1.0.3-py3-none-any.whl", hash = "sha256:e3bdbb8c82c26297c9a153902d0fd1858a6c3774bf53ff4f134788c3f2035c38", size = 15762, upload-time = "2024-11-02T10:29:45.706Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.4.0"
|
version = "2.4.0"
|
||||||
@@ -1632,6 +1657,8 @@ dependencies = [
|
|||||||
{ name = "flask", version = "3.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
{ name = "flask", version = "3.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||||
{ name = "python-pptx" },
|
{ name = "python-pptx" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
|
{ name = "simpleeval", version = "0.9.13", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||||
|
{ name = "simpleeval", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||||
{ name = "watchdog", version = "4.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
{ name = "watchdog", version = "4.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
|
||||||
{ name = "watchdog", version = "6.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
{ name = "watchdog", version = "6.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
|
||||||
]
|
]
|
||||||
@@ -1659,6 +1686,7 @@ requires-dist = [
|
|||||||
{ name = "pytest-mock", marker = "extra == 'dev'" },
|
{ name = "pytest-mock", marker = "extra == 'dev'" },
|
||||||
{ name = "python-pptx" },
|
{ name = "python-pptx" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
|
{ name = "simpleeval" },
|
||||||
{ name = "watchdog" },
|
{ name = "watchdog" },
|
||||||
]
|
]
|
||||||
provides-extras = ["dev"]
|
provides-extras = ["dev"]
|
||||||
|
|||||||
Reference in New Issue
Block a user