infinity-data 2.2.0__tar.gz → 3.1.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.
Files changed (64) hide show
  1. {infinity_data-2.2.0/src/infinity_data.egg-info → infinity_data-3.1.1}/PKG-INFO +1 -1
  2. {infinity_data-2.2.0 → infinity_data-3.1.1}/pyproject.toml +2 -2
  3. infinity_data-3.1.1/src/infinity_data/emit/__init__.py +35 -0
  4. infinity_data-3.1.1/src/infinity_data/emit/config.py +37 -0
  5. infinity_data-3.1.1/src/infinity_data/emit/converter.py +258 -0
  6. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/infra/path.py +17 -1
  7. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/parser/__init__.py +2 -0
  8. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/parser/diagnostics.py +5 -0
  9. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/parser/models.py +78 -38
  10. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/parser/parser.py +65 -29
  11. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/pipeline.py +13 -6
  12. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/sandbox/mediator.py +14 -0
  13. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/__init__.py +4 -0
  14. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/builder/builder.py +146 -100
  15. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/constraints.py +3 -0
  16. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/diagnostics.py +60 -0
  17. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/executor/executor.py +23 -2
  18. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/registry/__init__.py +37 -0
  19. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/registry/_core.py +31 -5
  20. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/registry/general.py +16 -0
  21. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/registry/logic.py +14 -3
  22. infinity_data-3.1.1/src/infinity_data/semantic/registry/types.py +321 -0
  23. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/resolver/imports.py +1 -1
  24. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/std.py +135 -5
  25. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/tokenizer/diagnostics.py +5 -0
  26. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/tokenizer/finalizer.py +26 -0
  27. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/tokenizer/models/raw_tokens.py +3 -2
  28. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/tokenizer/models/tokens.py +11 -0
  29. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/tokenizer/tokenizer.py +57 -17
  30. {infinity_data-2.2.0 → infinity_data-3.1.1/src/infinity_data.egg-info}/PKG-INFO +1 -1
  31. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data.egg-info/SOURCES.txt +1 -0
  32. infinity_data-2.2.0/src/infinity_data/emit/__init__.py +0 -9
  33. infinity_data-2.2.0/src/infinity_data/emit/converter.py +0 -56
  34. infinity_data-2.2.0/src/infinity_data/semantic/registry/types.py +0 -136
  35. {infinity_data-2.2.0 → infinity_data-3.1.1}/LICENSE +0 -0
  36. {infinity_data-2.2.0 → infinity_data-3.1.1}/README.md +0 -0
  37. {infinity_data-2.2.0 → infinity_data-3.1.1}/setup.cfg +0 -0
  38. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/__init__.py +0 -0
  39. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/frontend.py +0 -0
  40. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/infra/__init__.py +0 -0
  41. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/infra/diagnostics.py +0 -0
  42. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/infra/file.py +0 -0
  43. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/infra/ll1_stream.py +0 -0
  44. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/infra/location.py +0 -0
  45. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/parser/token_stream.py +0 -0
  46. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/py.typed +0 -0
  47. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/sandbox/__init__.py +0 -0
  48. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/sandbox/config.py +0 -0
  49. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/sandbox/errors.py +0 -0
  50. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/sandbox/schema.py +0 -0
  51. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/builder/__init__.py +0 -0
  52. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/builder/models.py +0 -0
  53. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/executor/__init__.py +0 -0
  54. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/jsonpath.py +0 -0
  55. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/registry/dict_constraints.py +0 -0
  56. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/resolver/__init__.py +0 -0
  57. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/resolver/identity.py +0 -0
  58. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/resolver/models.py +0 -0
  59. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/semantic/resolver/resolver.py +0 -0
  60. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/tokenizer/__init__.py +0 -0
  61. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/tokenizer/char_stream.py +0 -0
  62. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data/tokenizer/models/__init__.py +0 -0
  63. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data.egg-info/dependency_links.txt +0 -0
  64. {infinity_data-2.2.0 → infinity_data-3.1.1}/src/infinity_data.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: infinity-data
3
- Version: 2.2.0
3
+ Version: 3.1.1
4
4
  Summary: InfinityData:声明式配置语言(.infd/.inft)的 Python 编译器库
5
5
  Author: infinity_system: yin_bailiang
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "infinity-data"
3
- version = "2.2.0"
3
+ version = "3.1.1"
4
4
  description = "InfinityData:声明式配置语言(.infd/.inft)的 Python 编译器库"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -95,6 +95,6 @@ venvPath = "."
95
95
  venv = ".venv"
96
96
 
