Coverage for src\genesis\parsers\resolver.py: 100%
51 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-26 14:20 +0800
1"""URI resolver 与强验证(T12,OV3)。
3背景:design.md §9.2 定义 Citation URI 格式 `file.xlsx#SheetName!ColumnRow`,
4但仅 `build_source_uri` 存在,无解析、无存在性验证。OV3 裁定将其机制化:
5 - parse_source_uri:把 URI 解析为结构化 SourceRef(格式不一致即报错)
6 - provenance_to_uri:从 Provenance 还原 URI(与 build 互为逆)
7 - resolve_source_uri:在 StructuredSource 内定位真实单元格(存在性校验)
8 - validate_source_uris:批量强验证,区分 resolved/unresolved(防 QA#8 作弊——
9 编造的 URI 无法在源中定位,必落入 unresolved)
10"""
12from __future__ import annotations
14from dataclasses import dataclass
16from genesis.data_models import CellValue, Provenance, StructuredSource
17from genesis.parsers.provenance import build_source_uri
20class URIError(ValueError):
21 """URI 格式非法(不符合 file.xlsx#SheetName!CellRef)。"""
24@dataclass(frozen=True)
25class SourceRef:
26 """URI 解析后的结构化定位。"""
28 file_name: str
29 sheet_name: str
30 cell_ref: str
33def parse_source_uri(uri: str) -> SourceRef:
34 """解析 `file.xlsx#SheetName!C3` → SourceRef。
36 Raises:
37 URIError: 缺 `#` / 缺 `!` / 任一分段为空。
38 """
39 if not isinstance(uri, str) or "#" not in uri or "!" not in uri:
40 raise URIError(f"URI 格式非法(期望 file.xlsx#SheetName!CellRef): {uri!r}")
41 file_part, rest = uri.split("#", 1)
42 if not file_part or "!" not in rest:
43 raise URIError(f"URI 格式非法(期望 file.xlsx#SheetName!CellRef): {uri!r}")
44 sheet_name, cell_ref = rest.split("!", 1)
45 if not sheet_name or not cell_ref:
46 raise URIError(f"URI 格式非法(Sheet/Cell 段不可为空): {uri!r}")
47 return SourceRef(file_name=file_part, sheet_name=sheet_name, cell_ref=cell_ref)
50def provenance_to_uri(prov: Provenance) -> str:
51 """从 Provenance 还原 URI(与 build_source_uri 互逆)。"""
52 cell_ref = f"{prov.column}{prov.row}"
53 return build_source_uri(prov.file_name, prov.sheet_name, cell_ref)
56def resolve_source_uri(uri: str, source: StructuredSource) -> CellValue | None:
57 """在 StructuredSource 中定位 URI 指向的真实单元格;不存在返回 None。"""
58 ref = parse_source_uri(uri)
59 for table in source.tables:
60 for row in table.rows:
61 for cell in row.values():
62 if _matches(cell, ref):
63 return cell
64 return None
67def validate_source_uris(uris: list[str], source: StructuredSource) -> "ValidationResult":
68 """批量强验证:把 URI 分为可在源中定位(resolved)与不可定位(unresolved)。
70 格式错误或源中不存在的 URI 一律归入 unresolved —— 供 QA 校验断言
71 「所有引用的 URI 必须存在于输入中」(design.md §6.8 第五步,T12 落地)。
72 """
73 resolved: list[str] = []
74 unresolved: list[str] = []
75 for uri in uris:
76 try:
77 if resolve_source_uri(uri, source) is not None:
78 resolved.append(uri)
79 else:
80 unresolved.append(uri)
81 except URIError:
82 unresolved.append(uri)
83 return ValidationResult(resolved=resolved, unresolved=unresolved)
86@dataclass
87class ValidationResult:
88 resolved: list[str]
89 unresolved: list[str]
92def _matches(cell: CellValue, ref: SourceRef) -> bool:
93 prov = cell.provenance
94 if prov is None:
95 return False
96 return (
97 prov.file_name == ref.file_name
98 and prov.sheet_name == ref.sheet_name
99 and f"{prov.column}{prov.row}" == ref.cell_ref
100 )