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

1"""URI resolver 与强验证(T12,OV3)。 

2 

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""" 

11 

12from __future__ import annotations 

13 

14from dataclasses import dataclass 

15 

16from genesis.data_models import CellValue, Provenance, StructuredSource 

17from genesis.parsers.provenance import build_source_uri 

18 

19 

20class URIError(ValueError): 

21 """URI 格式非法(不符合 file.xlsx#SheetName!CellRef)。""" 

22 

23 

24@dataclass(frozen=True) 

25class SourceRef: 

26 """URI 解析后的结构化定位。""" 

27 

28 file_name: str 

29 sheet_name: str 

30 cell_ref: str 

31 

32 

33def parse_source_uri(uri: str) -> SourceRef: 

34 """解析 `file.xlsx#SheetName!C3` → SourceRef。 

35 

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) 

48 

49 

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) 

54 

55 

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 

65 

66 

67def validate_source_uris(uris: list[str], source: StructuredSource) -> "ValidationResult": 

68 """批量强验证:把 URI 分为可在源中定位(resolved)与不可定位(unresolved)。 

69 

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) 

84 

85 

86@dataclass 

87class ValidationResult: 

88 resolved: list[str] 

89 unresolved: list[str] 

90 

91 

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 )