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.
@@ -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,2 @@
1
+ [console_scripts]
2
+ pyda = pyda.__main__:main
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ }