Files
ProStock/src/factors/exceptions.py
liaozhaorun 05d0c90312 feat(factors): 新增公式解析基础组件
新增公式解析相关模块,支持将字符串表达式解析为 DSL 节点树:
- exceptions.py: 定义公式解析异常体系
  - FormulaParseError 基类,提供位置指示的错误信息
  - UnknownFunctionError 支持模糊匹配建议
  - InvalidSyntaxError、EmptyExpressionError 等具体异常
- parser.py: 基于 Python ast 的公式解析器
  - 支持符号引用、数值常量、二元/一元运算
  - 支持函数调用和比较运算
  - 常量折叠优化
- registry.py: 函数注册表
  - 支持动态注册和查询公式函数
  - 提供可用函数列表和重复注册检查
2026-03-03 00:04:48 +08:00

145 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""公式解析异常定义。
提供清晰的错误信息,帮助用户快速定位公式解析问题。
"""
import difflib
from typing import List, Optional
class FormulaParseError(Exception):
"""公式解析错误基类。
Attributes:
expr: 原始表达式字符串
lineno: 错误所在行号从1开始
col_offset: 错误所在列号从0开始
"""
def __init__(
self,
message: str,
expr: Optional[str] = None,
lineno: Optional[int] = None,
col_offset: Optional[int] = None,
):
self.expr = expr
self.lineno = lineno
self.col_offset = col_offset
# 构建详细错误信息
full_message = self._format_message(message)
super().__init__(full_message)
def _format_message(self, message: str) -> str:
"""格式化错误信息,包含位置指示器。"""
lines = [f"FormulaParseError: {message}"]
if self.expr:
lines.append(f" 公式: {self.expr}")
# 添加错误位置指示器
if self.col_offset is not None and self.lineno is not None:
# 计算错误行在表达式中的起始位置
expr_lines = self.expr.split("\n")
if 1 <= self.lineno <= len(expr_lines):
error_line = expr_lines[self.lineno - 1]
lines.append(f" {error_line}")
# 添加指向错误位置的箭头
pointer = " " * (self.col_offset + 7) + "^--- 此处出错"
lines.append(pointer)
return "\n".join(lines)
class UnknownFunctionError(FormulaParseError):
"""未知函数错误。
当表达式中使用了未注册的函数时抛出。
Attributes:
func_name: 未知的函数名
available: 可用函数列表
suggestions: 模糊匹配建议列表
"""
def __init__(
self,
func_name: str,
available: List[str],
expr: Optional[str] = None,
lineno: Optional[int] = None,
col_offset: Optional[int] = None,
):
self.func_name = func_name
self.available = available
# 使用 difflib 获取模糊匹配建议
self.suggestions = difflib.get_close_matches(
func_name, available, n=3, cutoff=0.5
)
# 构建错误信息
if self.suggestions:
suggestion_str = ", ".join(f"'{s}'" for s in self.suggestions)
hint_msg = f"你是不是想找: {suggestion_str}"
else:
# 只显示前10个可用函数
available_preview = ", ".join(available[:10])
if len(available) > 10:
available_preview += f", ... 等共 {len(available)} 个函数"
hint_msg = f"可用函数预览: {available_preview}"
msg = f"未知函数 '{func_name}'{hint_msg}"
super().__init__(
message=msg,
expr=expr,
lineno=lineno,
col_offset=col_offset,
)
class InvalidSyntaxError(FormulaParseError):
"""语法错误。
当表达式语法不正确或不支持时抛出。
"""
pass
class UnsupportedOperatorError(InvalidSyntaxError):
"""不支持的运算符错误。
当使用了不支持的运算符时抛出(如位运算、矩阵运算等)。
"""
pass
class EmptyExpressionError(FormulaParseError):
"""空表达式错误。"""
def __init__(self):
super().__init__("表达式不能为空或只包含空白字符")
class RegistryError(Exception):
"""注册表错误基类。"""
pass
class DuplicateFunctionError(RegistryError):
"""函数重复注册错误。
当尝试注册已存在的函数且未设置 force=True 时抛出。
"""
def __init__(self, func_name: str):
self.func_name = func_name
super().__init__(
f"函数 '{func_name}' 已存在。使用 force=True 覆盖,或选择其他名称。"
)