97
97
  [tool.interrogate]
98
- fail-under = 70
98
+ fail-under = 60
99
99
  # 测试函数名自解释,docstring 可选;产品代码(src)不豁免
100
100
  ignore-regex = ["^test_"]
@@ -0,0 +1,35 @@
1
+ """产物发射层:Python / std → 输出格式(JSON / YAML / TOML)。
2
+
3
+ 与 semantic 层边界:
4
+ - **内部表示(忠实双向)** → :mod:`infinity_data.semantic.std` 的
5
+ ``python_to_std`` / ``std_to_python``(path → PurePosixPath、float → Decimal、
6
+ noexist → :data:`~infinity_data.semantic.std.NOEXIST` 哨兵)
7
+ - **本层(emit,有损投影)** → :func:`to_json` / :func:`to_yaml` / :func:`to_toml`,
8
+ 按 :class:`EmitConfig` 控制 Decimal 编码与 noexist 标记;:func:`project_output`
9
+ 为纯 Python 值的轻量投影;:func:`restore_python` 反向**尝试还原**加载的原始
10
+ dict 为 :data:`~infinity_data.semantic.std.StdPythonValue`
11
+
12
+ **emit 不应被流水线内部使用**——内部表示一律走 semantic 的忠实转换。
13
+ """
14
+
15
+ from infinity_data.emit.config import EmitConfig
16
+ from infinity_data.emit.converter import (
17
+ dump_to_json,
18
+ load_from_json,
19
+ project_output,
20
+ restore_python,
21
+ to_json,
22
+ to_toml,
23
+ to_yaml,
24
+ )
25
+
26
+ __all__ = [
27
+ 'EmitConfig',
28
+ 'dump_to_json',
29
+ 'load_from_json',
30
+ 'project_output',
31
+ 'restore_python',
32
+ 'to_json',
33
+ 'to_toml',
34
+ 'to_yaml',
35
+ ]
@@ -0,0 +1,37 @@
1
+ """输出转换配置(emit 层):控制 std/python → 输出格式的有损投影语义。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ __all__ = ['EmitConfig']
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class EmitConfig:
12
+ """emit 层输出转换配置。
13
+
14
+ 转换语义:
15
+ - ``full_float``:``float`` 值(:class:`decimal.Decimal`)以自描述标记
16
+ ``{"__type__": "decimal", "num": "<十进制字符串>"}`` 编码(无损);False 时
17
+ 有限值转普通 JSON 数字,NaN / ±Infinity 无法用 JSON 数字表达 → 仍以标记编码
18
+ - ``full_path``:路径值(:class:`PurePosixPath`)以自描述标记
19
+ ``{"__type__": "path", "path": "<POSIX 字符串>"}`` 编码(可辨识「这是路径」);
20
+ False 时投影为普通 POSIX 字符串
21
+ - ``keep_noexist``:``noexist`` 字段保留为 ``{"__type__": "noexist"}`` 标记
22
+ (依赖 :data:`~infinity_data.semantic.std.NOEXIST` 哨兵保留三态);默认丢弃
23
+ (键不出现,§1.6)
24
+
25
+ 序列化选项:
26
+ - ``indent`` / ``sort_keys`` / ``ensure_ascii``:JSON 缩进 / 排序 / ASCII 转义
27
+ """
28
+
29
+ # ── 转换语义 ──────────────────────────────────────
30
+ full_float: bool = False
31
+ full_path: bool = False
32
+ keep_noexist: bool = False
33
+
34
+ # ── 序列化选项 ────────────────────────────────────
35
+ indent: int | None = 2
36
+ sort_keys: bool = False
37
+ ensure_ascii: bool = False
@@ -0,0 +1,258 @@
1
+ """产物发射层:Python / std → 输出格式(JSON / YAML / TOML)。
2
+
3
+ 职责边界(与 semantic 层严格分离):
4
+
5
+ - **内部表示**(忠实双向,语义最小丢失)::func:`~infinity_data.semantic.std.python_to_std`
6
+ 与 :func:`~infinity_data.semantic.std.std_to_python`——path 保持
7
+ :class:`PurePosixPath`、float 保持 :class:`decimal.Decimal`、noexist 经
8
+ :data:`~infinity_data.semantic.std.NOEXIST` 哨兵保留三态。
9
+ 流水线内部(如 :class:`~infinity_data.pipeline.CompilationResult.value`)一律走该层。
10
+ - **本层(emit)**:做**有损投影**供输出格式消费,无需担心完整性——
11
+ path → POSIX 字符串;Decimal 的 NaN / ±Infinity 等特殊编码由本层按
12
+ :class:`EmitConfig` 决定(``full_float`` 标记编码)。
13
+
14
+ 入口:
15
+ - :func:`to_json` / :func:`to_yaml` / :func:`to_toml`:输出文本(接受 std 或 Python 值)
16
+ - :func:`project_output`:纯 Python 值的轻量有损投影(path → 字符串)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import importlib
22
+ import json
23
+ from decimal import Decimal, InvalidOperation
24
+ from pathlib import PurePath, PurePosixPath
25
+ from typing import Any, cast
26
+
27
+ from infinity_data.emit.config import EmitConfig
28
+ from infinity_data.semantic.std import NOEXIST, StdPythonValue, is_std_value, std_to_python
29
+
30
+ __all__ = [
31
+ 'dump_to_json',
32
+ 'load_from_json',
33
+ 'project_output',
34
+ 'restore_python',
35
+ 'to_json',
36
+ 'to_yaml',
37
+ 'to_toml',
38
+ ]
39
+
40
+ # ── 自描述标记(输出格式无法原生表达的值的编码) ──────
41
+
42
+
43
+ def _decimal_marker(dec: Any) -> dict[str, str]:
44
+ """Decimal 自描述标记:``{"__type__": "decimal", "num": "<十进制字符串>"}``。"""
45
+ return {'__type__': 'decimal', 'num': str(dec)}
46
+
47
+
48
+ def _path_marker(s: str) -> dict[str, str]:
49
+ """路径自描述标记:``{"__type__": "path", "path": "<POSIX 字符串>"}``。"""
50
+ return {'__type__': 'path', 'path': s}
51
+
52
+
53
+ def _noexist_marker() -> dict[str, str]:
54
+ """noexist 自描述标记:``{"__type__": "noexist"}``。"""
55
+ return {'__type__': 'noexist'}
56
+
57
+
58
+ # ── 标记还原(反向:加载的原始 dict → StdPythonValue) ────
59
+
60
+
61
+ def _restore_marker(value: dict[Any, Any]) -> Any | None:
62
+ """若 dict 是 emit 自描述标记则还原为 Python 值,否则 None(非标记)。
63
+
64
+ 仅识别 emit 层产出的**精确标记形状**;结构不符(额外键 / 负载非字符串 /
65
+ 数值无法解析)一律视为普通 dict(返回 None,尽力而为)。
66
+ """
67
+ typ = value.get('__type__')
68
+ if typ == 'decimal':
69
+ if set(value) == {'__type__', 'num'} and isinstance(value.get('num'), str):
70
+ try:
71
+ return Decimal(value['num'])
72
+ except (InvalidOperation, ValueError, TypeError):
73
+ return None
74
+ return None
75
+ if typ == 'path':
76
+ if set(value) == {'__type__', 'path'} and isinstance(value.get('path'), str):
77
+ return PurePosixPath(value['path'])
78
+ return None
79
+ if typ == 'noexist':
80
+ if set(value) == {'__type__'}:
81
+ return NOEXIST
82
+ return None
83
+ return None
84
+
85
+
86
+ def restore_python(value: Any) -> StdPythonValue:
87
+ """从加载的原始 dict(含可选 emit 自描述标记)**尝试**还原为 StdPythonValue。
88
+
89
+ - ``{"__type__": "decimal", "num": "..."}`` → :class:`decimal.Decimal`
90
+ - ``{"__type__": "path", "path": "..."}`` → :class:`PurePosixPath`
91
+ - ``{"__type__": "noexist"}`` → :data:`NOEXIST`
92
+ - 普通 ``float`` → :class:`decimal.Decimal`(忠实形式,与
93
+ :func:`~infinity_data.semantic.std.python_to_std` 规范一致)
94
+ - int / bool / str / None 原样;dict / list 递归
95
+ - 识别不了的形状(含疑似标记但结构不符)按普通 dict / list 保留(尽力而为)
96
+
97
+ 配合 :func:`to_json` 等 + ``EmitConfig(full_float=True, full_path=True,
98
+ keep_noexist=True)`` 可实现「std → 输出文本 → 加载 → StdPythonValue」
99
+ 无损闭环(再经 :func:`~infinity_data.semantic.std.python_to_std` 回 std)。
100
+ """
101
+ if isinstance(value, dict):
102
+ obj = cast(dict[Any, Any], value)
103
+ marker = _restore_marker(obj)
104
+ if marker is not None:
105
+ return marker
106
+ return {str(k): restore_python(v) for k, v in obj.items()}
107
+ if isinstance(value, list):
108
+ arr = cast(list[Any], value)
109
+ return [restore_python(v) for v in arr]
110
+ if isinstance(value, bool):
111
+ return value
112
+ if isinstance(value, int):
113
+ return value
114
+ if isinstance(value, float):
115
+ return Decimal(str(value))
116
+ return value
117
+
118
+
119
+ # ── 序列化核心 ──────────────────────────────────────
120
+
121
+
122
+ def _to_serializable(value: Any, config: EmitConfig) -> Any:
123
+ """std 或 Python 值 → 可序列化结构(按 config 做有损投影)。"""
124
+ if is_std_value(value):
125
+ # std 输入:忠实转 Python(noexist 按需保留为 NOEXIST 哨兵),再投影
126
+ py = std_to_python(value, keep_noexist=config.keep_noexist)
127
+ return _serializable(py, config)
128
+ # Python 输入(如 CompilationResult.value):直接投影
129
+ return _serializable(value, config)
130
+
131
+
132
+ def _serializable(value: Any, config: EmitConfig) -> Any:
133
+ """Python 值 → 可序列化结构(递归有损投影)。
134
+
135
+ - :data:`NOEXIST` 哨兵 → ``{"__type__": "noexist"}``
136
+ - :class:`PurePath` → POSIX 字符串
137
+ - :class:`decimal.Decimal` → 按 ``full_float`` 编码(有限值默认 JSON 数字)
138
+ - dict / list 递归;其余(int / bool / str / None 等)原样
139
+ """
140
+ if value is NOEXIST:
141
+ return _noexist_marker()
142
+ if isinstance(value, PurePath):
143
+ s = value.as_posix()
144
+ return _path_marker(s) if config.full_path else s
145
+ if isinstance(value, dict):
146
+ obj = cast(dict[Any, Any], value)
147
+ return {k: _serializable(v, config) for k, v in obj.items()}
148
+ if isinstance(value, list):
149
+ arr = cast(list[Any], value)
150
+ return [_serializable(v, config) for v in arr]
151
+ if isinstance(value, Decimal):
152
+ return _decimal_serializable(value, config)
153
+ return value
154
+
155
+
156
+ def _decimal_serializable(dec: Any, config: EmitConfig) -> Any:
157
+ """Decimal → 可序列化形式。
158
+
159
+ - ``full_float`` → 自描述标记(无损)
160
+ - 否则:有限值 → JSON 数字(整值转 int,避免 ``1.0``;溢出回退标记);
161
+ NaN / ±Infinity 无法用 JSON 数字表达 → 自描述标记
162
+ """
163
+ if config.full_float:
164
+ return _decimal_marker(dec)
165
+ if dec.is_nan() or dec.is_infinite():
166
+ return _decimal_marker(dec)
167
+ try:
168
+ if dec == dec.to_integral_value():
169
+ return int(dec)
170
+ return float(dec)
171
+ except (OverflowError, ValueError):
172
+ return _decimal_marker(dec)
173
+
174
+
175
+ # ── 输出入口 ────────────────────────────────────────
176
+
177
+
178
+ def to_json(value: Any, *, config: EmitConfig | None = None) -> str:
179
+ """std 或 Python 值 → JSON 文本。
180
+
181
+ ``value`` 可为 :class:`~infinity_data.semantic.std.StdValue`(如
182
+ ``CompilationResult.document.root``,支持完整选项)或 Python 值
183
+ (如 ``CompilationResult.value``,noexist 信息已在转换时丢失)。
184
+ """
185
+ cfg = config or EmitConfig()
186
+ data = _to_serializable(value, cfg)
187
+ return json.dumps(data, indent=cfg.indent, sort_keys=cfg.sort_keys, ensure_ascii=cfg.ensure_ascii)
188
+
189
+
190
+ def to_yaml(value: Any, *, config: EmitConfig | None = None) -> str:
191
+ """std 或 Python 值 → YAML 文本(需安装 PyYAML)。"""
192
+ cfg = config or EmitConfig()
193
+ data = _to_serializable(value, cfg)
194
+ try:
195
+ yaml = importlib.import_module('yaml')
196
+ except ImportError as exc: # pragma: no cover - 依赖缺失路径
197
+ raise ImportError('to_yaml 需要 PyYAML(pip install pyyaml)') from exc
198
+ return yaml.safe_dump(data, allow_unicode=not cfg.ensure_ascii, sort_keys=cfg.sort_keys)
199
+
200
+
201
+ def to_toml(value: Any, *, config: EmitConfig | None = None) -> str:
202
+ """std 或 Python 值 → TOML 文本(需安装 tomli-w)。"""
203
+ cfg = config or EmitConfig()
204
+ data = _to_serializable(value, cfg)
205
+ try:
206
+ tomli_w = importlib.import_module('tomli_w')
207
+ except ImportError as exc: # pragma: no cover - 依赖缺失路径
208
+ raise NotImplementedError('to_toml 需要 tomli-w(pip install tomli-w);当前未安装') from exc
209
+ return tomli_w.dumps(data)
210
+
211
+
212
+ # ── 便捷自由函数(默认尝试保留一切语义) ────────────
213
+
214
+
215
+ def dump_to_json(value: Any, *, config: EmitConfig | None = None) -> str:
216
+ """std / Python 值 → JSON 文本(便捷入口,**默认保留一切语义**)。
217
+
218
+ 默认等价 ``EmitConfig(full_float=True, full_path=True, keep_noexist=True)``——
219
+ Decimal / PurePosixPath / noexist 以 ``{"__type__": ...}`` 标记编码;
220
+ 传入 ``config`` 可覆盖(关闭标记、改缩进等,见 :class:`EmitConfig`)。
221
+
222
+ 与 :func:`load_from_json` 配对可实现「std → JSON → StdPythonValue」无损闭环。
223
+ """
224
+ cfg = config or EmitConfig(full_float=True, full_path=True, keep_noexist=True)
225
+ return to_json(value, config=cfg)
226
+
227
+
228
+ def load_from_json(text: str) -> StdPythonValue:
229
+ """JSON 文本 → StdPythonValue(便捷入口,**默认尝试还原全部语义**)。
230
+
231
+ 解析后经 :func:`restore_python`:``{"__type__": ...}`` 标记还原为
232
+ Decimal / PurePosixPath / NOEXIST,普通 float 归一到 Decimal;
233
+ 识别不了的形状原样保留(尽力而为)。
234
+ """
235
+ return restore_python(json.loads(text))
236
+
237
+
238
+ # ── 轻量有损投影(纯 Python 值) ─────────────────────
239
+
240
+
241
+ def project_output(value: Any) -> Any:
242
+ """Python 值 → 输出友好投影(递归,有损,无配置)。
243
+
244
+ - :class:`PurePath`(含 :class:`PurePosixPath`)→ POSIX 字符串
245
+ - dict / list 递归投影;其余(int / Decimal / bool / str / None 等)原样保留
246
+
247
+ 完整选项(Decimal 编码 / noexist 标记)请用 :func:`to_json` 等带
248
+ :class:`EmitConfig` 的入口。
249
+ """
250
+ if isinstance(value, PurePath):
251
+ return value.as_posix()
252
+ if isinstance(value, dict):
253
+ obj = cast(dict[Any, Any], value)
254
+ return {k: project_output(v) for k, v in obj.items()}
255
+ if isinstance(value, list):
256
+ arr = cast(list[Any], value)
257
+ return [project_output(v) for v in arr]
258
+ return value
@@ -14,7 +14,23 @@ from __future__ import annotations
14
14
  import sys
