infinity-data 2.2.1__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.
- {infinity_data-2.2.1/src/infinity_data.egg-info → infinity_data-3.1.1}/PKG-INFO +1 -1
- {infinity_data-2.2.1 → infinity_data-3.1.1}/pyproject.toml +2 -2
- infinity_data-3.1.1/src/infinity_data/emit/__init__.py +35 -0
- infinity_data-3.1.1/src/infinity_data/emit/config.py +37 -0
- infinity_data-3.1.1/src/infinity_data/emit/converter.py +258 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/infra/path.py +17 -1
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/parser/diagnostics.py +5 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/parser/models.py +12 -6
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/parser/parser.py +42 -10
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/pipeline.py +13 -6
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/sandbox/mediator.py +14 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/__init__.py +4 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/builder/builder.py +24 -1
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/constraints.py +3 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/diagnostics.py +60 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/executor/executor.py +23 -2
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/registry/__init__.py +37 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/registry/_core.py +31 -5
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/registry/general.py +16 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/registry/logic.py +14 -3
- infinity_data-3.1.1/src/infinity_data/semantic/registry/types.py +321 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/resolver/imports.py +1 -1
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/std.py +135 -5
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/tokenizer/diagnostics.py +5 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/tokenizer/finalizer.py +26 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/tokenizer/models/raw_tokens.py +3 -2
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/tokenizer/models/tokens.py +11 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/tokenizer/tokenizer.py +57 -17
- {infinity_data-2.2.1 → infinity_data-3.1.1/src/infinity_data.egg-info}/PKG-INFO +1 -1
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data.egg-info/SOURCES.txt +1 -0
- infinity_data-2.2.1/src/infinity_data/emit/__init__.py +0 -9
- infinity_data-2.2.1/src/infinity_data/emit/converter.py +0 -56
- infinity_data-2.2.1/src/infinity_data/semantic/registry/types.py +0 -136
- {infinity_data-2.2.1 → infinity_data-3.1.1}/LICENSE +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/README.md +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/setup.cfg +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/__init__.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/frontend.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/infra/__init__.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/infra/diagnostics.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/infra/file.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/infra/ll1_stream.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/infra/location.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/parser/__init__.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/parser/token_stream.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/py.typed +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/sandbox/__init__.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/sandbox/config.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/sandbox/errors.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/sandbox/schema.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/builder/__init__.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/builder/models.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/executor/__init__.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/jsonpath.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/registry/dict_constraints.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/resolver/__init__.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/resolver/identity.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/resolver/models.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/semantic/resolver/resolver.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/tokenizer/__init__.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/tokenizer/char_stream.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data/tokenizer/models/__init__.py +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data.egg-info/dependency_links.txt +0 -0
- {infinity_data-2.2.1 → infinity_data-3.1.1}/src/infinity_data.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "infinity-data"
|
|
3
|
-
version = "
|
|
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 =
|
|
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:
|
|
@@ -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)
|
|
@@ -116,7 +122,7 @@ class TemplateImportItem(AstNode):
|
|
|
116
122
|
|
|
117
123
|
@dataclass
|
|
118
124
|
class TemplateImportStmt(AstNode):
|
|
119
|
-
"""模板导入: !from "path" import Name1, Name2 as N2"""
|
|
125
|
+
"""模板导入: !from p"path" import Name1, Name2 as N2"""
|
|
120
126
|
|
|
121
127
|
from_path: str # 文件路径(unix 风格)
|
|
122
128
|
items: list[TemplateImportItem] # 导入项列表
|
|
@@ -126,7 +132,7 @@ class TemplateImportStmt(AstNode):
|
|
|
126
132
|
|
|
127
133
|
def canonical(self) -> str:
|
|
128
134
|
items = ', '.join(i.canonical() for i in self.items)
|
|
129
|
-
return f'!from {
|
|
135
|
+
return f'!from {_canonical_path(self.from_path)} import {items}'
|
|
130
136
|
|
|
131
137
|
|
|
132
138
|
@dataclass
|
|
@@ -199,7 +205,7 @@ class FileImportItem(AstNode):
|
|
|
199
205
|
|
|
200
206
|
@dataclass
|
|
201
207
|
class FileImportStmt(AstNode):
|
|
202
|
-
"""配置文件导入: !file "path" as <format> import .path.to.key as alias, ..."""
|
|
208
|
+
"""配置文件导入: !file p"path" as <format> import .path.to.key as alias, ..."""
|
|
203
209
|
|
|
204
210
|
file_path: str # 文件路径
|
|
205
211
|
format: str | None # 文件格式: "yaml", "json", "toml", "raw" 或 None(自动检测后缀)
|
|
@@ -211,7 +217,7 @@ class FileImportStmt(AstNode):
|
|
|
211
217
|
def canonical(self) -> str:
|
|
212
218
|
fmt = f' as {self.format}' if self.format else ''
|
|
213
219
|
items = ', '.join(i.canonical() for i in self.imports)
|
|
214
|
-
return f'!file {
|
|
220
|
+
return f'!file {_canonical_path(self.file_path)}{fmt} import {items}'
|
|
215
221
|
|
|
216
222
|
|
|
217
223
|
@dataclass
|
|
@@ -428,7 +434,7 @@ class Constraints(AstNode):
|
|
|
428
434
|
class LiteralValue(AstNode):
|
|
429
435
|
"""字面量值"""
|
|
430
436
|
|
|
431
|
-
value: FloatToken | IntegerToken | BoolToken | NullToken | NoexistToken | StringToken
|
|
437
|
+
value: FloatToken | IntegerToken | BoolToken | NullToken | NoexistToken | StringToken | PathToken
|
|
432
438
|
|
|
433
439
|
def canonical(self) -> str:
|
|
434
440
|
return self.value.canonical()
|
|
@@ -442,7 +448,7 @@ class DollarValue(AstNode):
|
|
|
442
448
|
"""
|
|
443
449
|
|
|
444
450
|
name: str # 变量名
|
|
445
|
-
type_cast: Literal['int', 'float', 'bool', 'str', None] # 可选类型转换
|
|
451
|
+
type_cast: Literal['int', 'float', 'bool', 'str', 'path', None] # 可选类型转换
|
|
446
452
|
|
|
447
453
|
def canonical(self) -> str:
|
|
448
454
|
if self.type_cast is None:
|
|
@@ -67,6 +67,7 @@ from infinity_data.tokenizer.models.tokens import (
|
|
|
67
67
|
NewlineToken,
|
|
68
68
|
NoexistToken,
|
|
69
69
|
NullToken,
|
|
70
|
+
PathToken,
|
|
70
71
|
QuestionToken,
|
|
71
72
|
RangleToken,
|
|
72
73
|
RbraceToken,
|
|
@@ -333,12 +334,34 @@ class Parser:
|
|
|
333
334
|
alias=alias,
|
|
334
335
|
)
|
|
335
336
|
|
|
337
|
+
@staticmethod
|
|
338
|
+
def _expect_import_path(stream: TokenStream, collector: DiagnosticCollector) -> PathToken | SinglelineStringToken:
|
|
339
|
+
"""!file / !from 的路径参数:必须用 ``p"..."`` 路径字面量(§1.5 / §3.2)。
|
|
340
|
+
|
|
341
|
+
普通字符串(旧语法)→ ``parse.import_path_required`` 错误,仍容错取用其值继续编译;
|
|
342
|
+
其他 token → 走 :meth:`expect` 的错误恢复(合成 PathToken)。
|
|
343
|
+
"""
|
|
344
|
+
tok = stream.peek()
|
|
345
|
+
if isinstance(tok, PathToken):
|
|
346
|
+
return stream.expect(PathToken)
|
|
347
|
+
if isinstance(tok, SinglelineStringToken):
|
|
348
|
+
collector.add(diag('parse.import_path_required', {}, tok.raw.source))
|
|
349
|
+
return stream.expect(SinglelineStringToken)
|
|
350
|
+
return stream.expect(PathToken)
|
|
351
|
+
|
|
352
|
+
@staticmethod
|
|
353
|
+
def _import_path_str(path_tok: PathToken | SinglelineStringToken) -> str:
|
|
354
|
+
"""导入路径参数 → 语言内 POSIX 字符串(PathToken 取 as_posix)。"""
|
|
355
|
+
if isinstance(path_tok, PathToken):
|
|
356
|
+
return path_tok.value.as_posix()
|
|
357
|
+
return path_tok.value
|
|
358
|
+
|
|
336
359
|
@staticmethod
|
|
337
360
|
def _parse_file_import(
|
|
338
361
|
stream: TokenStream, collector: DiagnosticCollector, kw_tok: FileImportToken
|
|
339
362
|
) -> FileImportStmt:
|
|
340
|
-
"""!file "path" [as <format>] import .path.to.key as alias, ..."""
|
|
341
|
-
path_tok =
|
|
363
|
+
"""!file p"path" [as <format>] import .path.to.key as alias, ..."""
|
|
364
|
+
path_tok = Parser._expect_import_path(stream, collector)
|
|
342
365
|
|
|
343
366
|
# 可选 as <format>
|
|
344
367
|
fmt = None
|
|
@@ -380,7 +403,7 @@ class Parser:
|
|
|
380
403
|
# 注意:不在此消费尾部换行(语句间分隔由 _parse_statement 统一处理)
|
|
381
404
|
return FileImportStmt(
|
|
382
405
|
source=stream.span_from(kw_tok),
|
|
383
|
-
file_path=path_tok
|
|
406
|
+
file_path=Parser._import_path_str(path_tok),
|
|
384
407
|
format=fmt,
|
|
385
408
|
imports=items,
|
|
386
409
|
)
|
|
@@ -521,8 +544,8 @@ class Parser:
|
|
|
521
544
|
def _parse_template_import(
|
|
522
545
|
stream: TokenStream, collector: DiagnosticCollector, kw_tok: FromImportToken
|
|
523
546
|
) -> TemplateImportStmt:
|
|
524
|
-
"""!from "path" import Name1 [as Alias1], Name2, ..."""
|
|
525
|
-
path_tok =
|
|
547
|
+
"""!from p"path" import Name1 [as Alias1], Name2, ..."""
|
|
548
|
+
path_tok = Parser._expect_import_path(stream, collector)
|
|
526
549
|
Parser._expect_keyword(stream, collector, 'import')
|
|
527
550
|
|
|
528
551
|
# 导入项列表(项之间必须用逗号分隔)
|
|
@@ -545,7 +568,7 @@ class Parser:
|
|
|
545
568
|
# 注意:不在此消费尾部换行(语句间分隔由 _parse_statement 统一处理)
|
|
546
569
|
return TemplateImportStmt(
|
|
547
570
|
source=stream.span_from(kw_tok),
|
|
548
|
-
from_path=path_tok
|
|
571
|
+
from_path=Parser._import_path_str(path_tok),
|
|
549
572
|
items=items,
|
|
550
573
|
)
|
|
551
574
|
|
|
@@ -842,6 +865,7 @@ class Parser:
|
|
|
842
865
|
BoolToken,
|
|
843
866
|
NullToken,
|
|
844
867
|
NoexistToken,
|
|
868
|
+
PathToken,
|
|
845
869
|
LbraceToken,
|
|
846
870
|
LbracketToken,
|
|
847
871
|
IdentifierToken,
|
|
@@ -856,7 +880,7 @@ class Parser:
|
|
|
856
880
|
return False
|
|
857
881
|
return isinstance(
|
|
858
882
|
tok,
|
|
859
|
-
(IdentifierToken, QuestionToken, StringToken, IntegerToken, FloatToken, BoolToken, NullToken),
|
|
883
|
+
(IdentifierToken, QuestionToken, StringToken, IntegerToken, FloatToken, BoolToken, NullToken, PathToken),
|
|
860
884
|
)
|
|
861
885
|
|
|
862
886
|
# ═══════════════════════════════════════════════════════
|
|
@@ -1039,7 +1063,7 @@ class Parser:
|
|
|
1039
1063
|
stream.advance()
|
|
1040
1064
|
return ConstraintIdent(source=tok.raw.source, name='?')
|
|
1041
1065
|
|
|
1042
|
-
case StringToken() | IntegerToken() | FloatToken() | BoolToken() | NullToken():
|
|
1066
|
+
case StringToken() | IntegerToken() | FloatToken() | BoolToken() | NullToken() | PathToken():
|
|
1043
1067
|
base = Parser._parse_constraint_literal(stream)
|
|
1044
1068
|
|
|
1045
1069
|
case _:
|
|
@@ -1122,7 +1146,15 @@ class Parser:
|
|
|
1122
1146
|
return Parser._parse_dollar_value(stream, collector)
|
|
1123
1147
|
|
|
1124
1148
|
# ── 字面量(FloatToken 覆盖所有浮点值,含 NaN / ±Inf)──
|
|
1125
|
-
case
|
|
1149
|
+
case (
|
|
1150
|
+
StringToken()
|
|
1151
|
+
| IntegerToken()
|
|
1152
|
+
| FloatToken()
|
|
1153
|
+
| BoolToken()
|
|
1154
|
+
| NullToken()
|
|
1155
|
+
| NoexistToken()
|
|
1156
|
+
| PathToken() as tok
|
|
1157
|
+
):
|
|
1126
1158
|
stream.advance()
|
|
1127
1159
|
return Parser._wrap_literal(tok)
|
|
1128
1160
|
|
|
@@ -1176,7 +1208,7 @@ class Parser:
|
|
|
1176
1208
|
cast_tok = stream.peek()
|
|
1177
1209
|
if isinstance(cast_tok, IdentifierToken):
|
|
1178
1210
|
name = cast_tok.name
|
|
1179
|
-
if name in ('int', 'float', 'bool', 'str'):
|
|
1211
|
+
if name in ('int', 'float', 'bool', 'str', 'path'):
|
|
1180
1212
|
type_cast = name
|
|
1181
1213
|
else:
|
|
1182
1214
|
collector.add(diag('parse.invalid_cast', {'type': name}, cast_tok.raw.source))
|
|
@@ -18,9 +18,8 @@ from collections.abc import Mapping
|
|
|
18
18
|
from dataclasses import dataclass, field, replace
|
|
19
19
|
from functools import cached_property
|
|
20
20
|
from pathlib import Path
|
|
21
|
-
from typing import Any
|
|
21
|
+
from typing import Any, cast
|
|
22
22
|
|
|
23
|
-
from infinity_data.emit import reduce_object
|
|
24
23
|
from infinity_data.frontend import parse_source
|
|
25
24
|
from infinity_data.infra.diagnostics import Diagnostic, DiagnosticCollector, Severity
|
|
26
25
|
from infinity_data.infra.file import DiskFile, File, MemFile
|
|
@@ -29,6 +28,7 @@ from infinity_data.semantic.builder import AstBuilder, StdDocument
|
|
|
29
28
|
from infinity_data.semantic.executor import ConstraintExecutor
|
|
30
29
|
from infinity_data.semantic.registry import ConstraintRegistry
|
|
31
30
|
from infinity_data.semantic.resolver import ImportResolver, TemplateGraphResolver
|
|
31
|
+
from infinity_data.semantic.std import std_to_python
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
@dataclass(frozen=True)
|
|
@@ -60,8 +60,10 @@ class CompilationResult:
|
|
|
60
60
|
"""一次编译的完整产物(根产物 + 诊断)。
|
|
61
61
|
|
|
62
62
|
- ``document``::class:`StdDocument`(纯数据:root / templates / scope;诊断见 ``diagnostics``)
|
|
63
|
-
- ``root`` / ``value``:由 ``document``
|
|
64
|
-
|
|
63
|
+
- ``root`` / ``value``:由 ``document`` 派生(惰性)——value 经
|
|
64
|
+
:func:`~infinity_data.semantic.std.std_to_python` **忠实**转换
|
|
65
|
+
(path → :class:`PurePosixPath`、float → :class:`decimal.Decimal`,语义最小丢失);
|
|
66
|
+
有损输出投影(path → 字符串等)属 emit 层职责,不在本层发生
|
|
65
67
|
"""
|
|
66
68
|
|
|
67
69
|
document: StdDocument
|
|
@@ -69,8 +71,12 @@ class CompilationResult:
|
|
|
69
71
|
|
|
70
72
|
@cached_property
|
|
71
73
|
def value(self) -> dict[str, Any]:
|
|
72
|
-
"""
|
|
73
|
-
|
|
74
|
+
"""忠实 std → Python(path 保持 PurePosixPath;惰性计算)。
|
|
75
|
+
|
|
76
|
+
``noexist`` 显式丢弃(``keep_noexist=False``):转换层本身默认无损(§1.6
|
|
77
|
+
三态经 NOEXIST 哨兵保留),而这里是**输出投影**,兑现「noexist 键不出现」。
|
|
78
|
+
"""
|
|
79
|
+
return cast(dict[str, Any], std_to_python(self.document.root, keep_noexist=False))
|
|
74
80
|
|
|
75
81
|
@property
|
|
76
82
|
def has_errors(self) -> bool:
|
|
@@ -129,6 +135,7 @@ def _compile(file: File, options: CompileOptions) -> CompilationResult:
|
|
|
129
135
|
registry=resolver.registry,
|
|
130
136
|
templates=std.templates,
|
|
131
137
|
template_scopes=context.template_scopes,
|
|
138
|
+
sandbox=sandbox_impl,
|
|
132
139
|
)
|
|
133
140
|
executor.validate(std.root, collector)
|
|
134
141
|
if resolver.schema is not None:
|
|
@@ -193,6 +193,20 @@ class Sandbox:
|
|
|
193
193
|
"""!from 目标:路径解析 + allow_templates 授权 → File。"""
|
|
194
194
|
return self._open(from_path, self._config.allow_templates, '模板导入', base_dir, source)
|
|
195
195
|
|
|
196
|
+
def probe_path(self, from_path: str, *, base_dir: Path | None = None) -> Path | None:
|
|
197
|
+
"""构建期路径探测:路径解析 + allow_files 授权 → 原生 Path;未授权 → None。
|
|
198
|
+
|
|
199
|
+
供文件系统约束(``exist`` / ``dir`` / ``file`` / ``link``)使用——与导入同属
|
|
200
|
+
当前机器文件系统状态;相对路径以 ``base_dir``(默认编译入口目录)解析。
|
|
201
|
+
"""
|
|
202
|
+
base = base_dir if base_dir is not None else self._base_dir
|
|
203
|
+
path = posix_to_native(from_path) # 语言内 POSIX → 当前平台原生(/c/... → C:\...)
|
|
204
|
+
if not path.is_absolute():
|
|
205
|
+
path = base / path
|
|
206
|
+
if not match_globs(path, self._config.allow_files, self._base_dir):
|
|
207
|
+
return None
|
|
208
|
+
return path
|
|
209
|
+
|
|
196
210
|
def _open(
|
|
197
211
|
self,
|
|
198
212
|
from_path: str,
|