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.
Files changed (40) hide show
  1. openpuppet_language-2.0.data/data/share/puppet/conformance/README.md +274 -0
  2. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/binding.json +238 -0
  3. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/capabilities.json +155 -0
  4. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/grammar.json +248 -0
  5. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/interaction.json +220 -0
  6. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/listen.json +133 -0
  7. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/platform.json +274 -0
  8. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/probes.json +117 -0
  9. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/render.json +180 -0
  10. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/tabs.json +39 -0
  11. openpuppet_language-2.0.data/data/share/puppet/conformance/cases/templates.json +143 -0
  12. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_bad_result.py +11 -0
  13. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_basic.py +24 -0
  14. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_deps_mismatch.py +11 -0
  15. openpuppet_language-2.0.data/data/share/puppet/conformance/fixtures/capabilities_no_doc.py +10 -0
  16. openpuppet_language-2.0.data/data/share/puppet/conformance/runner.py +721 -0
  17. openpuppet_language-2.0.data/data/share/puppet/spec/01-grammar.md +283 -0
  18. openpuppet_language-2.0.data/data/share/puppet/spec/02-ir.md +77 -0
  19. openpuppet_language-2.0.data/data/share/puppet/spec/03-semantics.md +167 -0
  20. openpuppet_language-2.0.data/data/share/puppet/spec/04-vocabulary.md +227 -0
  21. openpuppet_language-2.0.data/data/share/puppet/spec/05-render-contract.md +126 -0
  22. openpuppet_language-2.0.data/data/share/puppet/spec/06-diagnostics.md +151 -0
  23. openpuppet_language-2.0.data/data/share/puppet/spec/README.md +73 -0
  24. openpuppet_language-2.0.dist-info/METADATA +50 -0
  25. openpuppet_language-2.0.dist-info/RECORD +40 -0
  26. openpuppet_language-2.0.dist-info/WHEEL +5 -0
  27. openpuppet_language-2.0.dist-info/entry_points.txt +2 -0
  28. openpuppet_language-2.0.dist-info/top_level.txt +1 -0
  29. puppet/__init__.py +19 -0
  30. puppet/adapter.py +112 -0
  31. puppet/capabilities.py +172 -0
  32. puppet/cli.py +132 -0
  33. puppet/diag.py +42 -0
  34. puppet/engine.py +1038 -0
  35. puppet/eval.py +243 -0
  36. puppet/ir.py +538 -0
  37. puppet/lang.py +755 -0
  38. puppet/raster_adapter.py +348 -0
  39. puppet/tk_adapter.py +374 -0
  40. puppet/vocab.py +147 -0
