feat: FormattingDetector(取消线/背景色/批注)

This commit is contained in:
lhl
2026-08-09 03:00:01 +08:00
parent 0051ad52b4
commit 5818dd01e4
3 changed files with 107 additions and 0 deletions
+1
View File
@@ -26,4 +26,5 @@
| 2026-08-08 | Agent 实现 | 里程碑2 Task2 实现:SheetDetector 类型识别。新建 src/genesis/parsers/sheet_detector.py(名称关键词→表头关键词→GENERIC 三级判定,detect_sheet_type(sheet_name, matrix)->SheetType)与 tests/test_sheet_detector.py3 用例:名称/表头/未知);TDD 验证 REDModuleNotFoundError)→ GREEN3 passed);修正 brief 缺陷:brief 原始 NAME_KEYWORDS 无法匹配「コード管理」判为 MASTER(不含"マスタ"),最小补入 ("コード", MASTER)pytest 全量 20 passed;提交 66753e4 | src/genesis/parsers/sheet_detector.py, tests/test_sheet_detector.py | deepseek-v4-flash-free |
| 2026-08-08 | Agent 实现 | 里程碑2 Task3 实现:Sheet 性质判定。新建 src/genesis/parsers/sheet_nature.py(纯函数:SheetNature 枚举 TABLE/FREE_TEXT/MIXED、find_header_row 表头行定位、classify_sheet 按 design §3.5.2 启发式判定)与 tests/test_sheet_nature.py4 用例);TDD RED→GREENpytest 全量 24 passed | src/genesis/parsers/sheet_nature.py, tests/test_sheet_nature.py | deepseek-v4-flash-free |
| 2026-08-08 23:04 | Agent 实现 | 里程碑2 Task4 实现:MergeHandler 与 TableExtractor。新建 src/genesis/parsers/merge_fill.pyforward_fill 合并单元格展开,(min_row,min_col,max_row,max_col) 1-based 范围用左上主格值填充,边界保护)与 src/genesis/parsers/table_extractor.pycolumn_letter 1→A/27→AAextract_table 首行表头→其后为数据行,构建 CellValue+Provenance(row=r-header_row 从 1 起、column 字母、column_header),无矩阵时返回空表)及 tests/test_table_extractor.py4 用例:纵向/横向合并、列字母、基本提取);TDD 验证 REDImportError: No module named 'genesis.parsers.merge_fill')→ GREEN4 passed);pytest 全量 28 passeddata_models.py 未改动;提交 63d0c90 | src/genesis/parsers/merge_fill.py, src/genesis/parsers/table_extractor.py, tests/test_table_extractor.py | deepseek-v4-flash-free |
| 2026-08-09 02:59 | Agent 实现 | 里程碑2 Task5 实现:FormattingDetector(取消线/背景色/批注)。新建 src/genesis/parsers/formatting_detector.pycell_formatting 检测 strike/字体色/背景色,纯默认单元格返回 Nonecell_comment 返回 CellCommentcollect_comments 遍历 ws.iter_rows 收集批注)与 tests/test_formatting_detector.py4 用例);TDD 验证 REDImportError)→ GREEN4 passed);修正 brief 两处 openpyxl 3.1.5 兼容问题(① 字体 Style 对象不可变,测试用 copy 副本设 strike;② 默认 theme 色 .rgb 返回错误文本而非 None,新增 _to_rgb_hex 严格校验仅接受 6/8 位十六进制并排除纯黑);pytest 全量 32 passeddata_models.py 未改动 | src/genesis/parsers/formatting_detector.py, tests/test_formatting_detector.py | deepseek-v4-flash-free |
@@ -0,0 +1,57 @@
from __future__ import annotations
from genesis.data_models import CellComment, CellFormatting
from genesis.parsers.provenance import build_source_uri
_BLACK_RGB = ("00000000", "FF000000")
def _to_rgb_hex(color) -> str | None:
"""将 openpyxl Color 转为 RGB 十六进制;非 RGB 主题色/默认色返回 None"""
if color is None:
return None
try:
value = str(color.rgb)
except Exception:
return None
if not value or value in _BLACK_RGB:
return None
if len(value) not in (6, 8):
return None
if not all(ch in "0123456789ABCDEFabcdef" for ch in value):
return None
return value
def cell_formatting(cell) -> CellFormatting | None:
strike = bool(cell.font.strike)
font_color = _to_rgb_hex(getattr(cell.font, "color", None))
bg_color = None
fill = cell.fill
if fill is not None:
bg_color = _to_rgb_hex(getattr(fill, "fgColor", None))
if strike or font_color or bg_color:
return CellFormatting(
strikethrough=strike, font_color=font_color, bg_color=bg_color,
)
return None
def cell_comment(cell, file_name: str) -> CellComment | None:
if cell.comment is None:
return None
return CellComment(
author=cell.comment.author or "",
text=cell.comment.text or "",
source_uri=build_source_uri(file_name, cell.parent.title, cell.coordinate),
)
def collect_comments(ws, file_name: str) -> list[CellComment]:
result = []
for row in ws.iter_rows():
for cell in row:
cm = cell_comment(cell, file_name)
if cm is not None:
result.append(cm)
return result
+49
View File
@@ -0,0 +1,49 @@
from copy import copy
from openpyxl import Workbook
from openpyxl.comments import Comment
from genesis.data_models import CellComment, CellFormatting
from genesis.parsers.formatting_detector import (
cell_comment, cell_formatting, collect_comments,
)
def make_wb():
wb = Workbook()
ws = wb.active
ws.title = "機能一覧"
ws["A1"] = "F001"
font = copy(ws["A1"].font)
font.strike = True
ws["A1"].font = font
ws["A2"] = "F002"
ws["A2"].comment = Comment("要確認", "reviewer")
return wb
def test_cell_formatting_detects_strike():
fmt = cell_formatting(make_wb().active["A1"])
assert fmt is not None
assert fmt.strikethrough is True
def test_cell_formatting_none_when_plain():
wb = Workbook()
ws = wb.active
ws["A1"] = "x"
assert cell_formatting(ws["A1"]) is None
def test_cell_comment_returns_obj():
wb = make_wb()
cm = cell_comment(wb.active["A2"], "f.xlsx")
assert isinstance(cm, CellComment)
assert cm.author == "reviewer"
assert cm.text == "要確認"
assert cm.source_uri == "f.xlsx#機能一覧!A2"
def test_collect_comments():
comments = collect_comments(make_wb().active, "f.xlsx")
assert len(comments) == 1