openpuppet-language 2.0__py3-none-any.whl
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.
- openpuppet_language-2.0.data/data/share/puppet/conformance/README.md +274 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/binding.json +238 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/capabilities.json +155 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/grammar.json +248 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/interaction.json +220 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/listen.json +133 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/platform.json +274 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/probes.json +117 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/render.json +180 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/tabs.json +39 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/cases/templates.json +143 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_bad_result.py +11 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_basic.py +24 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_deps_mismatch.py +11 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_no_doc.py +10 -0
- openpuppet_language-2.0.data/data/share/puppet/conformance/runner.py +721 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/01-grammar.md +283 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/02-ir.md +77 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/03-semantics.md +167 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/04-vocabulary.md +227 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/05-render-contract.md +126 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/06-diagnostics.md +151 -0
- openpuppet_language-2.0.data/data/share/puppet/spec/README.md +73 -0
- openpuppet_language-2.0.dist-info/METADATA +50 -0
- openpuppet_language-2.0.dist-info/RECORD +40 -0
- openpuppet_language-2.0.dist-info/WHEEL +5 -0
- openpuppet_language-2.0.dist-info/entry_points.txt +2 -0
- openpuppet_language-2.0.dist-info/top_level.txt +1 -0
- puppet/__init__.py +19 -0
- puppet/adapter.py +112 -0
- puppet/capabilities.py +172 -0
- puppet/cli.py +132 -0
- puppet/diag.py +42 -0
- puppet/engine.py +1038 -0
- puppet/eval.py +243 -0
- puppet/ir.py +538 -0
- puppet/lang.py +755 -0
- puppet/raster_adapter.py +348 -0
- puppet/tk_adapter.py +374 -0
- puppet/vocab.py +147 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/README.md,sha256=c0YLKLgVu05PgkP7UZGQCraonbJBt1C1QBpVR4ZlsuI,17495
|
|
2
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/runner.py,sha256=5psF1f4JaqUTYHpoJivR4eGGrETokHGjOvlx1Rsv7X0,32303
|
|
3
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/cases/binding.json,sha256=tbxo15lbtL5B1bPJcrjQeNz0RiAh7YQenJu8fln5RBs,6887
|
|
4
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/cases/capabilities.json,sha256=V8c3HjW0u3NQcNAbIXDr-ZmzZljLic29PaZxq7lp9Cw,5008
|
|
5
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/cases/grammar.json,sha256=eSe_60W8gpZUwidDjc4679DvVLlvUAn8-F30sXSB2zU,6668
|
|
6
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/cases/interaction.json,sha256=Pxa3cIcUhQDIQUVQ_B5lEaY0QHiRnZXrWXuwL4FH3qU,7556
|
|
7
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/cases/listen.json,sha256=Skk2HHzAjTOupRzrEg2IXnD4Iwna6eOX5UuPUw5hlwI,3939
|
|
8
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/cases/platform.json,sha256=Ikub0GSSR6OQZTZd63OBsmcB-gI-tXrWAlrPII6w05I,8042
|
|
9
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/cases/probes.json,sha256=OJ66wqXoQiuUpB4NgfykMJ3mg0_zbM9vgndTKrZj5QA,3024
|
|
10
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/cases/render.json,sha256=QjMcJO7Kn4E0j7ba_Rd_nfbexOXZUR7tQygyz1mFwYo,5957
|
|
11
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/cases/tabs.json,sha256=LZfCGt5exnVogvFk4qsraN14mYoqr94pQh_f3-5MNbc,973
|
|
12
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/cases/templates.json,sha256=6eMLhtvdbhb9raZ4h9AKkMsw6ms4P3n9ebEaPwvXTmY,4590
|
|
13
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_bad_result.py,sha256=CLcqL_fWmpqfmLvKoP1fJGl1BX78meXPVBlYYbCUt_I,306
|
|
14
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_basic.py,sha256=iMMUJF7Qslhc1ef_hZ5cwaVKMQdysQGnizWl1v01Sn0,675
|
|
15
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_deps_mismatch.py,sha256=Km57Ix7ZuqvTQpjKcCC6BYVtNrBmf8uc-tEefd5fyo4,316
|
|
16
|
+
openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_no_doc.py,sha256=qBa4U4d_tYg_27ryevra6UUziFI9tUWA4dERCvXUWy0,215
|
|
17
|
+
openpuppet_language-2.0.data/data/share/puppet/spec/01-grammar.md,sha256=ax5nNySdeXvybtKIphWPs8wpvVaM6rp3tFfPDFARrCc,13749
|
|
18
|
+
openpuppet_language-2.0.data/data/share/puppet/spec/02-ir.md,sha256=8Fjx8Wwl6kpwmVB5Y16A7SLOFQ-AHTnWPiZryasFNvQ,4124
|
|
19
|
+
openpuppet_language-2.0.data/data/share/puppet/spec/03-semantics.md,sha256=Ljc9TA28WnfBPYkz_3URr-PbJFlMDYunfps5ThKzfac,9813
|
|
20
|
+
openpuppet_language-2.0.data/data/share/puppet/spec/04-vocabulary.md,sha256=36Ao600pdHXQmPJ7n3_pPaLGrAp6eLaDxXJlSljbTZQ,9809
|
|
21
|
+
openpuppet_language-2.0.data/data/share/puppet/spec/05-render-contract.md,sha256=A9nYzVxItMvEeizr6gM_EKIbymB1suKIitfcPPpjLAw,9507
|
|
22
|
+
openpuppet_language-2.0.data/data/share/puppet/spec/06-diagnostics.md,sha256=Wpa5V3VfYGH7CbyuRzf6N7LdyxtsTKus4mTdGHRsU1c,8746
|
|
23
|
+
openpuppet_language-2.0.data/data/share/puppet/spec/README.md,sha256=GoSflg7wpY-hQ_fI0pGzP9kDRZ5spJgMGfXxrW0l4_U,5297
|
|
24
|
+
puppet/__init__.py,sha256=q4kHbcONrhkCj-u2MGOczy3mm0OvA9ni5auLEs2WKqE,639
|
|
25
|
+
puppet/adapter.py,sha256=WC_d2bIQtSO9h-KERjgrQyXC16i3lqPOz9diFCcI6Ns,4161
|
|
26
|
+
puppet/capabilities.py,sha256=wEnUJLT8-S7xXuNBAKWj1ItFhiFFhRBVDfQpBqLnyx4,7005
|
|
27
|
+
puppet/cli.py,sha256=5jGHDKZOgTR1fbxCbramiDuZO5faxctkHiEzV5xN1Pg,4681
|
|
28
|
+
puppet/diag.py,sha256=bWhP0BjBDlBr9Ywj_Xhxm05VU9dLrk3ioRaLdc8sJuQ,1200
|
|
29
|
+
puppet/engine.py,sha256=uyAfGzgHKZ0y5x3RoT6gvGjk2iuK4I2k4nKWtwe_9Sc,46198
|
|
30
|
+
puppet/eval.py,sha256=FC-l4iUBDbU2LTXb-30n4kbvjYTMq_xC45jgPQZB0dc,8673
|
|
31
|
+
puppet/ir.py,sha256=cP3M5TcLwpxf_X13B4lmU2OKRSzIzbBfrQwL2TGChfk,21452
|
|
32
|
+
puppet/lang.py,sha256=KJ6CC4tYgp0J1irNdauYqK4xNqRKt2wY88MW0U0HssY,23420
|
|
33
|
+
puppet/raster_adapter.py,sha256=btxTENjdZNU9pdwiu0J5fmKpYmLr7fFwlEkvoyi8z9I,15609
|
|
34
|
+
puppet/tk_adapter.py,sha256=JhStuobJu-oH6wNNQWGfuWIv2tfPabpZV4oyQtJnY44,16180
|
|
35
|
+
puppet/vocab.py,sha256=-mX92BjkGqAyOZbR7A9MyjQf78O1CDBcrbC6s5ifMB0,5506
|
|
36
|
+
openpuppet_language-2.0.dist-info/METADATA,sha256=FQK1r413FXZCScqecPAlMi-Nh6NmsXw525KmMSM_PIM,2047
|
|
37
|
+
openpuppet_language-2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
38
|
+
openpuppet_language-2.0.dist-info/entry_points.txt,sha256=T3PaN03V-_GiGTrMawIEh2J5OjWe54N2NpLcvwYy6vI,43
|
|
39
|
+
openpuppet_language-2.0.dist-info/top_level.txt,sha256=O6DGsH-AdDl1k7C2RtsmmtnKBX8kjSgIvhUy3pUiCSY,7
|
|
40
|
+
openpuppet_language-2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
puppet
|
puppet/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Puppet 语言标准实现。
|
|
2
|
+
|
|
3
|
+
本包是 `spec/` 下语言规范的**参考实现**:解析、校验、IR、执行语义。
|
|
4
|
+
它不包含任何渲染实现;渲染由下游软件(puppethub)按渲染契约提供。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .capabilities import capability, contract_of, load_capability_module
|
|
8
|
+
from .diag import Diagnostic, ERROR, INFO, WARNING
|
|
9
|
+
from .engine import Engine
|
|
10
|
+
from .ir import Program, new_program, validate
|
|
11
|
+
|
|
12
|
+
SPEC_VERSION = "2.0"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Diagnostic", "ERROR", "INFO", "WARNING",
|
|
16
|
+
"Engine", "Program", "new_program", "validate",
|
|
17
|
+
"capability", "contract_of", "load_capability_module",
|
|
18
|
+
"SPEC_VERSION",
|
|
19
|
+
]
|
puppet/adapter.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""conformance 适配器:行分隔 JSON over stdio(见 `conformance/README.md` 第 4 节)。
|
|
2
|
+
|
|
3
|
+
这个文件是"标准实现"与"外部测试者"之间的边界;任何语言都可以实现同一协议。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from . import vocab
|
|
12
|
+
from .engine import Engine
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def describe_rendering() -> dict:
|
|
16
|
+
"""参考渲染器的能力声明(启动握手 `hello` 的响应体)。
|
|
17
|
+
|
|
18
|
+
本实现是**无渲染器的语义引擎**:词汇全支持,但几何与截图**不提供**
|
|
19
|
+
(二者都必须走可见降级);可无 GUI 驱动。
|
|
20
|
+
- 数组 = 声明支持的子集;`null` = 支持全部标准(本实现选择显式列全)。
|
|
21
|
+
- `geometry` / `snapshot` / `headless` 是布尔观测/驱动能力。
|
|
22
|
+
"""
|
|
23
|
+
return {
|
|
24
|
+
"controls": list(vocab.NODE_TYPES),
|
|
25
|
+
"attributes": sorted(vocab.ALL_ATTRS),
|
|
26
|
+
"animations": sorted(vocab.ANIMATABLE),
|
|
27
|
+
"icons": sorted(vocab.CORE_ICONS),
|
|
28
|
+
"geometry": False,
|
|
29
|
+
"snapshot": False,
|
|
30
|
+
"interaction": False,
|
|
31
|
+
"headless": True,
|
|
32
|
+
"notes": "参考语义引擎:无渲染器;几何、截图与用户动作投递均按规范可见降级。"
|
|
33
|
+
"conformance 可用 load.renderGeometry / load.rendering 注入测试替身。",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def handle(engine: Engine, request: dict) -> dict:
|
|
38
|
+
op = request.get("op")
|
|
39
|
+
if op == "hello":
|
|
40
|
+
return {"protocol": "1", "rendering": describe_rendering()}
|
|
41
|
+
if op == "load":
|
|
42
|
+
diags = engine.load(
|
|
43
|
+
request.get("program", []),
|
|
44
|
+
capabilities=request.get("capabilities"),
|
|
45
|
+
limits=request.get("limits"),
|
|
46
|
+
seed_state=request.get("seedState"),
|
|
47
|
+
render_geometry=request.get("renderGeometry"),
|
|
48
|
+
rendering=request.get("rendering"),
|
|
49
|
+
capability_modules=request.get("capabilityModules"),
|
|
50
|
+
)
|
|
51
|
+
return {"diagnostics": [d.to_dict() for d in diags]}
|
|
52
|
+
if op == "send":
|
|
53
|
+
diags = engine.apply_batch(request.get("batch", []))
|
|
54
|
+
return {"diagnostics": [d.to_dict() for d in diags]}
|
|
55
|
+
if op == "fire":
|
|
56
|
+
diags = engine.fire(request.get("target", ""), request.get("event", ""),
|
|
57
|
+
request.get("row"), request.get("value"))
|
|
58
|
+
return {"diagnostics": [d.to_dict() for d in diags]}
|
|
59
|
+
if op == "restart":
|
|
60
|
+
diags = engine.restart()
|
|
61
|
+
return {"diagnostics": [d.to_dict() for d in diags]}
|
|
62
|
+
if op == "observe":
|
|
63
|
+
return engine.observe()
|
|
64
|
+
if op == "interact":
|
|
65
|
+
return engine.deliver_interaction(request.get("target", ""),
|
|
66
|
+
request.get("action", ""),
|
|
67
|
+
request.get("value"))
|
|
68
|
+
if op == "snapshot":
|
|
69
|
+
return engine.snapshot()
|
|
70
|
+
if op == "quit":
|
|
71
|
+
return {"ok": True}
|
|
72
|
+
return {"error": "未知操作 %r" % op}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _emit(payload: dict) -> None:
|
|
76
|
+
"""输出纯 ASCII 的行分隔 JSON。
|
|
77
|
+
|
|
78
|
+
刻意用 `ensure_ascii=True`:协议不依赖子进程的标准输出编码,
|
|
79
|
+
在 Windows 上尤为关键(默认可能是 GBK)。
|
|
80
|
+
"""
|
|
81
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=True) + "\n")
|
|
82
|
+
sys.stdout.flush()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def main() -> int:
|
|
86
|
+
try:
|
|
87
|
+
sys.stdin.reconfigure(encoding="utf-8", errors="replace")
|
|
88
|
+
sys.stdout.reconfigure(encoding="ascii", errors="backslashreplace")
|
|
89
|
+
except Exception: # noqa: BLE001 - 老环境没有 reconfigure 也不致命
|
|
90
|
+
pass
|
|
91
|
+
engine = Engine()
|
|
92
|
+
for line in sys.stdin:
|
|
93
|
+
line = line.strip()
|
|
94
|
+
if not line:
|
|
95
|
+
continue
|
|
96
|
+
try:
|
|
97
|
+
request = json.loads(line)
|
|
98
|
+
except json.JSONDecodeError as ex:
|
|
99
|
+
_emit({"error": "请求不是合法 JSON:%s" % ex})
|
|
100
|
+
continue
|
|
101
|
+
try:
|
|
102
|
+
response = handle(engine, request)
|
|
103
|
+
except Exception as ex: # noqa: BLE001 - 适配器绝不静默:错误原样回传
|
|
104
|
+
response = {"error": "%s: %s" % (type(ex).__name__, ex)}
|
|
105
|
+
_emit(response)
|
|
106
|
+
if request.get("op") == "quit":
|
|
107
|
+
break
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
raise SystemExit(main())
|
puppet/capabilities.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""能力(工具)的契约提取与真实加载(规范 `03-semantics.md` 第 4 节)。
|
|
2
|
+
|
|
3
|
+
三条硬性义务,缺一即违规:
|
|
4
|
+
|
|
5
|
+
- **说明文本非空**:docstring 是 LLM 发现工具的唯一说明书(`03` 第 4.5 节),
|
|
6
|
+
缺失即 **错误** `CAP_NO_DOC`,且该能力**不注册**。
|
|
7
|
+
- **依赖显式声明 + 扫描兜底 + 不一致必须报警**:模块级 `REQUIRES` 与源码实际 import
|
|
8
|
+
不一致时 **警告** `CAP_DEPS_MISMATCH`——否则部署机上才缺包(v1 的教训)。
|
|
9
|
+
- **返回值可序列化**:由引擎校验(`CALL_RESULT`);`async` 函数直接 await,
|
|
10
|
+
同步函数走线程池,超时 / 取消由槽负责。
|
|
11
|
+
|
|
12
|
+
测试替身(conformance 的 `load.capabilities`)与真实模块(`load.capabilityModules`)
|
|
13
|
+
走**同一份契约形状**,因此行为用例对二者同样适用。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
import importlib.util
|
|
20
|
+
import inspect
|
|
21
|
+
import os
|
|
22
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
23
|
+
|
|
24
|
+
from .diag import ERROR, INFO, WARNING, Diagnostic
|
|
25
|
+
|
|
26
|
+
MARKER = "__puppet_capability__"
|
|
27
|
+
|
|
28
|
+
_PY_TYPES = {"str": str, "int": int, "float": float, "bool": bool,
|
|
29
|
+
"list": list, "dict": dict, "any": object}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def capability(fn=None, *, name: Optional[str] = None, returns: Any = None,
|
|
33
|
+
requires: Optional[List[str]] = None):
|
|
34
|
+
"""把一个函数标记为能力。
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
@capability()
|
|
38
|
+
def compress(path: str, level: int = 6) -> dict:
|
|
39
|
+
\"\"\"压缩文件,返回 {"bytes": <大小>}。\"\"\"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`requires` 是**本能力**额外需要的依赖;模块级 `REQUIRES` 覆盖全模块。
|
|
43
|
+
"""
|
|
44
|
+
def wrap(func):
|
|
45
|
+
setattr(func, MARKER, {"name": name or func.__name__,
|
|
46
|
+
"returns": returns, "requires": requires})
|
|
47
|
+
return func
|
|
48
|
+
return wrap if fn is None else wrap(fn)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def type_name(annotation: Any) -> str:
|
|
52
|
+
"""把类型注解归一化成规范里的类型名(`str` / `int` / …)。"""
|
|
53
|
+
if annotation is inspect.Parameter.empty or annotation is None:
|
|
54
|
+
return "any"
|
|
55
|
+
if isinstance(annotation, str):
|
|
56
|
+
return annotation.strip() or "any"
|
|
57
|
+
return getattr(annotation, "__name__", "any")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def contract_of(func) -> Optional[Dict[str, Any]]:
|
|
61
|
+
"""从签名与类型注解提取契约;非能力函数返回 `None`。"""
|
|
62
|
+
marker = getattr(func, MARKER, None)
|
|
63
|
+
if marker is None:
|
|
64
|
+
return None
|
|
65
|
+
try:
|
|
66
|
+
sig = inspect.signature(func)
|
|
67
|
+
except (TypeError, ValueError): # 不可 introspect 的可调用对象
|
|
68
|
+
params = []
|
|
69
|
+
else:
|
|
70
|
+
params = []
|
|
71
|
+
for pname, param in sig.parameters.items():
|
|
72
|
+
if pname in ("self", "cls"):
|
|
73
|
+
continue
|
|
74
|
+
params.append({
|
|
75
|
+
"name": pname,
|
|
76
|
+
"type": type_name(param.annotation),
|
|
77
|
+
"required": param.default is inspect.Parameter.empty,
|
|
78
|
+
**({} if param.default is inspect.Parameter.empty
|
|
79
|
+
else {"default": param.default}),
|
|
80
|
+
})
|
|
81
|
+
doc = (func.__doc__ or "").strip()
|
|
82
|
+
return {
|
|
83
|
+
"name": marker.get("name") or func.__name__,
|
|
84
|
+
"params": params,
|
|
85
|
+
"returns": type_name(marker.get("returns"))
|
|
86
|
+
if marker.get("returns") is not None
|
|
87
|
+
else type_name(getattr(func, "__annotations__", {}).get("return")),
|
|
88
|
+
"doc": doc,
|
|
89
|
+
"requires": list(marker.get("requires") or []),
|
|
90
|
+
"callable": func,
|
|
91
|
+
"behavior": "callable",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def type_matches(declared: str, value: Any) -> bool:
|
|
96
|
+
"""按契约声明的类型判定实参;`any` 或未声明一律接受。"""
|
|
97
|
+
expected = _PY_TYPES.get(str(declared).lower())
|
|
98
|
+
if expected is None or expected is object:
|
|
99
|
+
return True
|
|
100
|
+
if expected is float and isinstance(value, int) and not isinstance(value, bool):
|
|
101
|
+
return True # 整数提升为浮点(规范 04 的隐式转换)
|
|
102
|
+
if expected is int and isinstance(value, bool):
|
|
103
|
+
return False # 布尔不参与数值比较
|
|
104
|
+
return isinstance(value, expected)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _imported_modules(path: str) -> List[str]:
|
|
108
|
+
"""扫描源码的顶层 import(兜底:动态 import 扫不到,故仍要求显式声明)。"""
|
|
109
|
+
try:
|
|
110
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
111
|
+
tree = ast.parse(fh.read(), filename=path)
|
|
112
|
+
except (OSError, SyntaxError):
|
|
113
|
+
return []
|
|
114
|
+
found = set()
|
|
115
|
+
for node in tree.body:
|
|
116
|
+
if isinstance(node, ast.Import):
|
|
117
|
+
for alias in node.names:
|
|
118
|
+
found.add(alias.name.split(".")[0])
|
|
119
|
+
elif isinstance(node, ast.ImportFrom) and node.module and node.level == 0:
|
|
120
|
+
found.add(node.module.split(".")[0])
|
|
121
|
+
return sorted(found)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def load_capability_module(path: str) -> Tuple[Dict[str, dict], List[Diagnostic]]:
|
|
125
|
+
"""加载一个能力模块,返回 `(契约表, 诊断)`。
|
|
126
|
+
|
|
127
|
+
失败(找不到文件 / 导入报错)**必须可见**:`CAP_IMPORT` 错误,不静默跳过。
|
|
128
|
+
"""
|
|
129
|
+
diags: List[Diagnostic] = []
|
|
130
|
+
name = os.path.splitext(os.path.basename(path))[0]
|
|
131
|
+
if not os.path.isfile(path):
|
|
132
|
+
diags.append(Diagnostic("CAP_IMPORT", ERROR, "能力模块不存在:%s" % path))
|
|
133
|
+
return {}, diags
|
|
134
|
+
spec = importlib.util.spec_from_file_location("puppet_capability_" + name, path)
|
|
135
|
+
if spec is None or spec.loader is None:
|
|
136
|
+
diags.append(Diagnostic("CAP_IMPORT", ERROR, "能力模块无法加载:%s" % path))
|
|
137
|
+
return {}, diags
|
|
138
|
+
module = importlib.util.module_from_spec(spec)
|
|
139
|
+
try:
|
|
140
|
+
spec.loader.exec_module(module)
|
|
141
|
+
except Exception as ex: # noqa: BLE001 - 一律转为可见失败
|
|
142
|
+
diags.append(Diagnostic("CAP_IMPORT", ERROR,
|
|
143
|
+
"能力模块导入失败(%s):%s: %s"
|
|
144
|
+
% (name, type(ex).__name__, ex)))
|
|
145
|
+
return {}, diags
|
|
146
|
+
|
|
147
|
+
# 依赖核对:显式声明 vs 实际 import
|
|
148
|
+
declared = list(getattr(module, "REQUIRES", []) or [])
|
|
149
|
+
scanned = _imported_modules(path)
|
|
150
|
+
if declared and sorted(set(declared)) != scanned:
|
|
151
|
+
diags.append(Diagnostic(
|
|
152
|
+
"CAP_DEPS_MISMATCH", WARNING,
|
|
153
|
+
"能力模块 %s 的依赖声明与实际 import 不一致:声明 %s,实际 %s"
|
|
154
|
+
% (name, sorted(set(declared)), scanned)))
|
|
155
|
+
|
|
156
|
+
contracts: Dict[str, dict] = {}
|
|
157
|
+
for attr in vars(module).values():
|
|
158
|
+
contract = contract_of(attr)
|
|
159
|
+
if contract is None:
|
|
160
|
+
continue
|
|
161
|
+
if not contract["doc"]:
|
|
162
|
+
diags.append(Diagnostic(
|
|
163
|
+
"CAP_NO_DOC", ERROR,
|
|
164
|
+
"能力 %s 没有说明文本(docstring 是工具的唯一说明书),不予注册"
|
|
165
|
+
% contract["name"]))
|
|
166
|
+
continue
|
|
167
|
+
if contract["name"] in contracts:
|
|
168
|
+
diags.append(Diagnostic(
|
|
169
|
+
"CALL_UNKNOWN", ERROR, "能力 %s 重复定义" % contract["name"]))
|
|
170
|
+
continue
|
|
171
|
+
contracts[contract["name"]] = contract
|
|
172
|
+
return contracts, diags
|
puppet/cli.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""`puppet` 命令:标准工具(不需要 app 即可运行)。
|
|
2
|
+
|
|
3
|
+
子命令:`spec`(规范位置)、`validate`(静态校验)、`conformance`(跑合规用例)。
|
|
4
|
+
|
|
5
|
+
注意:`validate` 只做静态校验——解析与 IR 校验,不起运行时。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from . import SPEC_VERSION
|
|
16
|
+
from .engine import Engine
|
|
17
|
+
from .ir import apply_stmt, new_program, validate
|
|
18
|
+
from .lang import parse_program
|
|
19
|
+
|
|
20
|
+
def _assets_dir() -> str:
|
|
21
|
+
"""定位随发行包分发的 `spec/` 与 `conformance/`。
|
|
22
|
+
|
|
23
|
+
开发态(源码树 / `pip install -e`):仓库根。
|
|
24
|
+
安装态:setuptools 的 data-files 落在 `<prefix>/share/puppet`。
|
|
25
|
+
可用环境变量 `PUPPET_ASSETS` 覆盖(下游打包可自行安置资产)。
|
|
26
|
+
"""
|
|
27
|
+
override = os.environ.get("PUPPET_ASSETS")
|
|
28
|
+
if override:
|
|
29
|
+
return override
|
|
30
|
+
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
31
|
+
if os.path.isdir(os.path.join(root, "spec")):
|
|
32
|
+
return root
|
|
33
|
+
installed = os.path.join(sys.prefix, "share", "puppet")
|
|
34
|
+
if os.path.isdir(os.path.join(installed, "spec")):
|
|
35
|
+
return installed
|
|
36
|
+
return root
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
ROOT = _assets_dir()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def cmd_spec(_args) -> int:
|
|
43
|
+
path = os.path.join(ROOT, "spec")
|
|
44
|
+
if not os.path.isdir(path):
|
|
45
|
+
print("找不到规范目录:%s(发行包安装不完整?)" % path, file=sys.stderr)
|
|
46
|
+
return 1
|
|
47
|
+
print("规范版本:%s" % SPEC_VERSION)
|
|
48
|
+
print(path)
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _validate_target(path: str):
|
|
53
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
54
|
+
lines = fh.read().splitlines()
|
|
55
|
+
program = new_program()
|
|
56
|
+
diags = []
|
|
57
|
+
stmts, pdiags = parse_program(lines)
|
|
58
|
+
diags += pdiags
|
|
59
|
+
for stmt in stmts:
|
|
60
|
+
apply_stmt(program, stmt, diags)
|
|
61
|
+
diags += validate(program)
|
|
62
|
+
return diags
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def cmd_validate(args) -> int:
|
|
66
|
+
if not os.path.exists(args.path):
|
|
67
|
+
print("路径不存在:%s" % args.path)
|
|
68
|
+
return 1
|
|
69
|
+
if os.path.isdir(args.path):
|
|
70
|
+
targets = [os.path.join(args.path, name) for name in sorted(os.listdir(args.path))
|
|
71
|
+
if name.endswith(".puppet")]
|
|
72
|
+
if not targets:
|
|
73
|
+
print("目录里没有 .puppet 文件:%s" % args.path)
|
|
74
|
+
return 1
|
|
75
|
+
else:
|
|
76
|
+
targets = [args.path]
|
|
77
|
+
|
|
78
|
+
total_diags = total_errors = 0
|
|
79
|
+
for target in targets:
|
|
80
|
+
diags = _validate_target(target)
|
|
81
|
+
total_diags += len(diags)
|
|
82
|
+
total_errors += sum(1 for d in diags if d.level == "error")
|
|
83
|
+
if not diags:
|
|
84
|
+
print("%s:零诊断" % os.path.basename(target))
|
|
85
|
+
continue
|
|
86
|
+
print("%s:" % os.path.basename(target))
|
|
87
|
+
for diag in diags:
|
|
88
|
+
mark = {"error": "错误", "warning": "警告", "info": "信息"}[diag.level]
|
|
89
|
+
location = ("第 %d 行" % diag.line) if diag.line else "全局"
|
|
90
|
+
print(" %s %s %s: %s" % (location, mark, diag.code, diag.message))
|
|
91
|
+
print("合计 %d 条诊断,其中错误 %d 条" % (total_diags, total_errors))
|
|
92
|
+
if args.strict:
|
|
93
|
+
return 1 if total_diags else 0
|
|
94
|
+
return 1 if total_errors else 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cmd_conformance(args) -> int:
|
|
98
|
+
runner = os.path.join(ROOT, "conformance", "runner.py")
|
|
99
|
+
if not os.path.isfile(runner):
|
|
100
|
+
print("找不到 conformance 运行器:%s(发行包安装不完整?)" % runner,
|
|
101
|
+
file=sys.stderr)
|
|
102
|
+
return 1
|
|
103
|
+
module = "puppet.tk_adapter" if args.tk else "puppet.adapter"
|
|
104
|
+
cmd = [sys.executable, runner, "--impl", '"%s" -m %s' % (sys.executable, module)]
|
|
105
|
+
if args.filter:
|
|
106
|
+
cmd += ["--filter", args.filter]
|
|
107
|
+
return subprocess.call(cmd, cwd=ROOT)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def main(argv=None) -> int:
|
|
111
|
+
ap = argparse.ArgumentParser(prog="puppet", description="Puppet 语言标准工具")
|
|
112
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
113
|
+
|
|
114
|
+
sub.add_parser("spec", help="显示规范版本与位置").set_defaults(func=cmd_spec)
|
|
115
|
+
|
|
116
|
+
p_val = sub.add_parser("validate", help="静态校验 .puppet 文件或目录")
|
|
117
|
+
p_val.add_argument("path", help="文件路径,或包含 .puppet 的目录")
|
|
118
|
+
p_val.add_argument("--strict", action="store_true", help="有任何诊断即视为失败")
|
|
119
|
+
p_val.set_defaults(func=cmd_validate)
|
|
120
|
+
|
|
121
|
+
p_conf = sub.add_parser("conformance", help="运行合规用例集")
|
|
122
|
+
p_conf.add_argument("--filter", default="")
|
|
123
|
+
p_conf.add_argument("--tk", action="store_true",
|
|
124
|
+
help="用 Tk 渲染器适配器跑(真实几何;需要显示器)")
|
|
125
|
+
p_conf.set_defaults(func=cmd_conformance)
|
|
126
|
+
|
|
127
|
+
args = ap.parse_args(argv)
|
|
128
|
+
return args.func(args)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
raise SystemExit(main())
|
puppet/diag.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""诊断:级别、形状、码。
|
|
2
|
+
|
|
3
|
+
码表与 `spec/06-diagnostics.md` 一致;此处只定义形状,码以字符串常量使用。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
ERROR = "error"
|
|
11
|
+
WARNING = "warning"
|
|
12
|
+
INFO = "info"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Diagnostic:
|
|
17
|
+
code: str
|
|
18
|
+
level: str
|
|
19
|
+
message: str
|
|
20
|
+
line: int = 0
|
|
21
|
+
col: int = 0
|
|
22
|
+
suggest: str = ""
|
|
23
|
+
# 仅 DEGRADED_FEATURE 使用:被降级的那个特性(`geometry` / `snapshot` /
|
|
24
|
+
# `control:<类型>` / `attr:<名>` / `animation:<名>` / `icon:<名>`)。
|
|
25
|
+
# conformance 的"声明即 oracle"按它比对,故必须机读。
|
|
26
|
+
feature: str = ""
|
|
27
|
+
|
|
28
|
+
def to_dict(self):
|
|
29
|
+
out = {"code": self.code, "level": self.level, "message": self.message}
|
|
30
|
+
if self.line:
|
|
31
|
+
out["line"] = self.line
|
|
32
|
+
if self.col:
|
|
33
|
+
out["col"] = self.col
|
|
34
|
+
if self.suggest:
|
|
35
|
+
out["suggest"] = self.suggest
|
|
36
|
+
if self.feature:
|
|
37
|
+
out["feature"] = self.feature
|
|
38
|
+
return out
|
|
39
|
+
|
|
40
|
+
def key(self):
|
|
41
|
+
"""用于"本步是否新增"的判据(不含行号:行号随批次变化)。"""
|
|
42
|
+
return (self.code, self.message, self.level)
|