15
15
  from pathlib import Path, PurePosixPath, PureWindowsPath
16
16
 
17
- __all__ = ['posix_to_native', 'native_to_posix']
17
+ __all__ = ['posix_to_native', 'native_to_posix', 'is_valid_posix_path']
18
+
19
+
20
+ def is_valid_posix_path(s: str) -> bool:
21
+ """语言内 POSIX 路径合法性(纯语法校验,无文件系统访问)。
22
+
23
+ - 非空
24
+ - 无 NUL 字符(:class:`PurePosixPath` 拒绝)
25
+ - 可被 :class:`PurePosixPath` 解析
26
+ """
27
+ if not s or '\x00' in s:
28
+ return False
29
+ try:
30
+ PurePosixPath(s)
31
+ return True
32
+ except ValueError:
33
+ return False
18
34
 
19
35
 
20
36
  def posix_to_native(path: str, *, platform: str | None = None) -> Path:
@@ -34,6 +34,7 @@ from infinity_data.parser.models import (
34
34
  JsonPathKey,
35
35
  JsonPathSegment,
36
36
  LiteralValue,
37
+ NamedArg,
37
38
  Statement,
38
39
  TemplateCallValue,
39
40
  TemplateConfig,
@@ -72,6 +73,7 @@ __all__ = [
72
73
  'JsonPathKey',
73
74
  'JsonPathSegment',
74
75
  'LiteralValue',
76
+ 'NamedArg',
75
77
  'Parser',
76
78
  'Statement',
77
79
  'TemplateCallValue',
@@ -63,6 +63,11 @@ register_diagnostic_define(
63
63
  '[{location}] 导入语句必须以换行结尾(同一行逗号后不能再接其他语句)',
64
64
  en='[{location}] import statement must end with a newline (cannot be followed by more tokens on the same line)',
65
65
  ),
66
+ diagnostic_define(
67
+ 'parse.import_path_required',
68
+ '[{location}] 导入路径必须用 p"..." 路径字面量(§1.5 / §3.2),已按字符串容错取用',
69
+ en='[{location}] import path must use a p"..." path literal (§1.5 / §3.2); recovered as string',
70
+ ),
66
71
  diagnostic_define(
67
72
  'parse.value_field',
68
73
  '[{location}] 值位置出现字段定义 {name}(外层数组/对象未闭合)',
@@ -14,6 +14,7 @@ from infinity_data.tokenizer.models.tokens import (
14
14
  IntegerToken,
15
15
  NoexistToken,
16
16
  NullToken,
17
+ PathToken,
17
18
  StringToken,
18
19
  )
19
20
 
@@ -27,6 +28,11 @@ def _canonical_str(s: str) -> str:
27
28
  return json.dumps(s, ensure_ascii=False)
28
29
 
29
30
 
31
+ def _canonical_path(s: str) -> str:
32
+ """路径 → 标准 infd 路径字面量(``p"..."``,§1.5)。"""
33
+ return 'p' + json.dumps(s, ensure_ascii=False)
34
+
35
+
30
36
  def _canonical_constraint_list(constraints: Iterable['Constraint']) -> str:
31
37
  """约束列表 → infd 类型标注:单约束省略尖括号,多约束 ``<a, b>``。"""
32
38
  items = list(constraints)
@@ -37,6 +43,16 @@ def _canonical_constraint_list(constraints: Iterable['Constraint']) -> str:
37
43
  return '<' + ', '.join(c.canonical() for c in items) + '>'
38
44
 
39
45
 
46
+ def _canonical_error(message: str) -> str:
47
+ """错误节点容错序列化:``error("...")`` 占位符。
48
+
49
+ 错误节点是**解析失败恢复**的产物,对应的源码本就无效,无法还原为等价 AST——
50
+ canonical 仅作**容错**(模板真名哈希 / 调试 / LSP 提示不因错误节点崩溃),
51
+ 不承诺 round-trip(``error(...)`` 是显示占位符,不是可解析的模板调用语法)。
52
+ """
53
+ return f'error({_canonical_str(message)})'
54
+
55
+
40
56
  # ═══════════════════════════════════════════════════════════
41
57
  # 节点
42
58
  # ═══════════════════════════════════════════════════════════
@@ -106,7 +122,7 @@ class TemplateImportItem(AstNode):
106
122
 
107
123
  @dataclass
108
124
  class TemplateImportStmt(AstNode):
109
- """模板导入: !from "path" import Name1, Name2 as N2"""
125
+ """模板导入: !from p"path" import Name1, Name2 as N2"""
110
126
 
111
127
  from_path: str # 文件路径(unix 风格)
112
128
  items: list[TemplateImportItem] # 导入项列表
@@ -116,7 +132,7 @@ class TemplateImportStmt(AstNode):
116
132
 
117
133
  def canonical(self) -> str:
118
134
  items = ', '.join(i.canonical() for i in self.items)
119
- return f'!from {_canonical_str(self.from_path)} import {items}'
135
+ return f'!from {_canonical_path(self.from_path)} import {items}'
120
136
 
121
137
 
122
138
  @dataclass
@@ -189,7 +205,7 @@ class FileImportItem(AstNode):
189
205
 
190
206
  @dataclass
191
207
  class FileImportStmt(AstNode):
192
- """配置文件导入: !file "path" as <format> import .path.to.key as alias, ..."""
208
+ """配置文件导入: !file p"path" as <format> import .path.to.key as alias, ..."""
193
209
 
194
210
  file_path: str # 文件路径
195
211
  format: str | None # 文件格式: "yaml", "json", "toml", "raw" 或 None(自动检测后缀)
@@ -201,7 +217,7 @@ class FileImportStmt(AstNode):
201
217
  def canonical(self) -> str:
202
218
  fmt = f' as {self.format}' if self.format else ''
203
219
  items = ', '.join(i.canonical() for i in self.imports)
204
- return f'!file {_canonical_str(self.file_path)}{fmt} import {items}'
220
+ return f'!file {_canonical_path(self.file_path)}{fmt} import {items}'
205
221
 
206
222
 
207
223
  @dataclass
@@ -418,7 +434,7 @@ class Constraints(AstNode):
418
434
  class LiteralValue(AstNode):
419
435
  """字面量值"""
420
436
 
421
- value: FloatToken | IntegerToken | BoolToken | NullToken | NoexistToken | StringToken
437
+ value: FloatToken | IntegerToken | BoolToken | NullToken | NoexistToken | StringToken | PathToken
422
438
 
423
439
  def canonical(self) -> str:
424
440
  return self.value.canonical()
@@ -432,7 +448,7 @@ class DollarValue(AstNode):
432
448
  """
433
449
 
434
450
  name: str # 变量名
435
- type_cast: Literal['int', 'float', 'bool', 'str', None] # 可选类型转换
451
+ type_cast: Literal['int', 'float', 'bool', 'str', 'path', None] # 可选类型转换
436
452
 
437
453
  def canonical(self) -> str:
438
454
  if self.type_cast is None:
@@ -457,24 +473,41 @@ class UnpackValue(AstNode):
457
473
  return ('**' if self.double else '*') + self.value.canonical()
458
474
 
459
475
 
476
+ @dataclass
477
+ class NamedArg(AstNode):
478
+ """模板调用命名参数槽: name = value。
479
+
480
+ 与 ``**expr`` 解包项(:class:`UnpackValue`)在 :attr:`TemplateCallValue.named_args`
481
+ 中按**源码顺序**混排——键冲突「保留先到者」(disjoint merge,§2.7)与书写顺序一致。
482
+ """
483
+
484
+ name: str
485
+ value: Value
486
+
487
+ def children(self) -> Iterable[AstNode]:
488
+ return [self.value]
489
+
490
+ def canonical(self) -> str:
491
+ return f'{self.name} = {self.value.canonical()}'
492
+
493
+
460
494
  @dataclass
461
495
  class DictValue(AstNode):
462
496
  """对象值: { ... }
463
497
 
464
498
  ``: <constraint, ...>`` 结构级约束作用于该字面量 dict 的整体。
465
- ``unpacks``:``**expr`` 解包项(展开为键值对后并入字段集)。
499
+ ``items``:字段(:class:`Field`)与 ``**expr`` 解包项(:class:`UnpackValue`)按
500
+ **源码顺序**混排——键冲突「保留先到者」(disjoint merge,§2.7)与书写顺序一致。
466
501
  """
467
502
 
468
- fields: list[Field]
503
+ items: list[Field | UnpackValue]
469
504
  constraints: list[Constraint] = field(default_factory=lambda: [])
470
- unpacks: list[UnpackValue] = field(default_factory=lambda: [])
471
505
 
472
506
  def children(self) -> Iterable[AstNode]:
473
- return [*self.fields, *self.constraints, *self.unpacks]
507
+ return [*self.items, *self.constraints]
474
508
 
475
509
  def canonical(self) -> str:
476
- inner: list[str] = [f.canonical() for f in self.fields]
477
- inner.extend(u.canonical() for u in self.unpacks)
510
+ inner: list[str] = [i.canonical() for i in self.items]
478
511
  cs = _canonical_constraint_list(self.constraints)
479
512
  if cs:
480
513
  inner.append(': ' + cs)
@@ -501,18 +534,21 @@ class TemplateCallValue(AstNode):
501
534
  展开(§2.8):参数值后缀 ``...`` = 展开轴;调用级(``)`` 后)``...`` = 展开传播
502
535
  (本调用的展开结果作为包围模板调用的轴,整体模式逐元素重复)、``^`` = 笛卡尔积
503
536
  (多轴全组合,首轴最慢变化;与 ``propagate`` 正交,``^...`` 可叠加)。
504
- - ``axis_positional``:位置参数中带 ``...`` 的索引
505
- - ``axis_named``:命名参数中带 ``...`` 的键
506
- - ``axis_unpack_kwargs``:``**expr`` 解包参数中带 ``...`` 的索引
537
+ - ``positional_args``:位置参数槽,**保持源码顺序**;``*expr`` 解包项
538
+ (:class:`UnpackValue`,double=False)也在其中,实例化时展开为多个位置参数(§2.7)
539
+ - ``named_args``:命名参数槽,**保持源码顺序**;显式 ``name = value``(:class:`NamedArg`)
540
+ 与 ``**expr`` 解包项(:class:`UnpackValue`,double=True)混排——键冲突「保留先到者」
541
+ 与书写顺序一致(§2.7 disjoint merge)
542
+ - ``axis_positional``:位置参数槽中带 ``...`` 的索引(``*expr`` 解包项不是轴)
543
+ - ``axis_named``:命名参数(:class:`NamedArg`)中带 ``...`` 的键
544
+ - ``axis_unpack_kwargs``:``**expr`` 解包项(named_args 槽索引)中带 ``...`` 的索引
507
545
  - ``propagate``:调用级 ``...``(展开传播;无 = 展开止于本调用)
508
546
  - ``cartesian``:调用级 ``^``(笛卡尔积展开;无 = zip 配对)
509
547
  """
510
548
 
511
549
  template_name: str
512
- positional_args: list[Value]
513
- named_args: dict[str, Value]
514
- unpack_args: list[UnpackValue] = field(default_factory=lambda: []) # *expr(list → 位置参数)
515
- unpack_kwargs: list[UnpackValue] = field(default_factory=lambda: []) # **expr(dict → 命名参数)
550
+ positional_args: list[Value | UnpackValue]
551
+ named_args: list[NamedArg | UnpackValue]
516
552
  axis_positional: frozenset[int] = field(default_factory=frozenset[int])
517
553
  axis_named: frozenset[str] = field(default_factory=frozenset[str])
518
554
  axis_unpack_kwargs: frozenset[int] = field(default_factory=frozenset[int])
@@ -522,27 +558,22 @@ class TemplateCallValue(AstNode):
522
558
  def children(self) -> Iterable[AstNode]:
523
559
  return [
524
560
  *self.positional_args,
525
- *self.named_args.values(),
526
- *self.unpack_args,
527
- *self.unpack_kwargs,
561
+ *self.named_args,
528
562
  ]
529
563
 
530
564
  def canonical(self) -> str:
531
565
  args: list[str] = []
532
566
  for i, a in enumerate(self.positional_args):
533
567
  s = a.canonical()
534
- if i in self.axis_positional:
535
- s += '...'
536
- args.append(s)
537
- args.extend(u.canonical() for u in self.unpack_args)
538
- for k, v in sorted(self.named_args.items()):
539
- s = f'{k} = {v.canonical()}'
540
- if k in self.axis_named:
568
+ if not isinstance(a, UnpackValue) and i in self.axis_positional:
541
569
  s += '...'
542
570
  args.append(s)
543
- for j, u in enumerate(self.unpack_kwargs):
544
- s = u.canonical()
545
- if j in self.axis_unpack_kwargs:
571
+ for i, n in enumerate(self.named_args):
572
+ s = n.canonical()
573
+ if isinstance(n, UnpackValue):
574
+ if i in self.axis_unpack_kwargs:
575
+ s += '...'
576
+ elif n.name in self.axis_named:
546
577
  s += '...'
547
578
  args.append(s)
548
579
  out = f'{self.template_name}({", ".join(args)})'
@@ -560,32 +591,41 @@ class TemplateCallValue(AstNode):
560
591
 
561
592
  @dataclass
562
593
  class ErrorStatement(AstNode):
563
- """解析失败的语句。用于错误恢复。"""
594
+ """解析失败的语句。用于错误恢复。
595
+
596
+ canonical 容错输出 ``error("message")``(不抛异常),供哈希 / LSP 提示。
597
+ """
564
598
 
565
599
  message: str
566
600
 
567
601
  def canonical(self) -> str:
568
- raise TypeError('错误节点不可序列化为 infd')
602
+ return _canonical_error(self.message)
569
603
 
570
604
 
571
605
  @dataclass
572
606
  class ErrorValue(AstNode):
573
- """解析失败的值。用于错误恢复。"""
607
+ """解析失败的值。用于错误恢复。
608
+
609
+ canonical 容错输出 ``error("message")``(不抛异常),供哈希 / LSP 提示。
610
+ """
574
611
 
575
612
  message: str
576
613
 
577
614
  def canonical(self) -> str:
578
- raise TypeError('错误节点不可序列化为 infd')
615
+ return _canonical_error(self.message)
579
616
 
580
617
 
581
618
  @dataclass
582
619
  class ErrorConstraint(AstNode):
583
- """解析失败的约束。用于错误恢复。"""
620
+ """解析失败的约束。用于错误恢复。
621
+
622
+ canonical 容错输出 ``error("message")``(不抛异常),供哈希 / LSP 提示。
623
+ """
584
624
 
585
625
  message: str
586
626
 
587
627
  def canonical(self) -> str:
588
- raise TypeError('错误节点不可序列化为 infd')
628
+ return _canonical_error(self.message)
589
629
 
590
630
 
591
631
  # ═══════════════════════════════════════════════════════════