pyda-config 0.0.1__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.
- pyda_config-0.0.1/LICENSE +21 -0
- pyda_config-0.0.1/PKG-INFO +125 -0
- pyda_config-0.0.1/README.md +100 -0
- pyda_config-0.0.1/pyda/__init__.py +146 -0
- pyda_config-0.0.1/pyda/__main__.py +42 -0
- pyda_config-0.0.1/pyda_config.egg-info/PKG-INFO +125 -0
- pyda_config-0.0.1/pyda_config.egg-info/SOURCES.txt +12 -0
- pyda_config-0.0.1/pyda_config.egg-info/dependency_links.txt +1 -0
- pyda_config-0.0.1/pyda_config.egg-info/entry_points.txt +2 -0
- pyda_config-0.0.1/pyda_config.egg-info/top_level.txt +1 -0
- pyda_config-0.0.1/pyproject.toml +41 -0
- pyda_config-0.0.1/setup.cfg +4 -0
- pyda_config-0.0.1/tests/test_invalid.py +71 -0
- pyda_config-0.0.1/tests/test_valid.da.py +23 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 wangzk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyda-config
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python Dict Annotation — 受限字面量子集的 .da.py 配置文件校验与加载
|
|
5
|
+
Author-email: wangzk <biocory42@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: config,dict,literal,ast,annotation,validation
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Text Processing
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# PyDA — Python Dict Annotation
|
|
27
|
+
|
|
28
|
+
受限的 Python 字面量子集,用于**人类可读、可注释、可调试**的配置文件。
|
|
29
|
+
|
|
30
|
+
扩展名约定 `.da.py`(Dict Annotation),文件内容是一个**裸 dict 表达式**(可嵌套),只能用字面量,不能有任何求值、赋值、导入、调用。通过白名单 AST 校验保证它能被 `ast.literal_eval` 安全求值。
|
|
31
|
+
|
|
32
|
+
## 为什么
|
|
33
|
+
|
|
34
|
+
- 比 JSON 多了**注释**和**尾逗号**;
|
|
35
|
+
- 比 YAML 多了**结构严格、零歧义**;
|
|
36
|
+
- 比「真正的 Python 模块」多了**安全保证**(不执行任意代码,不引用外部符号)。
|
|
37
|
+
|
|
38
|
+
适合作为纳管多套资源的注册表(集群、服务、账号等)。
|
|
39
|
+
|
|
40
|
+
## 子集规则
|
|
41
|
+
|
|
42
|
+
### 允许
|
|
43
|
+
|
|
44
|
+
| 类型 | 语法 |
|
|
45
|
+
|---|---|
|
|
46
|
+
| 字典 | `{k: v}` |
|
|
47
|
+
| 列表 / 元组 / 集合 | `[...]` / `(...)` / `{...}` |
|
|
48
|
+
| 标量 | str / int / float / bool / None |
|
|
49
|
+
| 一元正负号 | `-1` / `+2` |
|
|
50
|
+
| 嵌套 | 任意深度 |
|
|
51
|
+
|
|
52
|
+
### 禁止(一旦出现即拒收)
|
|
53
|
+
|
|
54
|
+
- 裸变量名 `Name`(最关键的安全闸 —— 禁掉它就无法引用任何外部符号)
|
|
55
|
+
- 赋值 / 海象 `:=`
|
|
56
|
+
- `import`
|
|
57
|
+
- 函数调用 `f(...)`
|
|
58
|
+
- 二元运算、布尔运算、比较 `a + b` / `a and b` / `a == b`
|
|
59
|
+
- 属性 / 下标 / 切片 `a.b` / `a[b]`
|
|
60
|
+
- 列表 / 字典 / 集合推导式
|
|
61
|
+
- `def` / `class` / `lambda` / `for` / `if` 等一切语句
|
|
62
|
+
- `bytes` / `complex` / `...` 常量
|
|
63
|
+
|
|
64
|
+
## 用法
|
|
65
|
+
|
|
66
|
+
### 命令行
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python -m pyda clusters.da.py credentials.da.py
|
|
70
|
+
# 或 pip install -e . 之后:
|
|
71
|
+
pyda clusters.da.py credentials.da.py
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
通过打印 `OK: <file>` 退出 0;不通过逐条报错并退出 1。
|
|
75
|
+
|
|
76
|
+
### Python API
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pyda import check, load
|
|
80
|
+
|
|
81
|
+
# 只校验
|
|
82
|
+
errors = check("clusters.da.py")
|
|
83
|
+
assert errors == []
|
|
84
|
+
|
|
85
|
+
# 校验 + 求值
|
|
86
|
+
cfg = load("clusters.da.py")
|
|
87
|
+
print(cfg["clusters"][0]["name"])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 示例 `.da.py` 文件
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
{
|
|
94
|
+
"name": "a-cluster",
|
|
95
|
+
"port": 22,
|
|
96
|
+
"sudo": False, # 注释 OK
|
|
97
|
+
"tags": ["cpu", "gpu"],
|
|
98
|
+
"storage": {
|
|
99
|
+
"shared": "/shared/a",
|
|
100
|
+
"quota_gb": 5000,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
被拒收的写法:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
HOST = "login01" # ✗ 赋值
|
|
109
|
+
import os # ✗ import
|
|
110
|
+
{"a": len([1, 2])} # ✗ 函数调用
|
|
111
|
+
{"a": my_var} # ✗ 裸变量名
|
|
112
|
+
config["b"] # ✗ 下标
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 安装
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install -e .
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
仅依赖 Python 标准库。
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# PyDA — Python Dict Annotation
|
|
2
|
+
|
|
3
|
+
受限的 Python 字面量子集,用于**人类可读、可注释、可调试**的配置文件。
|
|
4
|
+
|
|
5
|
+
扩展名约定 `.da.py`(Dict Annotation),文件内容是一个**裸 dict 表达式**(可嵌套),只能用字面量,不能有任何求值、赋值、导入、调用。通过白名单 AST 校验保证它能被 `ast.literal_eval` 安全求值。
|
|
6
|
+
|
|
7
|
+
## 为什么
|
|
8
|
+
|
|
9
|
+
- 比 JSON 多了**注释**和**尾逗号**;
|
|
10
|
+
- 比 YAML 多了**结构严格、零歧义**;
|
|
11
|
+
- 比「真正的 Python 模块」多了**安全保证**(不执行任意代码,不引用外部符号)。
|
|
12
|
+
|
|
13
|
+
适合作为纳管多套资源的注册表(集群、服务、账号等)。
|
|
14
|
+
|
|
15
|
+
## 子集规则
|
|
16
|
+
|
|
17
|
+
### 允许
|
|
18
|
+
|
|
19
|
+
| 类型 | 语法 |
|
|
20
|
+
|---|---|
|
|
21
|
+
| 字典 | `{k: v}` |
|
|
22
|
+
| 列表 / 元组 / 集合 | `[...]` / `(...)` / `{...}` |
|
|
23
|
+
| 标量 | str / int / float / bool / None |
|
|
24
|
+
| 一元正负号 | `-1` / `+2` |
|
|
25
|
+
| 嵌套 | 任意深度 |
|
|
26
|
+
|
|
27
|
+
### 禁止(一旦出现即拒收)
|
|
28
|
+
|
|
29
|
+
- 裸变量名 `Name`(最关键的安全闸 —— 禁掉它就无法引用任何外部符号)
|
|
30
|
+
- 赋值 / 海象 `:=`
|
|
31
|
+
- `import`
|
|
32
|
+
- 函数调用 `f(...)`
|
|
33
|
+
- 二元运算、布尔运算、比较 `a + b` / `a and b` / `a == b`
|
|
34
|
+
- 属性 / 下标 / 切片 `a.b` / `a[b]`
|
|
35
|
+
- 列表 / 字典 / 集合推导式
|
|
36
|
+
- `def` / `class` / `lambda` / `for` / `if` 等一切语句
|
|
37
|
+
- `bytes` / `complex` / `...` 常量
|
|
38
|
+
|
|
39
|
+
## 用法
|
|
40
|
+
|
|
41
|
+
### 命令行
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python -m pyda clusters.da.py credentials.da.py
|
|
45
|
+
# 或 pip install -e . 之后:
|
|
46
|
+
pyda clusters.da.py credentials.da.py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
通过打印 `OK: <file>` 退出 0;不通过逐条报错并退出 1。
|
|
50
|
+
|
|
51
|
+
### Python API
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from pyda import check, load
|
|
55
|
+
|
|
56
|
+
# 只校验
|
|
57
|
+
errors = check("clusters.da.py")
|
|
58
|
+
assert errors == []
|
|
59
|
+
|
|
60
|
+
# 校验 + 求值
|
|
61
|
+
cfg = load("clusters.da.py")
|
|
62
|
+
print(cfg["clusters"][0]["name"])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 示例 `.da.py` 文件
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
{
|
|
69
|
+
"name": "a-cluster",
|
|
70
|
+
"port": 22,
|
|
71
|
+
"sudo": False, # 注释 OK
|
|
72
|
+
"tags": ["cpu", "gpu"],
|
|
73
|
+
"storage": {
|
|
74
|
+
"shared": "/shared/a",
|
|
75
|
+
"quota_gb": 5000,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
被拒收的写法:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
HOST = "login01" # ✗ 赋值
|
|
84
|
+
import os # ✗ import
|
|
85
|
+
{"a": len([1, 2])} # ✗ 函数调用
|
|
86
|
+
{"a": my_var} # ✗ 裸变量名
|
|
87
|
+
config["b"] # ✗ 下标
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 安装
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install -e .
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
仅依赖 Python 标准库。
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""PyDA — Python Dict Annotation.
|
|
2
|
+
|
|
3
|
+
受限的 Python 字面量子集,用于人类可读、可注释、可调试的配置文件。
|
|
4
|
+
|
|
5
|
+
子集规则(白名单 AST 节点):
|
|
6
|
+
允许: Dict/List/Tuple/Set/Constant/UnaryOp(UAdd,USub) + 模块层 Expr
|
|
7
|
+
禁止: Name(裸变量)、Assign/Import/Call/BinOp/Attribute/Subscript/推导式/def/class/...
|
|
8
|
+
|
|
9
|
+
加载方式: check() 校验 → ast.literal_eval() 求值。
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
__version__ = "0.0.1"
|
|
19
|
+
|
|
20
|
+
__all__ = ["check", "check_many", "load", "DaCheckError", "ALLOWED_NODES"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# 允许的 AST 节点类型白名单。
|
|
24
|
+
# Constant 覆盖 str/int/float/bool/None (Py3.8+)。
|
|
25
|
+
# UnaryOp + UAdd/USub 允许写 -1、+2 这样的负正号。
|
|
26
|
+
ALLOWED_NODES: frozenset[type[ast.AST]] = frozenset({
|
|
27
|
+
ast.Dict,
|
|
28
|
+
ast.List,
|
|
29
|
+
ast.Tuple,
|
|
30
|
+
ast.Set,
|
|
31
|
+
ast.Constant,
|
|
32
|
+
ast.UnaryOp,
|
|
33
|
+
ast.UAdd,
|
|
34
|
+
ast.USub,
|
|
35
|
+
ast.Expr,
|
|
36
|
+
ast.Module,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
# Constant 节点允许的值类型(进一步收紧,排除 bytes/complex/Ellipsis)。
|
|
40
|
+
_ALLOWED_CONSTANT_TYPES: tuple[type, ...] = (str, int, float, bool, type(None))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DaCheckError(ValueError):
|
|
44
|
+
"""PyDA 文件未通过子集校验。"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class _Issue:
|
|
49
|
+
line: int
|
|
50
|
+
msg: str
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
return f"line {self.line}: {self.msg}"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _check_source(src: str, filename: str) -> list[str]:
|
|
57
|
+
"""校验源码字符串,返回错误信息列表(空表示通过)。"""
|
|
58
|
+
issues: list[_Issue] = []
|
|
59
|
+
|
|
60
|
+
# 关卡 1: 语法解析
|
|
61
|
+
try:
|
|
62
|
+
tree = ast.parse(src, filename=filename)
|
|
63
|
+
except SyntaxError as e:
|
|
64
|
+
return [f"line {e.lineno or 0}: syntax error: {e.msg}"]
|
|
65
|
+
|
|
66
|
+
if not tree.body:
|
|
67
|
+
return ["file is empty (no top-level expression)"]
|
|
68
|
+
|
|
69
|
+
# 关卡 2: 模块层只允许 Expr(裸表达式)
|
|
70
|
+
for node in tree.body:
|
|
71
|
+
if not isinstance(node, ast.Expr):
|
|
72
|
+
issues.append(_Issue(node.lineno,
|
|
73
|
+
f"top-level only allows bare expression, "
|
|
74
|
+
f"forbidden {type(node).__name__} statement"))
|
|
75
|
+
|
|
76
|
+
# 关卡 3: 节点白名单遍历
|
|
77
|
+
# 只检查"带位置"的节点(有 lineno):真正的语法结构。
|
|
78
|
+
# 跳过操作符(Add/Mult/Eq)和上下文(Load/Store)等无位置子节点。
|
|
79
|
+
for node in ast.walk(tree):
|
|
80
|
+
if isinstance(node, ast.Module):
|
|
81
|
+
continue
|
|
82
|
+
if isinstance(node, ast.Expr):
|
|
83
|
+
continue
|
|
84
|
+
if not hasattr(node, "lineno"):
|
|
85
|
+
continue
|
|
86
|
+
# Name 是最关键的安全闸:禁止一切裸变量引用。
|
|
87
|
+
if isinstance(node, ast.Name):
|
|
88
|
+
issues.append(_Issue(node.lineno,
|
|
89
|
+
f"forbidden name '{node.id}', "
|
|
90
|
+
f"only literals allowed"))
|
|
91
|
+
continue
|
|
92
|
+
if isinstance(node, ast.Constant):
|
|
93
|
+
if not isinstance(node.value, _ALLOWED_CONSTANT_TYPES):
|
|
94
|
+
issues.append(_Issue(node.lineno,
|
|
95
|
+
f"forbidden constant type "
|
|
96
|
+
f"{type(node.value).__name__}, "
|
|
97
|
+
f"only str/int/float/bool/None"))
|
|
98
|
+
continue
|
|
99
|
+
if type(node) not in ALLOWED_NODES:
|
|
100
|
+
issues.append(_Issue(node.lineno,
|
|
101
|
+
f"forbidden node {type(node).__name__}"))
|
|
102
|
+
|
|
103
|
+
# 关卡 4: 双保险,白名单通过后再 literal_eval
|
|
104
|
+
if not issues:
|
|
105
|
+
try:
|
|
106
|
+
ast.literal_eval(src)
|
|
107
|
+
except Exception as e: # noqa: BLE001
|
|
108
|
+
issues.append(_Issue(0, f"literal_eval failed: {e}"))
|
|
109
|
+
|
|
110
|
+
return [f"line {i.line}: {i.msg}" if i.line else i.msg for i in issues]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def check(path: str | Path) -> list[str]:
|
|
114
|
+
"""校验单个 .da.py 文件。
|
|
115
|
+
|
|
116
|
+
返回错误信息列表;为空表示通过子集校验。
|
|
117
|
+
"""
|
|
118
|
+
p = Path(path)
|
|
119
|
+
if not p.exists():
|
|
120
|
+
return [f"{p}: file not found"]
|
|
121
|
+
src = p.read_text(encoding="utf-8")
|
|
122
|
+
return _check_source(src, str(p))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_many(paths) -> dict[Path, list[str]]:
|
|
126
|
+
"""批量校验,返回 {path: errors}。"""
|
|
127
|
+
result = {}
|
|
128
|
+
for path in paths:
|
|
129
|
+
p = Path(path)
|
|
130
|
+
result[p] = check(p)
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def load(path: str | Path):
|
|
135
|
+
"""校验通过后返回 literal_eval 的求值结果。
|
|
136
|
+
|
|
137
|
+
未通过校验则 raise DaCheckError,错误信息附在异常中。
|
|
138
|
+
"""
|
|
139
|
+
p = Path(path)
|
|
140
|
+
errors = check(p)
|
|
141
|
+
if errors:
|
|
142
|
+
raise DaCheckError(
|
|
143
|
+
f"{p} 未通过 PyDA 子集校验:\n - " + "\n - ".join(errors)
|
|
144
|
+
)
|
|
145
|
+
src = p.read_text(encoding="utf-8")
|
|
146
|
+
return ast.literal_eval(src)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""PyDA 命令行入口。
|
|
2
|
+
|
|
3
|
+
用法:
|
|
4
|
+
python -m pyda <file.da.py> [file2.da.py ...]
|
|
5
|
+
pyda <file.da.py> [file2.da.py ...] (pip install 后)
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from . import check_many
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
args = sys.argv[1:] if argv is None else argv
|
|
17
|
+
|
|
18
|
+
if not args:
|
|
19
|
+
print("usage: pyda <file.da.py> [file2.da.py ...]")
|
|
20
|
+
print("validate .da.py files against the PyDA literal subset")
|
|
21
|
+
return 2
|
|
22
|
+
|
|
23
|
+
results = check_many(args)
|
|
24
|
+
|
|
25
|
+
all_ok = True
|
|
26
|
+
for path, errors in results.items():
|
|
27
|
+
if not errors:
|
|
28
|
+
print(f"OK: {path}")
|
|
29
|
+
else:
|
|
30
|
+
all_ok = False
|
|
31
|
+
for e in errors:
|
|
32
|
+
print(f"{path}: {e}", file=sys.stderr)
|
|
33
|
+
|
|
34
|
+
if all_ok:
|
|
35
|
+
print(f"passed: {len(results)} file(s) conform to PyDA subset",
|
|
36
|
+
file=sys.stderr)
|
|
37
|
+
return 0
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyda-config
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Python Dict Annotation — 受限字面量子集的 .da.py 配置文件校验与加载
|
|
5
|
+
Author-email: wangzk <biocory42@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: config,dict,literal,ast,annotation,validation
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Text Processing
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# PyDA — Python Dict Annotation
|
|
27
|
+
|
|
28
|
+
受限的 Python 字面量子集,用于**人类可读、可注释、可调试**的配置文件。
|
|
29
|
+
|
|
30
|
+
扩展名约定 `.da.py`(Dict Annotation),文件内容是一个**裸 dict 表达式**(可嵌套),只能用字面量,不能有任何求值、赋值、导入、调用。通过白名单 AST 校验保证它能被 `ast.literal_eval` 安全求值。
|
|
31
|
+
|
|
32
|
+
## 为什么
|
|
33
|
+
|
|
34
|
+
- 比 JSON 多了**注释**和**尾逗号**;
|
|
35
|
+
- 比 YAML 多了**结构严格、零歧义**;
|
|
36
|
+
- 比「真正的 Python 模块」多了**安全保证**(不执行任意代码,不引用外部符号)。
|
|
37
|
+
|
|
38
|
+
适合作为纳管多套资源的注册表(集群、服务、账号等)。
|
|
39
|
+
|
|
40
|
+
## 子集规则
|
|
41
|
+
|
|
42
|
+
### 允许
|
|
43
|
+
|
|
44
|
+
| 类型 | 语法 |
|
|
45
|
+
|---|---|
|
|
46
|
+
| 字典 | `{k: v}` |
|
|
47
|
+
| 列表 / 元组 / 集合 | `[...]` / `(...)` / `{...}` |
|
|
48
|
+
| 标量 | str / int / float / bool / None |
|
|
49
|
+
| 一元正负号 | `-1` / `+2` |
|
|
50
|
+
| 嵌套 | 任意深度 |
|
|
51
|
+
|
|
52
|
+
### 禁止(一旦出现即拒收)
|
|
53
|
+
|
|
54
|
+
- 裸变量名 `Name`(最关键的安全闸 —— 禁掉它就无法引用任何外部符号)
|
|
55
|
+
- 赋值 / 海象 `:=`
|
|
56
|
+
- `import`
|
|
57
|
+
- 函数调用 `f(...)`
|
|
58
|
+
- 二元运算、布尔运算、比较 `a + b` / `a and b` / `a == b`
|
|
59
|
+
- 属性 / 下标 / 切片 `a.b` / `a[b]`
|
|
60
|
+
- 列表 / 字典 / 集合推导式
|
|
61
|
+
- `def` / `class` / `lambda` / `for` / `if` 等一切语句
|
|
62
|
+
- `bytes` / `complex` / `...` 常量
|
|
63
|
+
|
|
64
|
+
## 用法
|
|
65
|
+
|
|
66
|
+
### 命令行
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python -m pyda clusters.da.py credentials.da.py
|
|
70
|
+
# 或 pip install -e . 之后:
|
|
71
|
+
pyda clusters.da.py credentials.da.py
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
通过打印 `OK: <file>` 退出 0;不通过逐条报错并退出 1。
|
|
75
|
+
|
|
76
|
+
### Python API
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from pyda import check, load
|
|
80
|
+
|
|
81
|
+
# 只校验
|
|
82
|
+
errors = check("clusters.da.py")
|
|
83
|
+
assert errors == []
|
|
84
|
+
|
|
85
|
+
# 校验 + 求值
|
|
86
|
+
cfg = load("clusters.da.py")
|
|
87
|
+
print(cfg["clusters"][0]["name"])
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 示例 `.da.py` 文件
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
{
|
|
94
|
+
"name": "a-cluster",
|
|
95
|
+
"port": 22,
|
|
96
|
+
"sudo": False, # 注释 OK
|
|
97
|
+
"tags": ["cpu", "gpu"],
|
|
98
|
+
"storage": {
|
|
99
|
+
"shared": "/shared/a",
|
|
100
|
+
"quota_gb": 5000,
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
被拒收的写法:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
HOST = "login01" # ✗ 赋值
|
|
109
|
+
import os # ✗ import
|
|
110
|
+
{"a": len([1, 2])} # ✗ 函数调用
|
|
111
|
+
{"a": my_var} # ✗ 裸变量名
|
|
112
|
+
config["b"] # ✗ 下标
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 安装
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install -e .
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
仅依赖 Python 标准库。
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pyda/__init__.py
|
|
5
|
+
pyda/__main__.py
|
|
6
|
+
pyda_config.egg-info/PKG-INFO
|
|
7
|
+
pyda_config.egg-info/SOURCES.txt
|
|
8
|
+
pyda_config.egg-info/dependency_links.txt
|
|
9
|
+
pyda_config.egg-info/entry_points.txt
|
|
10
|
+
pyda_config.egg-info/top_level.txt
|
|
11
|
+
tests/test_invalid.py
|
|
12
|
+
tests/test_valid.da.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyda
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyda-config"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Python Dict Annotation — 受限字面量子集的 .da.py 配置文件校验与加载"
|
|
9
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "wangzk", email = "biocory42@gmail.com" }]
|
|
13
|
+
keywords = ["config", "dict", "literal", "ast", "annotation", "validation"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.8",
|
|
21
|
+
"Programming Language :: Python :: 3.9",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Topic :: Text Processing",
|
|
28
|
+
]
|
|
29
|
+
dependencies = [] # 纯标准库
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
# Homepage = "https://github.com/<user>/pyda" # 仓库公开后补上
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
pyda = "pyda.__main__:main"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools]
|
|
38
|
+
packages = ["pyda"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""PyDA 非法写法测试:各种应被拒收的子集违规。
|
|
2
|
+
|
|
3
|
+
每条是一个独立的非法源码片段,_check_source 应返回非空错误列表。
|
|
4
|
+
运行: python tests/test_invalid.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# 确保 UTF-8 输出(Windows GBK 终端兼容)
|
|
13
|
+
for stream in (sys.stdout, sys.stderr):
|
|
14
|
+
if hasattr(stream, "reconfigure"):
|
|
15
|
+
stream.reconfigure(encoding="utf-8")
|
|
16
|
+
|
|
17
|
+
# 让脚本能直接 import 已安装的 pyda
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
19
|
+
|
|
20
|
+
from pyda import _check_source # type: ignore # noqa: E402
|
|
21
|
+
from pyda import check, load # noqa: E402
|
|
22
|
+
|
|
23
|
+
INVALID_CASES = [
|
|
24
|
+
('HOST = "login01"', "assignment"),
|
|
25
|
+
("import os", "import"),
|
|
26
|
+
('{"a": len([1, 2])}', "function call"),
|
|
27
|
+
('{"a": my_var}', "bare name"),
|
|
28
|
+
('config["b"]', "subscript"),
|
|
29
|
+
("a.b", "attribute"),
|
|
30
|
+
("1 + 2", "binary op"),
|
|
31
|
+
("True and False", "bool op"),
|
|
32
|
+
("1 == 1", "compare"),
|
|
33
|
+
("[x for x in range(3)]", "list comprehension"),
|
|
34
|
+
("{k: 1 for k in [1,2]}", "dict comprehension"),
|
|
35
|
+
("(y := 1)", "walrus"),
|
|
36
|
+
("def f(): pass", "def statement"),
|
|
37
|
+
("(lambda: 1)", "lambda"),
|
|
38
|
+
('b"data"', "bytes constant"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main() -> int:
|
|
43
|
+
failures = []
|
|
44
|
+
for i, (src, desc) in enumerate(INVALID_CASES, 1):
|
|
45
|
+
errors = _check_source(src, f"<case {i}>")
|
|
46
|
+
if not errors:
|
|
47
|
+
failures.append((i, desc, src))
|
|
48
|
+
print(f"FAIL [{desc}] NOT rejected, src: {src}")
|
|
49
|
+
else:
|
|
50
|
+
print(f"OK [{desc}] rejected: {errors[0]}")
|
|
51
|
+
|
|
52
|
+
# 合法文件应能通过 check + load
|
|
53
|
+
valid = Path(__file__).resolve().parent / "test_valid.da.py"
|
|
54
|
+
if check(valid):
|
|
55
|
+
failures.append((0, "valid file rejected", str(valid)))
|
|
56
|
+
print(f"FAIL [valid file] rejected: {check(valid)}")
|
|
57
|
+
else:
|
|
58
|
+
cfg = load(valid)
|
|
59
|
+
print(f"OK [valid file] loaded: "
|
|
60
|
+
f"{len(cfg['nested']['partitions'])} partitions")
|
|
61
|
+
|
|
62
|
+
if failures:
|
|
63
|
+
print(f"\n{len(failures)} case(s) failed", file=sys.stderr)
|
|
64
|
+
return 1
|
|
65
|
+
print(f"\nall {len(INVALID_CASES)} invalid cases rejected, "
|
|
66
|
+
f"valid file loaded", file=sys.stderr)
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
# 顶层裸 dict,带注释
|
|
3
|
+
"name": "a-cluster",
|
|
4
|
+
"host": "login01.example.edu",
|
|
5
|
+
"port": 22,
|
|
6
|
+
"sudo": False, # bool 是 Constant,不是 Name
|
|
7
|
+
"tags": ["cpu", "gpu"], # 列表
|
|
8
|
+
"quota": -1, # 一元负号 OK
|
|
9
|
+
"ratio": +0.5, # 一元正号 OK
|
|
10
|
+
"nested": {
|
|
11
|
+
"storage": {
|
|
12
|
+
"shared": "/shared/a",
|
|
13
|
+
"quota_gb": 5000,
|
|
14
|
+
"purge_days": 30,
|
|
15
|
+
},
|
|
16
|
+
"partitions": [
|
|
17
|
+
{"name": "cpu", "max_walltime": "72:00:00"},
|
|
18
|
+
{"name": "gpu", "gres": "gpu:a100:4", "max_walltime": "48:00:00"},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
"notes": None, # None 是 Constant
|
|
22
|
+
"empty": {}, # 空 dict OK
|
|
23
|
+
}
|