@@ -0,0 +1,348 @@
1
+ """conformance 适配器(软件光栅渲染器):真实几何 + 真实 PNG 截图 + 命中测试交互。
2
+
3
+ 与 `tk_adapter` 同一协议,但**不需要显示器、不依赖任何第三方库**:
4
+
5
+ - **布局**:自带堆叠布局(col/row 主轴、`gap` 相邻间隔、`flex` 分配剩余空间、`w/h` 固定
6
+ 尺寸、`x/y` 脱离流动、`visible=false` 不占位)——完全确定性,故可与 Tk **交叉验证**几何。
7
+ - **截图**:纯标准库(`zlib` + `struct`)编码真 PNG,不是"假装成功"的空图。
8
+ - **交互**:**命中测试**(hit test)——与 Tk 的事件注入是两条不同路径,互为交叉验证。
9
+
10
+ 因此 CI 的语言层作业可以直接跑它,不需要 `xvfb`。
11
+ 文本不做字形栅格化(按"内容带"绘制),故像素级外观不在其能力声明内——这是能力声明的
12
+ 诚实表达,不是缺陷。
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import json
19
+ import struct
20
+ import sys
21
+ import zlib
22
+ from typing import Dict
23
+
24
+ from . import adapter as base
25
+ from .diag import ERROR, INFO, Diagnostic
26
+ from .engine import Engine
27
+ from .lang import Lit
28
+
29
+ RENDERING = {
30
+ "controls": ["window", "col", "row", "template", "text", "button", "input",
31
+ "checkbox", "divider", "spacer", "icon", "progress"],
32
+ "attributes": ["gap", "w", "h", "x", "y", "bgcolor", "fg", "text",
33
+ "title", "flex"],
34
+ "animations": [],
35
+ "icons": [],
36
+ "geometry": True,
37
+ "snapshot": True,
38
+ "interaction": True,
39
+ "headless": True,
40
+ "notes": "软件光栅参考渲染器:确定性堆叠布局 + 纯标准库 PNG 截图 + 命中测试交互;"
41
+ "无需显示器。文本按内容带绘制(无字形栅格化),故像素级外观不在其能力内。",
42
+ }
43
+
44
+ _CONTAINERS = ("window", "col", "row")
45
+ _GAP_DEFAULT = 8
46
+ # 未声明 w/h 时的自然尺寸(确定性,不依赖字体度量——故跨实现可比)
47
+ _NATURAL = {
48
+ "text": (80, 24), "button": (80, 24), "input": (120, 24), "checkbox": (100, 24),
49
+ "divider": (100, 1), "spacer": (8, 8), "icon": (16, 16), "progress": (100, 8),
50
+ "image": (80, 60), "avatar": (32, 32), "navbar": (100, 40), "slider": (100, 24),
51
+ "dropdown": (120, 24), "switch": (60, 24), "tabs": (100, 60), "list": (100, 60),
52
+ }
53
+ _NATURAL_DEFAULT = (80, 24)
54
+ _WINDOW_DEFAULT = (400, 300)
55
+ _BG = {
56
+ "window": (255, 255, 255), "col": (248, 250, 252), "row": (248, 250, 252),
57
+ "text": (30, 41, 59), "button": (37, 99, 235), "input": (255, 255, 255),
58
+ "checkbox": (255, 255, 255), "divider": (226, 232, 240), "spacer": (241, 245, 249),
59
+ "icon": (100, 116, 139), "progress": (191, 219, 254),
60
+ }
61
+ _BG_DEFAULT = (226, 232, 240)
62
+ _TEXTUAL = ("text", "button", "input", "checkbox")
63
+
64
+
65
+ def _rgb(color: str, fallback):
66
+ """`#rrggbb` → (r, g, b);不是颜色就退回默认(渲染自由,不算违反)。"""
67
+ if not isinstance(color, str) or not color.startswith("#"):
68
+ return fallback
69
+ try:
70
+ return (int(color[1:3], 16), int(color[3:5], 16), int(color[5:7], 16))
71
+ except (ValueError, IndexError):
72
+ return fallback
73
+
74
+
75
+ def _png(width: int, height: int, pixels: bytearray) -> bytes:
76
+ """把 RGB 像素缓冲编码为 PNG(8 位真彩色,无滤波)。仅用标准库。"""
77
+ stride = width * 3
78
+ raw = bytearray()
79
+ for row in range(height):
80
+ raw.append(0) # filter type 0 (None)
81
+ raw += pixels[row * stride:(row + 1) * stride]
82
+
83
+ def chunk(tag: bytes, data: bytes) -> bytes:
84
+ return (struct.pack(">I", len(data)) + tag + data
85
+ + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
86
+
87
+ header = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
88
+ return (b"\x89PNG\r\n\x1a\n"
89
+ + chunk(b"IHDR", header)
90
+ + chunk(b"IDAT", zlib.compress(bytes(raw), 9))
91
+ + chunk(b"IEND", b""))
92
+
93
+
94
+ class RasterRenderer:
95
+ """把程序 IR 布局成矩形并绘制成 PNG;交互走命中测试。"""
96
+
97
+ def __init__(self, engine: Engine):
98
+ self.engine = engine
99
+ self.rects: Dict[str, dict] = {}
100
+ self.size = _WINDOW_DEFAULT
101
+ self.state: Dict[str, object] = {}
102
+ self.values: Dict[str, object] = {}
103
+ # 观测替身(load.renderGeometry):一旦提供即为真值,优先于自算布局
104
+ self.double = None
105
+ self.declares_no_geometry = False
106
+
107
+ # ------------------------------------------------------------ 布局
108
+
109
+ def relayout(self) -> None:
110
+ self.rects = {}
111
+ self.values = self.engine._observe_attrs()
112
+ self.state = {}
113
+ program = self.engine.program
114
+ root = program.nodes.get("root")
115
+ if root is None:
116
+ return
117
+ for wid in root.children:
118
+ node = program.nodes.get(wid)
119
+ if node is not None and node.type == "window":
120
+ width, height = self._natural(node, _WINDOW_DEFAULT)
121
+ self.size = (int(width), int(height))
122
+ self.rects[node.id] = {"x": 0.0, "y": 0.0,
123
+ "width": float(self.size[0]),
124
+ "height": float(self.size[1])}
125
+ self._layout_container(node, 0.0, 0.0, float(self.size[0]),
126
+ float(self.size[1]), vertical=True)
127
+ break
128
+
129
+ def _static(self, node, name):
130
+ expr = node.attrs.get(name)
131
+ return expr.value if isinstance(expr, Lit) else None
132
+
133
+ def _natural(self, node, fallback):
134
+ width, height = self._static(node, "w"), self._static(node, "h")
135
+ nat = _NATURAL.get(node.type, _NATURAL_DEFAULT)
136
+ return (width if isinstance(width, (int, float)) else nat[0],
137
+ height if isinstance(height, (int, float)) else nat[1])
138
+
139
+ def _layout_container(self, node, x, y, width, height, vertical) -> None:
140
+ gap = self._static(node, "gap")
141
+ gap = float(gap) if isinstance(gap, (int, float)) else float(_GAP_DEFAULT)
142
+ program = self.engine.program
143
+ flow, absolute = [], []
144
+ for cid in node.children:
145
+ child = program.nodes.get(cid)
146
+ if child is None or child.type == "template":
147
+ continue
148
+ if not self.engine.flag(child.id, "visible"):
149
+ continue # visible=false 不占位(规范 05 第 2 节)
150
+ cx, cy = self._static(child, "x"), self._static(child, "y")
151
+ if isinstance(cx, (int, float)) and isinstance(cy, (int, float)):
152
+ absolute.append(child) # 绝对定位:脱离流动
153
+ else:
154
+ flow.append(child)
155
+
156
+ sizes = [self._natural(child, _NATURAL_DEFAULT) for child in flow]
157
+ count = len(flow)
158
+ if count:
159
+ main_total = float(height if vertical else width)
160
+ used = sum(s[1] if vertical else s[0] for s in sizes)
161
+ used += gap * (count - 1)
162
+ flex_total = 0.0
163
+ for child in flow:
164
+ flex = self._static(child, "flex")
165
+ if isinstance(flex, (int, float)) and flex > 0:
166
+ flex_total += float(flex)
167
+ extra = max(main_total - used, 0.0)
168
+ if flex_total > 0: # flex 按比例分配剩余空间
169
+ for idx, child in enumerate(flow):
170
+ flex = self._static(child, "flex")
171
+ if isinstance(flex, (int, float)) and flex > 0:
172
+ share = extra * (float(flex) / flex_total)
173
+ if vertical:
174
+ sizes[idx] = (sizes[idx][0], sizes[idx][1] + share)
175
+ else:
176
+ sizes[idx] = (sizes[idx][0] + share, sizes[idx][1])
177
+ cursor = 0.0
178
+ for idx, child in enumerate(flow):
179
+ cw, ch = float(sizes[idx][0]), float(sizes[idx][1])
180
+ if vertical:
181
+ box = (x, y + cursor, width, ch)
182
+ else:
183
+ box = (x + cursor, y, cw, height)
184
+ self._place(child, *box)
185
+ cursor += (ch if vertical else cw) + gap
186
+ for child in absolute:
187
+ cw, ch = self._natural(child, _NATURAL_DEFAULT)
188
+ self._place(child, x + float(self._static(child, "x")),
189
+ y + float(self._static(child, "y")), float(cw), float(ch))
190
+
191
+ def _place(self, node, x, y, width, height) -> None:
192
+ self.rects[node.id] = {"x": x, "y": y, "width": width, "height": height}
193
+ if node.type in _CONTAINERS:
194
+ self._layout_container(node, x, y, width, height,
195
+ vertical=node.type != "row")
196
+ elif node.type in ("input", "checkbox"):
197
+ key = "#" + node.id
198
+ self.state[node.id] = (self.values.get(key + ".value")
199
+ if self.values.get(key + ".value") is not None
200
+ else ("" if node.type == "input" else False))
201
+
202
+ def geometry(self) -> dict:
203
+ return {"#" + nid: dict(rect) for nid, rect in self.rects.items()}
204
+
205
+ # ------------------------------------------------------------ 绘制
206
+
207
+ def _fill(self, pixels, width, height, x, y, w, h, color) -> None:
208
+ x0, y0 = max(int(x), 0), max(int(y), 0)
209
+ x1, y1 = min(int(x + w), width), min(int(y + h), height)
210
+ if x1 <= x0 or y1 <= y0:
211
+ return
212
+ row = bytes(color) * (x1 - x0)
213
+ for yy in range(y0, y1):
214
+ start = (yy * width + x0) * 3
215
+ pixels[start:start + len(row)] = row
216
+
217
+ def snapshot(self) -> dict:
218
+ width, height = self.size
219
+ pixels = bytearray(width * height * 3)
220
+ self._fill(pixels, width, height, 0, 0, width, height, (255, 255, 255))
221
+ program = self.engine.program
222
+ for nid, rect in self.rects.items():
223
+ node = program.nodes.get(nid)
224
+ if node is None:
225
+ continue
226
+ color = _rgb(self.values.get("#" + nid + ".bgcolor")
227
+ or self._static(node, "bgcolor"), _BG.get(node.type, _BG_DEFAULT))
228
+ self._fill(pixels, width, height, rect["x"], rect["y"],
229
+ rect["width"], rect["height"], color)
230
+ if node.type in _TEXTUAL:
231
+ # 文本控件画一条"内容带":足以让图像反映布局,又不需要字形栅格化
232
+ inner = tuple(min(255, int(c * 0.35 + 255 * 0.65)) for c in color)
233
+ self._fill(pixels, width, height, rect["x"] + 4, rect["y"] + 4,
234
+ max(rect["width"] - 8, 0), max(rect["height"] - 8, 0), inner)
235
+ png = _png(width, height, pixels)
236
+ return {"image": base64.b64encode(png).decode("ascii"),
237
+ "format": "png", "width": width, "height": height}
238
+
239
+ # ------------------------------------------------------------ 交互(命中测试)
240
+
241
+ def _hit(self, x, y):
242
+ """命中测试:返回包含该点的最深层节点(后画的小节点优先)。"""
243
+ best = None
244
+ for nid, rect in self.rects.items():
245
+ if (rect["x"] <= x < rect["x"] + rect["width"]
246
+ and rect["y"] <= y < rect["y"] + rect["height"]):
247
+ if best is None or rect["width"] * rect["height"] <= \
248
+ self.rects[best]["width"] * self.rects[best]["height"]:
249
+ best = nid
250
+ return best
251
+
252
+ def deliver(self, target, action, value=None) -> dict:
253
+ nid = (target or "").lstrip("#")
254
+ rect = self.rects.get(nid)
255
+ if rect is None or nid not in self.engine.program.nodes:
256
+ return {"delivered": False, "diagnostics": [Diagnostic(
257
+ "TARGET_MISSING", ERROR,
258
+ "节点 #%s 不存在或不可见" % nid).to_dict()]}
259
+ node = self.engine.program.nodes[nid]
260
+ # 落点取目标中心,并确认命中的是它自己或它的后代(真实 UI 的命中规则)
261
+ hit = self._hit(rect["x"] + rect["width"] / 2.0,
262
+ rect["y"] + rect["height"] / 2.0)
263
+ if hit is None:
264
+ return {"delivered": False, "diagnostics": [Diagnostic(
265
+ "DEGRADED_FEATURE", INFO, "该位置没有可命中的部件", feature="interaction"
266
+ ).to_dict()]}
267
+ target_id = hit
268
+ tnode = self.engine.program.nodes[target_id]
269
+ event, new_value = action, value
270
+ if tnode.type == "button" and action == "click":
271
+ event = "click"
272
+ elif tnode.type == "checkbox":
273
+ if action == "click":
274
+ new_value = not bool(self.state.get(target_id))
275
+ event = "change"
276
+ elif tnode.type == "input":
277
+ if action == "click":
278
+ event = "focus"
279
+ elif action == "submit":
280
+ event = "submit"
281
+ else:
282
+ event = "change"
283
+ elif action == "click":
284
+ event = "click"
285
+ if tnode.type in ("input", "checkbox"):
286
+ self.state[target_id] = new_value
287
+ self.engine.queue(self.engine.fire(target_id, event, None, new_value))
288
+ return {"delivered": True, "hit": "#" + target_id,
289
+ "event": event, "value": new_value}
290
+
291
+
292
+ def handle(engine: Engine, renderer: RasterRenderer, request: dict) -> dict:
293
+ op = request.get("op")
294
+ if op == "hello":
295
+ return {"protocol": "1", "rendering": RENDERING}
296
+ if op == "interact":
297
+ return renderer.deliver(request.get("target", ""),
298
+ request.get("action", ""), request.get("value"))
299
+ if op == "snapshot":
300
+ return renderer.snapshot()
301
+ return base.handle(engine, request)
302
+
303
+
304
+ def main() -> int:
305
+ try:
306
+ sys.stdin.reconfigure(encoding="utf-8", errors="replace")
307
+ sys.stdout.reconfigure(encoding="ascii", errors="backslashreplace")
308
+ except Exception: # noqa: BLE001 - 老环境没有 reconfigure 也不致命
309
+ pass
310
+ engine = Engine(rendering=RENDERING)
311
+ renderer = RasterRenderer(engine)
312
+ for line in sys.stdin:
313
+ line = line.strip()
314
+ if not line:
315
+ continue
316
+ try:
317
+ request = json.loads(line)
318
+ except json.JSONDecodeError as ex:
319
+ base._emit({"error": "请求不是合法 JSON:%s" % ex})
320
+ continue
321
+ if request.get("op") == "load":
322
+ # 每次装载都是新用例:先回到渲染器自身的声明,再让声明替身覆盖
323
+ renderer.declares_no_geometry = False
324
+ renderer.double = request.get("renderGeometry")
325
+ if isinstance(request.get("rendering"), dict):
326
+ declared = request["rendering"].get("geometry")
327
+ if declared is not None:
328
+ renderer.declares_no_geometry = not bool(declared)
329
+ renderer.relayout()
330
+ # 几何来源优先级:观测替身 > 声明替身(geometry:false 即无) > 自算布局
331
+ if renderer.double is not None:
332
+ engine.render_geometry = renderer.double
333
+ elif renderer.declares_no_geometry:
334
+ engine.render_geometry = {}
335
+ else:
336
+ engine.render_geometry = renderer.geometry()
337
+ try:
338
+ response = handle(engine, renderer, request)
339
+ except Exception as ex: # noqa: BLE001 - 适配器绝不静默:错误原样回传
340
+ response = {"error": "%s: %s" % (type(ex).__name__, ex)}
341
+ base._emit(response)
342
+ if request.get("op") == "quit":
343
+ break
344
+ return 0
345
+
346
+
347
+ if __name__ == "__main__":
348
+ raise SystemExit(main())