avada-eval 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- avada_eval-0.1.0/.gitignore +10 -0
- avada_eval-0.1.0/.project-root +1 -0
- avada_eval-0.1.0/LICENSE +22 -0
- avada_eval-0.1.0/Makefile +15 -0
- avada_eval-0.1.0/PKG-INFO +132 -0
- avada_eval-0.1.0/README.md +103 -0
- avada_eval-0.1.0/ci.sh +14 -0
- avada_eval-0.1.0/docs/adr/0001-record-architecture-decisions.md +16 -0
- avada_eval-0.1.0/docs/adr/0002-decimal-semantics.md +29 -0
- avada_eval-0.1.0/docs/adr/0003-resource-exhaustion-limits.md +83 -0
- avada_eval-0.1.0/pyproject.toml +101 -0
- avada_eval-0.1.0/scripts/avada.py +6 -0
- avada_eval-0.1.0/scripts/check_conformance.py +264 -0
- avada_eval-0.1.0/scripts/check_drift.py +53 -0
- avada_eval-0.1.0/src/avada_eval/__init__.py +72 -0
- avada_eval-0.1.0/src/avada_eval/cli.py +102 -0
- avada_eval-0.1.0/src/avada_eval/core/__init__.py +3 -0
- avada_eval-0.1.0/src/avada_eval/core/settings.py +39 -0
- avada_eval-0.1.0/src/avada_eval/decimal_ops.py +152 -0
- avada_eval-0.1.0/src/avada_eval/evaluator.py +1544 -0
- avada_eval-0.1.0/src/avada_eval/exceptions.py +88 -0
- avada_eval-0.1.0/src/avada_eval/llm/__init__.py +21 -0
- avada_eval-0.1.0/src/avada_eval/llm/finance.py +62 -0
- avada_eval-0.1.0/src/avada_eval/llm/preprocess.py +174 -0
- avada_eval-0.1.0/src/avada_eval/llm/tool.py +126 -0
- avada_eval-0.1.0/src/avada_eval/py.typed +0 -0
- avada_eval-0.1.0/tests/conftest.py +21 -0
- avada_eval-0.1.0/tests/test_cli_and_settings.py +65 -0
- avada_eval-0.1.0/tests/test_decimal_semantics.py +285 -0
- avada_eval-0.1.0/tests/test_dos_limits.py +351 -0
- avada_eval-0.1.0/tests/test_llm.py +149 -0
- avada_eval-0.1.0/tests/test_security_decimal.py +88 -0
- avada_eval-0.1.0/tests/upstream/__init__.py +0 -0
- avada_eval-0.1.0/tests/upstream/conftest.py +41 -0
- avada_eval-0.1.0/tests/upstream/test_allowed_attributes.py +95 -0
- avada_eval-0.1.0/tests/upstream/test_basics.py +188 -0
- avada_eval-0.1.0/tests/upstream/test_compound_types.py +140 -0
- avada_eval-0.1.0/tests/upstream/test_comprehensions.py +148 -0
- avada_eval-0.1.0/tests/upstream/test_deliberately_missed_features.py +22 -0
- avada_eval-0.1.0/tests/upstream/test_disallowed_functions.py +67 -0
- avada_eval-0.1.0/tests/upstream/test_evaluator.py +51 -0
- avada_eval-0.1.0/tests/upstream/test_exceptions.py +39 -0
- avada_eval-0.1.0/tests/upstream/test_extending_the_class.py +28 -0
- avada_eval-0.1.0/tests/upstream/test_functions.py +141 -0
- avada_eval-0.1.0/tests/upstream/test_get_item_unhappy_paths.py +45 -0
- avada_eval-0.1.0/tests/upstream/test_method_chaining.py +33 -0
- avada_eval-0.1.0/tests/upstream/test_module_wrapper.py +168 -0
- avada_eval-0.1.0/tests/upstream/test_names.py +246 -0
- avada_eval-0.1.0/tests/upstream/test_no_entries.py +37 -0
- avada_eval-0.1.0/tests/upstream/test_operators.py +29 -0
- avada_eval-0.1.0/tests/upstream/test_reference_cleanup.py +50 -0
- avada_eval-0.1.0/tests/upstream/test_security.py +660 -0
- avada_eval-0.1.0/tests/upstream/test_short_circuiting.py +43 -0
- avada_eval-0.1.0/tests/upstream/test_simple_eval_function.py +17 -0
- avada_eval-0.1.0/tests/upstream/test_unusual_comparisons.py +64 -0
- avada_eval-0.1.0/tests/upstream/test_whitespace.py +34 -0
- avada_eval-0.1.0/uv.lock +493 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# 本文件标记仓库根(tests/conftest.py 的「从仓库根运行」守卫与 scripts/check_conformance.py 依赖它),请勿删除。
|
avada_eval-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013-2026 Daniel Fairhead
|
|
4
|
+
Copyright (c) 2026 lin han
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
22
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
check: # 完整门禁(唯一正确性判据);也可直接 ./ci.sh
|
|
2
|
+
./ci.sh
|
|
3
|
+
|
|
4
|
+
dev-test: # 本地快速迭代:只跑测试,不跑 lint/漂移(完整判据仍是 ./ci.sh)
|
|
5
|
+
uv run --locked pytest
|
|
6
|
+
|
|
7
|
+
fmt: # 唯一写入式清理入口:safe lint fixes(含 I/UP)后再 formatter
|
|
8
|
+
uv run --locked ruff check --fix --no-unsafe-fixes --exit-zero .
|
|
9
|
+
uv run --locked ruff format .
|
|
10
|
+
|
|
11
|
+
hooks: # 一次性:把同一条门钉在 push 前(不引入 pre-commit 框架)
|
|
12
|
+
@test -d .git/hooks || { echo "不是 git 仓库(没有 .git/hooks/),先 git init。"; exit 1; }
|
|
13
|
+
@printf '#!/bin/sh\nexec ./ci.sh\n' > .git/hooks/pre-push
|
|
14
|
+
@chmod +x .git/hooks/pre-push
|
|
15
|
+
@echo "✓ 已装 .git/hooks/pre-push → ./ci.sh"
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: avada-eval
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Safe, Decimal-exact expression evaluator for LLM tools, derived from simpleeval.
|
|
5
|
+
Project-URL: Homepage, https://github.com/VoldemortGin/avada-eval
|
|
6
|
+
Project-URL: Repository, https://github.com/VoldemortGin/avada-eval
|
|
7
|
+
Project-URL: Issues, https://github.com/VoldemortGin/avada-eval/issues
|
|
8
|
+
Author-email: lin han <mn2895566@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: calculator,decimal,eval,evaluator,expression,llm,sandbox,simpleeval,tool
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
23
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: beartype>=0.20
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# avada-eval
|
|
31
|
+
|
|
32
|
+
**Safe, Decimal-exact expression evaluator for LLM tools, derived from simpleeval.**
|
|
33
|
+
|
|
34
|
+
安全、可扩展、给大模型当计算工具用的表达式求值器。派生自
|
|
35
|
+
[simpleeval](https://github.com/danthedeckie/simpleeval)(v1.0.8,commit
|
|
36
|
+
`3b30888d0d5954df8a11b7e5ef26d17e19a96c29`,MIT,© 2013-2026 Daniel Fairhead),
|
|
37
|
+
核心区别:**用 `decimal.Decimal` 代替 float 做精确计算**,并加固了资源耗尽(DoS)防护。
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install avada-eval # 或 uv add avada-eval
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
需要 Python ≥ 3.11;唯一运行时依赖 `beartype`(运行时类型检查,`AVADA_BEARTYPE_ON=false` 可关闭)。
|
|
44
|
+
|
|
45
|
+
## 快速示例
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from avada_eval import simple_eval, SimpleEval # 替换 `from simpleeval import ...`
|
|
49
|
+
|
|
50
|
+
simple_eval("0.1 + 0.2") # Decimal('0.3')
|
|
51
|
+
simple_eval("1 / 3") # Decimal('0.3333333333333333333333333333')
|
|
52
|
+
simple_eval("2 ** 100") # 1267650600228229401496703205376(int 精确)
|
|
53
|
+
simple_eval("price * qty", names={"price": 19.99, "qty": 3}) # Decimal('59.97')
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Decimal 语义(与 simpleeval 的差异)
|
|
57
|
+
|
|
58
|
+
| 项 | simpleeval | avada-eval |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| 小数字面量 | float | 按**源码原文**构造的 `Decimal`(`1.10` 保留尾零) |
|
|
61
|
+
| 整数字面量 | int | int(`**` `//` `%` 位运算与 Python 一致) |
|
|
62
|
+
| `/` | float | `Decimal`(int/int 亦然) |
|
|
63
|
+
| 负/小数指数 `**` | float | `Decimal`;Decimal 路径只限制指数,结果超出 `Emax` → `NumberTooHigh` |
|
|
64
|
+
| Decimal 的 `//` `%` | — | 取整向下(与 int/float 一致,而非 decimal 模块的截断) |
|
|
65
|
+
| 除零 | `ZeroDivisionError` | `DivisionByZero`(同时是 `InvalidExpression` 与 `ZeroDivisionError`) |
|
|
66
|
+
| 外部 float | 原样 | `float_policy="convert"`(默认,`Decimal(str(x))`)或 `"error"`(`FloatNotAllowed`) |
|
|
67
|
+
| 默认函数 | rand randint int float str | 另加 `decimal round abs min max sum`;`float()`/`rand()` 返回 Decimal |
|
|
68
|
+
| 精度/舍入 | — | `context=decimal.Context(...)`,每次 `eval` 在 `localcontext` 中执行 |
|
|
69
|
+
|
|
70
|
+
默认 context:28 位有效数字、ROUND_HALF_EVEN、`Emax/Emin = ±999999`,InvalidOperation /
|
|
71
|
+
DivisionByZero / Overflow 以异常抛出。设计决策见
|
|
72
|
+
[ADR 0002](https://github.com/VoldemortGin/avada-eval/blob/main/docs/adr/0002-decimal-semantics.md)。
|
|
73
|
+
|
|
74
|
+
## LLM 工具用法(`avada_eval.llm`)
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from avada_eval.llm import evaluate_for_llm, TOOL_DESCRIPTION
|
|
78
|
+
|
|
79
|
+
evaluate_for_llm("revenue * 12.5% + 1,000", {"revenue": 80000}).to_dict()
|
|
80
|
+
# {'ok': True, 'normalized': 'revenue * 0.125 + 1000', 'value': '11000.000', 'variables': {'revenue': '80000'}, ...}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- `evaluate_for_llm` 从不因表达式错误而抛异常:语法错误、未知名字、除零、安全拒绝等都以
|
|
84
|
+
`ok=False` + `error` / `error_type` 返回,模型可据此重试;`TOOL_DESCRIPTION` 可直接作为工具描述。
|
|
85
|
+
- 财务函数:`pct_change(new, old)`、`cagr(end, start, years)`(均返回百分点)、`ratio(a, b)`、`avg(...)`。
|
|
86
|
+
- 千分位:逗号两侧**无空格**且分组合法(首组 1-3 位、其后每组 3 位)才视为千分位;逗号后带空格永远是参数分隔符(`max(1, 234)`)。
|
|
87
|
+
- 百分号:紧贴数字、且其后是结尾/空白/非操作数(`)` `,` `*` …)的 `%` 是百分号;`10%3`、`10 % 3` 仍是取模。
|
|
88
|
+
|
|
89
|
+
## 安全
|
|
90
|
+
|
|
91
|
+
安全模型继承 simpleeval 并**只加强不削弱**:禁止 `_`/`func_` 前缀属性、`format`/`format_map`
|
|
92
|
+
等危险方法、`DISALLOW_FUNCTIONS`、模块访问(需显式 `ModuleWrapper`)、lambda / import / 赋值。
|
|
93
|
+
|
|
94
|
+
资源耗尽防护在**分配或计算之前**按估算值拦截(修复了 simpleeval 中
|
|
95
|
+
`'%999999999d' % 1`、`f"{1:999999999}"`、`"a".center(999999999)`、`4000000 ** 4000000`、
|
|
96
|
+
`'%d' % 1e999999` 等先分配/计算再检查的问题)。上限为 `avada_eval.evaluator` 的模块级常量,调用时读取:
|
|
97
|
+
|
|
98
|
+
| 常量 | 默认 | 作用 |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `MAX_STRING_LENGTH` | 100000 | 构造出的字符串/序列长度(容器按嵌套元素总数计) |
|
|
101
|
+
| `MAX_FORMAT_WIDTH` | 10000 | 格式化 width/precision 与 `center/ljust/rjust/zfill/expandtabs` 宽度 |
|
|
102
|
+
| `MAX_INT_BITS` | 15000 | 任何 int 的位数 |
|
|
103
|
+
| `MAX_POWER` / `MAX_SHIFT` | 4000000 / 10000 | `**` 底数与指数、移位量 |
|
|
104
|
+
| `MAX_POWER_BASE_DIGITS` | 1000 | Decimal 小数次幂的底数位数 |
|
|
105
|
+
| `MAX_COMPREHENSION_LENGTH` | 10000 | 推导式迭代次数(`EvalWithCompoundTypes`) |
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import avada_eval.evaluator as evaluator
|
|
109
|
+
|
|
110
|
+
evaluator.MAX_FORMAT_WIDTH = 1000 # 与 simpleeval 一样,直接改模块属性
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
完整排查清单与**拦不住的部分**(表达式长度、推导式总内存、自定义 context 等)见
|
|
114
|
+
[ADR 0003](https://github.com/VoldemortGin/avada-eval/blob/main/docs/adr/0003-resource-exhaustion-limits.md)。
|
|
115
|
+
对不可信输入,仍建议限制表达式长度,并在带超时与内存上限的子进程中求值。
|
|
116
|
+
|
|
117
|
+
## 命令行
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
avada "0.1 + 0.2"
|
|
121
|
+
avada "round(x * 3, 1)" -v x=0.05 --rounding ROUND_HALF_UP
|
|
122
|
+
avada "a + 1,000" -v a=1.5 --json
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## 开发
|
|
126
|
+
|
|
127
|
+
`uv sync` 后,`make fmt` 做格式化,`./ci.sh`(从仓库根运行)是唯一的零警告质量门:
|
|
128
|
+
ruff format/check → mypy --strict → 结构检查 → 文档漂移 → pytest。
|
|
129
|
+
|
|
130
|
+
## 许可
|
|
131
|
+
|
|
132
|
+
MIT,见 [LICENSE](https://github.com/VoldemortGin/avada-eval/blob/main/LICENSE)(保留 simpleeval 原版权声明)。
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# avada-eval
|
|
2
|
+
|
|
3
|
+
**Safe, Decimal-exact expression evaluator for LLM tools, derived from simpleeval.**
|
|
4
|
+
|
|
5
|
+
安全、可扩展、给大模型当计算工具用的表达式求值器。派生自
|
|
6
|
+
[simpleeval](https://github.com/danthedeckie/simpleeval)(v1.0.8,commit
|
|
7
|
+
`3b30888d0d5954df8a11b7e5ef26d17e19a96c29`,MIT,© 2013-2026 Daniel Fairhead),
|
|
8
|
+
核心区别:**用 `decimal.Decimal` 代替 float 做精确计算**,并加固了资源耗尽(DoS)防护。
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install avada-eval # 或 uv add avada-eval
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
需要 Python ≥ 3.11;唯一运行时依赖 `beartype`(运行时类型检查,`AVADA_BEARTYPE_ON=false` 可关闭)。
|
|
15
|
+
|
|
16
|
+
## 快速示例
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from avada_eval import simple_eval, SimpleEval # 替换 `from simpleeval import ...`
|
|
20
|
+
|
|
21
|
+
simple_eval("0.1 + 0.2") # Decimal('0.3')
|
|
22
|
+
simple_eval("1 / 3") # Decimal('0.3333333333333333333333333333')
|
|
23
|
+
simple_eval("2 ** 100") # 1267650600228229401496703205376(int 精确)
|
|
24
|
+
simple_eval("price * qty", names={"price": 19.99, "qty": 3}) # Decimal('59.97')
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Decimal 语义(与 simpleeval 的差异)
|
|
28
|
+
|
|
29
|
+
| 项 | simpleeval | avada-eval |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| 小数字面量 | float | 按**源码原文**构造的 `Decimal`(`1.10` 保留尾零) |
|
|
32
|
+
| 整数字面量 | int | int(`**` `//` `%` 位运算与 Python 一致) |
|
|
33
|
+
| `/` | float | `Decimal`(int/int 亦然) |
|
|
34
|
+
| 负/小数指数 `**` | float | `Decimal`;Decimal 路径只限制指数,结果超出 `Emax` → `NumberTooHigh` |
|
|
35
|
+
| Decimal 的 `//` `%` | — | 取整向下(与 int/float 一致,而非 decimal 模块的截断) |
|
|
36
|
+
| 除零 | `ZeroDivisionError` | `DivisionByZero`(同时是 `InvalidExpression` 与 `ZeroDivisionError`) |
|
|
37
|
+
| 外部 float | 原样 | `float_policy="convert"`(默认,`Decimal(str(x))`)或 `"error"`(`FloatNotAllowed`) |
|
|
38
|
+
| 默认函数 | rand randint int float str | 另加 `decimal round abs min max sum`;`float()`/`rand()` 返回 Decimal |
|
|
39
|
+
| 精度/舍入 | — | `context=decimal.Context(...)`,每次 `eval` 在 `localcontext` 中执行 |
|
|
40
|
+
|
|
41
|
+
默认 context:28 位有效数字、ROUND_HALF_EVEN、`Emax/Emin = ±999999`,InvalidOperation /
|
|
42
|
+
DivisionByZero / Overflow 以异常抛出。设计决策见
|
|
43
|
+
[ADR 0002](https://github.com/VoldemortGin/avada-eval/blob/main/docs/adr/0002-decimal-semantics.md)。
|
|
44
|
+
|
|
45
|
+
## LLM 工具用法(`avada_eval.llm`)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from avada_eval.llm import evaluate_for_llm, TOOL_DESCRIPTION
|
|
49
|
+
|
|
50
|
+
evaluate_for_llm("revenue * 12.5% + 1,000", {"revenue": 80000}).to_dict()
|
|
51
|
+
# {'ok': True, 'normalized': 'revenue * 0.125 + 1000', 'value': '11000.000', 'variables': {'revenue': '80000'}, ...}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- `evaluate_for_llm` 从不因表达式错误而抛异常:语法错误、未知名字、除零、安全拒绝等都以
|
|
55
|
+
`ok=False` + `error` / `error_type` 返回,模型可据此重试;`TOOL_DESCRIPTION` 可直接作为工具描述。
|
|
56
|
+
- 财务函数:`pct_change(new, old)`、`cagr(end, start, years)`(均返回百分点)、`ratio(a, b)`、`avg(...)`。
|
|
57
|
+
- 千分位:逗号两侧**无空格**且分组合法(首组 1-3 位、其后每组 3 位)才视为千分位;逗号后带空格永远是参数分隔符(`max(1, 234)`)。
|
|
58
|
+
- 百分号:紧贴数字、且其后是结尾/空白/非操作数(`)` `,` `*` …)的 `%` 是百分号;`10%3`、`10 % 3` 仍是取模。
|
|
59
|
+
|
|
60
|
+
## 安全
|
|
61
|
+
|
|
62
|
+
安全模型继承 simpleeval 并**只加强不削弱**:禁止 `_`/`func_` 前缀属性、`format`/`format_map`
|
|
63
|
+
等危险方法、`DISALLOW_FUNCTIONS`、模块访问(需显式 `ModuleWrapper`)、lambda / import / 赋值。
|
|
64
|
+
|
|
65
|
+
资源耗尽防护在**分配或计算之前**按估算值拦截(修复了 simpleeval 中
|
|
66
|
+
`'%999999999d' % 1`、`f"{1:999999999}"`、`"a".center(999999999)`、`4000000 ** 4000000`、
|
|
67
|
+
`'%d' % 1e999999` 等先分配/计算再检查的问题)。上限为 `avada_eval.evaluator` 的模块级常量,调用时读取:
|
|
68
|
+
|
|
69
|
+
| 常量 | 默认 | 作用 |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `MAX_STRING_LENGTH` | 100000 | 构造出的字符串/序列长度(容器按嵌套元素总数计) |
|
|
72
|
+
| `MAX_FORMAT_WIDTH` | 10000 | 格式化 width/precision 与 `center/ljust/rjust/zfill/expandtabs` 宽度 |
|
|
73
|
+
| `MAX_INT_BITS` | 15000 | 任何 int 的位数 |
|
|
74
|
+
| `MAX_POWER` / `MAX_SHIFT` | 4000000 / 10000 | `**` 底数与指数、移位量 |
|
|
75
|
+
| `MAX_POWER_BASE_DIGITS` | 1000 | Decimal 小数次幂的底数位数 |
|
|
76
|
+
| `MAX_COMPREHENSION_LENGTH` | 10000 | 推导式迭代次数(`EvalWithCompoundTypes`) |
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import avada_eval.evaluator as evaluator
|
|
80
|
+
|
|
81
|
+
evaluator.MAX_FORMAT_WIDTH = 1000 # 与 simpleeval 一样,直接改模块属性
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
完整排查清单与**拦不住的部分**(表达式长度、推导式总内存、自定义 context 等)见
|
|
85
|
+
[ADR 0003](https://github.com/VoldemortGin/avada-eval/blob/main/docs/adr/0003-resource-exhaustion-limits.md)。
|
|
86
|
+
对不可信输入,仍建议限制表达式长度,并在带超时与内存上限的子进程中求值。
|
|
87
|
+
|
|
88
|
+
## 命令行
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
avada "0.1 + 0.2"
|
|
92
|
+
avada "round(x * 3, 1)" -v x=0.05 --rounding ROUND_HALF_UP
|
|
93
|
+
avada "a + 1,000" -v a=1.5 --json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## 开发
|
|
97
|
+
|
|
98
|
+
`uv sync` 后,`make fmt` 做格式化,`./ci.sh`(从仓库根运行)是唯一的零警告质量门:
|
|
99
|
+
ruff format/check → mypy --strict → 结构检查 → 文档漂移 → pytest。
|
|
100
|
+
|
|
101
|
+
## 许可
|
|
102
|
+
|
|
103
|
+
MIT,见 [LICENSE](https://github.com/VoldemortGin/avada-eval/blob/main/LICENSE)(保留 simpleeval 原版权声明)。
|
avada_eval-0.1.0/ci.sh
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 唯一的零警告门:人和 agent 共用的「是否完成」判据。任一项非零即整体失败。
|
|
3
|
+
# 必须从仓库根运行(深子目录下 pytest 会静默收集 0 个用例)。
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
# CI/完成门只读;写入式清理由 `make fmt` 在门前完成。
|
|
7
|
+
# 一律经项目 uv.lock:`--locked` 禁止命令静默改锁或漂到全局工具版本。
|
|
8
|
+
uv run --locked ruff format --check .
|
|
9
|
+
uv run --locked ruff check --no-fix .
|
|
10
|
+
uv run --locked mypy .
|
|
11
|
+
uv run --locked python scripts/check_conformance.py . # 结构不变量(hook / 零 SDK 外泄 / 绝对导入…)
|
|
12
|
+
uv run --locked python scripts/check_drift.py # 文档/工件漂移守卫
|
|
13
|
+
uv run --locked pytest # 运行时类型(beartype)+ 行为 + 冒烟 + warnings-as-errors
|
|
14
|
+
echo "✓ 全绿"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
covers: docs/adr
|
|
3
|
+
---
|
|
4
|
+
# 1. 用 ADR 记录架构决策
|
|
5
|
+
|
|
6
|
+
- 状态:已采纳
|
|
7
|
+
- 日期:2026-09-24
|
|
8
|
+
|
|
9
|
+
## 背景
|
|
10
|
+
架构/产品决策需要在 AI 动手前钉死方向,并留下"为什么否决备选"以防重走弯路。AI 最擅长明确约束下填实现,最不可靠的是替你做含糊的方向取舍。
|
|
11
|
+
|
|
12
|
+
## 决策
|
|
13
|
+
所有重大决策写成编号、不可变的 ADR(背景 + 选定方案 + 被否决备选及理由)。AI 只在锁定边界内填空。
|
|
14
|
+
|
|
15
|
+
## 后果
|
|
16
|
+
方向可审计;AI 不会替我们做含糊取舍,也不会重走已排除的路。
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
covers: [src/avada_eval/evaluator.py, src/avada_eval/decimal_ops.py, src/avada_eval/llm/preprocess.py]
|
|
3
|
+
---
|
|
4
|
+
# 2. Decimal 语义
|
|
5
|
+
|
|
6
|
+
- 状态:已采纳
|
|
7
|
+
- 日期:2026-09-24
|
|
8
|
+
|
|
9
|
+
## 背景
|
|
10
|
+
simpleeval 用 float 计算,`0.1 + 0.2 != 0.3`;给 LLM 当计算器时这类误差会直接进入答案。
|
|
11
|
+
|
|
12
|
+
## 决策
|
|
13
|
+
1. **小数字面量**:`SimpleEval.parse` 用 `ast.get_source_segment` 取源码原文构造 `Decimal`,
|
|
14
|
+
并以 `float(原文) == ast 值` 校验对位;替换为 `DecimalLiteral` 节点。永不 `Decimal(float)`。
|
|
15
|
+
2. **整数保持 int**:int 精确且任意精度,`//` `%` 位运算、索引、`str * n` 语义与 Python 一致;
|
|
16
|
+
`/` 与负/小数指数 `**` 转入 Decimal。Decimal 的 `//` `%` 改为向下取整,
|
|
17
|
+
使一个数在 int 与 Decimal 之间切换时结果不变。
|
|
18
|
+
3. **Context**:默认 prec=28、ROUND_HALF_EVEN、Emax/Emin=±999999、trap
|
|
19
|
+
InvalidOperation/DivisionByZero/Overflow;实例持有副本,`eval` 在 `localcontext` 中执行。
|
|
20
|
+
4. **外部 float**:每个节点求值后检查;默认 `Decimal(str(x))`,可选严格报错(含容器内)。
|
|
21
|
+
5. **`**`**:int**非负 int 保持 simpleeval 限制(底数与指数 ≤ MAX_POWER);其余走 Decimal,
|
|
22
|
+
成本受精度约束,只限制指数;超 Emax → NumberTooHigh;`0 ** 负数` → DivisionByZero;
|
|
23
|
+
负底数小数指数 → InvalidExpression。
|
|
24
|
+
6. **新增安全边界**:字面量指数超出 context 范围 → NumberTooHigh;`int()` 拒绝 ≥ 4300 位的 Decimal。
|
|
25
|
+
|
|
26
|
+
## 被否决的备选
|
|
27
|
+
- **所有数字统一 Decimal**:`2**100` 会被舍到 28 位、`-7 // 2` 变成截断语义、位运算与索引全部失效。
|
|
28
|
+
- **tokenize 取字面量原文**:3.12 前后 f-string 的分词方式不同;`get_source_segment` 在 3.10–3.14 均准确。
|
|
29
|
+
- **`Decimal(repr(float))` 兜底**:超过 17 位有效数字会静默丢精度,违背"不静默失败"。
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
covers: [src/avada_eval/evaluator.py, tests/test_dos_limits.py]
|
|
3
|
+
---
|
|
4
|
+
# 3. 资源耗尽(DoS)防护:先估算、后分配
|
|
5
|
+
|
|
6
|
+
- 状态:已采纳
|
|
7
|
+
- 日期:2026-09-24
|
|
8
|
+
|
|
9
|
+
## 背景
|
|
10
|
+
simpleeval 的长度检查大多发生在**结果生成之后**:`'%999999999d' % 1`、`f"{1:999999999}"`、
|
|
11
|
+
`"a".center(999999999)` 会先分配 1 GB 字符串再报错(或直接耗尽内存)。系统排查后还发现若干
|
|
12
|
+
继承来的 CPU/内存放大途径(见下表)。
|
|
13
|
+
|
|
14
|
+
## 决策
|
|
15
|
+
所有拦截都在**分配/计算之前**按估算值判定;上限均为 `avada_eval.evaluator` 的模块级常量,
|
|
16
|
+
与 `MAX_POWER` 一样在调用时读取,可直接改模块属性调整。
|
|
17
|
+
|
|
18
|
+
| 常量 | 默认 | 含义 |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `MAX_STRING_LENGTH`(上游) | 100000 | 任何构造出的字符串/序列长度;容器按**嵌套元素总数**计 |
|
|
21
|
+
| `MAX_FORMAT_WIDTH`(新) | 10000 | 格式化 width/precision、`center/ljust/rjust/zfill/expandtabs` 的宽度 |
|
|
22
|
+
| `MAX_INT_BITS`(新) | 15000 | 任何 int 的位数(≈4500 位十进制,略高于 Python 的 4300 位 int↔str 限制) |
|
|
23
|
+
| `MAX_POWER_BASE_DIGITS`(新) | 1000 | Decimal 小数次幂底数的有效位数 |
|
|
24
|
+
| `MAX_INT_DIGITS` | 4300 | `int()` / `%d` / `as_integer_ratio()` 可转换的 Decimal 量级 |
|
|
25
|
+
|
|
26
|
+
`MAX_FORMAT_WIDTH` 与 `MAX_STRING_LENGTH` 的关系:前者是**单个字段的填充上限**,在分配前检查;
|
|
27
|
+
后者是**整串上限**,拼接后仍然生效(f-string 累加检查、`%` 总长估算)。应保持
|
|
28
|
+
`MAX_FORMAT_WIDTH <= MAX_STRING_LENGTH`。异常选用既有的 `IterableTooLong`(长度类)与
|
|
29
|
+
`NumberTooHigh`(数值量级类),不新增异常类型,调用方现有的 `except` 无需改动。
|
|
30
|
+
|
|
31
|
+
## 排查清单与结论
|
|
32
|
+
|
|
33
|
+
| 途径 | 默认下的代价(修复前) | 结论 |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `%` 格式化 width/precision(含 `*`、`%(key)`、bytes) | 1 GB 分配 | **已拦**:按 CPython 语法逐个解析,超 `MAX_FORMAT_WIDTH` 抛 `IterableTooLong`;总长估算超 `MAX_STRING_LENGTH` 同样拒绝 |
|
|
36
|
+
| f-string format spec(含嵌套 `{x:{w}}`、数字填充符) | 1 GB 分配 | **已拦**:解析最终 spec;无法识别的自定义 spec 保守地检查所有数字串 |
|
|
37
|
+
| f-string `f"{1e999999:f}"`(Decimal 定点展开) | 1 MB(自定义大 Emax 时无界) | **已拦**:按 `abs(adjusted)+位数+precision` 估算 |
|
|
38
|
+
| f-string spec 拼进 `"{:" + spec + "}".format()` | 格式串注入风险 | **已改**:改用 `format(value, spec)` |
|
|
39
|
+
| `str.center/ljust/rjust/zfill`(含未绑定 `str.center(s, n)`、bytes) | 1 GB 分配 | **已拦**:宽度超 `MAX_FORMAT_WIDTH` |
|
|
40
|
+
| `str.expandtabs` | `('\t'*1e5).expandtabs(1e4)` = 1e9 字符 | **已拦**:tabsize 上限 + `len + 制表符数×tabsize` 估算 |
|
|
41
|
+
| `sep.join(parts)` | `('a'*1e5).join('b'*1e5)` = 1e10 字符 | **已拦**:`len(sep)×(n-1) + Σlen` 提前估算 |
|
|
42
|
+
| `str.replace` | `s.replace('', 'b'*1e3)` 放大 | **已拦**:命中次数 × 长度差估算 |
|
|
43
|
+
| `str.translate({c: 长串})` | 放大 len(table 值) 倍 | **已拦**(Mapping 表) |
|
|
44
|
+
| `str.format` / `format_map` | 格式串注入 | 上游已禁(`DISALLOW_METHODS`),复核通过 |
|
|
45
|
+
| 内置 `format()` / `range` | — | 默认不可达;调用方自行加入函数时需自行把关 |
|
|
46
|
+
| `"x" * n`、`list * n` | — | 上游已检查,复核通过;**加强**:嵌套容器按总元素计(`[[1]*1e5]*1e5` 被拒) |
|
|
47
|
+
| `seq * seq`(如 `str * list`) | 上游检查里先算 `list * len(str)`,可达 1e10 元素 | **已修**:仅在另一侧为 int 时计算估算值 |
|
|
48
|
+
| `str(容器)` / `f"{容器}"` / `'%s' % 容器` | `str(s.split('b')*1e5)` = 1e10 字符 | **已拦**:按文本长度估算(迭代、带提前退出,自引用不会无限递归) |
|
|
49
|
+
| `list.extend` 自身倍增(推导式内循环) | 2^40 元素 | **已拦**:`len(self)+len(arg)` |
|
|
50
|
+
| `int.to_bytes(n)` | 1 GB 分配 | **已拦**:`n > MAX_STRING_LENGTH` |
|
|
51
|
+
| int `**`(上游仅限底数与指数 ≤ 4e6) | `4000000**4000000` ≈ 20 s、8800 万位 | **已拦**:计算前按 `b·log2(a)` 估算,超 `MAX_INT_BITS` 抛 `NumberTooHigh` |
|
|
52
|
+
| int `*` 连乘 | 大数相乘 40 s+ | **已拦**:`bit_length` 之和预检 |
|
|
53
|
+
| 其它途径产生的大 int(`from_bytes`、`int(s, 16)`、names 传入) | 线性代价 | **已拦**:每个节点结果(含容器内)检查 `MAX_INT_BITS` |
|
|
54
|
+
| int ↔ Decimal 转换(`1e999999 == 10**999999`、`Decimal(2**4e6)`) | 30 s+,近乎卡死 | **已拦**:int 受 `MAX_INT_BITS` 约束后转换 < 1 ms |
|
|
55
|
+
| `'%d' % 1e999999`(隐式 `int(Decimal)`) | 卡死 | **已拦**:`adjusted() >= MAX_INT_DIGITS` 抛 `NumberTooHigh` |
|
|
56
|
+
| `Decimal.as_integer_ratio()` | 百万位 int | **已拦**:同上量级判断 |
|
|
57
|
+
| Decimal 小数次幂、底数位数很多(`decimal('0.'+'3'*1e4) ** 0.5`) | 5 s(1e5 位时卡死) | **已拦**:`MAX_POWER_BASE_DIGITS` |
|
|
58
|
+
| Decimal `sqrt/exp/ln/log10`、整数次幂、`quantize`、`scaleb`、`10.0 ** 999999` | ≤ 20 ms | **无需拦**:受 context 精度与 Emax 约束(Overflow/InvalidOperation 已 trap) |
|
|
59
|
+
| 超长小数字面量 / `decimal("9"*99999)` | 构造与四则运算毫秒级 | **无需拦**:字面量受 `MAX_STRING_LENGTH` 约束 |
|
|
60
|
+
| int→str 4300 位限制 | — | Python 进程级设置;即使宿主调用 `sys.set_int_max_str_digits(0)`,`MAX_INT_BITS` 仍兜底 |
|
|
61
|
+
| 模块 `_check_disallowed_items` 遍历共享引用(DAG) | 指数级遍历 | **已修**:改为迭代 + 按 id 去重 |
|
|
62
|
+
|
|
63
|
+
### 拦不住 / 未拦(写明,由调用方控制)
|
|
64
|
+
1. **表达式长度**:每个节点的结果有界,但节点数与表达式长度成正比。函数实参等会同时存活,
|
|
65
|
+
内存上界约为 `len(expr) × MAX_STRING_LENGTH / 10`。调用方应限制表达式长度(LLM 工具场景
|
|
66
|
+
建议 ≤ 数千字符)。
|
|
67
|
+
2. **推导式(仅 `EvalWithCompoundTypes`)**:`["a"*100000 for c in "a"*10000]` 可生成
|
|
68
|
+
`MAX_COMPREHENSION_LENGTH × MAX_STRING_LENGTH` ≈ 1 GB 的**不同**字符串;结果容器只按元素数
|
|
69
|
+
检查。需要时调低这两个常量。
|
|
70
|
+
3. **大字符串的逐元素比较**:`[s1]*1e5 == [s2]*1e5`(s1、s2 为相等但不同的 10 万字符串)约
|
|
71
|
+
1e10 字节 memcmp,秒级。上界为 `MAX_STRING_LENGTH²`,调低 `MAX_STRING_LENGTH` 可平方级缩小。
|
|
72
|
+
4. **自定义 decimal context**:放大 `prec`/`Emax` 会同比放大 Decimal 运算成本,后果由调用方承担。
|
|
73
|
+
5. **调用方传入的数据与函数**:names 中的容器只在参与"构造"(运算、调用、字面量)时检查
|
|
74
|
+
(int 例外:读取即检查 `MAX_INT_BITS`);
|
|
75
|
+
调用方自行添加的函数(`format`、`range`、自定义函数)不在防护范围内。
|
|
76
|
+
|
|
77
|
+
对不可信输入的最终兜底仍是**进程隔离 + 超时 + 内存上限**(如子进程 + `resource.RLIMIT_AS`)。
|
|
78
|
+
|
|
79
|
+
## 被否决的备选
|
|
80
|
+
- **事后检查结果长度**(上游做法):分配已经发生,拦不住内存峰值。
|
|
81
|
+
- **用 `sys.setrecursionlimit`/信号超时兜底**:不跨平台(Windows 无 `SIGALRM`),且 C 层分配无法被中断。
|
|
82
|
+
- **新增 `ResourceLimitExceeded` 异常**:会让现有 `except IterableTooLong/NumberTooHigh` 漏捕,违背"只加强不削弱"。
|
|
83
|
+
- **所有 int→Decimal 转换处分别检查**:混合运算走 Python 原生 `Decimal.__add__` 等,点位太多;统一约束 int 大小更简单可靠。
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "avada-eval"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Safe, Decimal-exact expression evaluator for LLM tools, derived from simpleeval."
|
|
5
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "lin han", email = "mn2895566@gmail.com" }]
|
|
9
|
+
# 3.11 下限:覆盖现有 simpleeval 使用方(如 SuperIndex: >=3.11,<3.13),可直接替换 import。
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = ["beartype>=0.20"]
|
|
12
|
+
keywords = [
|
|
13
|
+
"eval",
|
|
14
|
+
"expression",
|
|
15
|
+
"evaluator",
|
|
16
|
+
"calculator",
|
|
17
|
+
"decimal",
|
|
18
|
+
"sandbox",
|
|
19
|
+
"simpleeval",
|
|
20
|
+
"llm",
|
|
21
|
+
"tool",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 3 - Alpha",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Programming Language :: Python :: 3.13",
|
|
32
|
+
"Programming Language :: Python :: 3.14",
|
|
33
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
34
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
35
|
+
"Topic :: Software Development :: Interpreters",
|
|
36
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/VoldemortGin/avada-eval"
|
|
42
|
+
Repository = "https://github.com/VoldemortGin/avada-eval"
|
|
43
|
+
Issues = "https://github.com/VoldemortGin/avada-eval/issues"
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
avada = "avada_eval.cli:main"
|
|
47
|
+
|
|
48
|
+
[dependency-groups]
|
|
49
|
+
dev = ["mypy", "ruff", "pytest", "hypothesis"]
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["hatchling>=1.27"] # PEP 639 license / license-files
|
|
53
|
+
build-backend = "hatchling.build"
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
packages = ["src/avada_eval"]
|
|
57
|
+
|
|
58
|
+
[tool.hatch.build.targets.sdist]
|
|
59
|
+
include = [
|
|
60
|
+
"src/avada_eval",
|
|
61
|
+
"tests",
|
|
62
|
+
"docs",
|
|
63
|
+
"scripts",
|
|
64
|
+
"README.md",
|
|
65
|
+
"LICENSE",
|
|
66
|
+
"pyproject.toml",
|
|
67
|
+
"uv.lock",
|
|
68
|
+
"ci.sh",
|
|
69
|
+
"Makefile",
|
|
70
|
+
".project-root",
|
|
71
|
+
]
|
|
72
|
+
exclude = ["__pycache__", "*.pyc"]
|
|
73
|
+
|
|
74
|
+
[tool.mypy]
|
|
75
|
+
python_version = "3.11"
|
|
76
|
+
strict = true
|
|
77
|
+
warn_unreachable = true
|
|
78
|
+
enable_error_code = ["ignore-without-code"]
|
|
79
|
+
|
|
80
|
+
[tool.ruff]
|
|
81
|
+
target-version = "py311"
|
|
82
|
+
src = ["src", "tests"]
|
|
83
|
+
unsafe-fixes = false
|
|
84
|
+
|
|
85
|
+
[tool.ruff.lint]
|
|
86
|
+
select = ["E4", "E7", "E9", "F", "I", "UP", "ANN", "B", "SIM", "RUF", "TID", "BLE", "PGH", "T20"]
|
|
87
|
+
|
|
88
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
89
|
+
ban-relative-imports = "all"
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint.flake8-tidy-imports.banned-api]
|
|
92
|
+
"__future__.annotations".msg = "本规范禁用 from __future__ import annotations:注解保持活对象,前向/自引用类型加引号即可。"
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint.per-file-ignores]
|
|
95
|
+
# scripts/ 是命令行工具层:输出契约就是 stdout。
|
|
96
|
+
"scripts/*.py" = ["T20"]
|
|
97
|
+
|
|
98
|
+
[tool.pytest.ini_options]
|
|
99
|
+
addopts = "-q --import-mode=importlib"
|
|
100
|
+
testpaths = ["tests"]
|
|
101
|
+
filterwarnings = ["error"]
|