feat: FormattingDetector(取消线/背景色/批注)
This commit is contained in:
@@ -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.py(3 用例:名称/表头/未知);TDD 验证 RED(ModuleNotFoundError)→ GREEN(3 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.py(4 用例);TDD RED→GREEN;pytest 全量 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.py(forward_fill 合并单元格展开,(min_row,min_col,max_row,max_col) 1-based 范围用左上主格值填充,边界保护)与 src/genesis/parsers/table_extractor.py(column_letter 1→A/27→AA;extract_table 首行表头→其后为数据行,构建 CellValue+Provenance(row=r-header_row 从 1 起、column 字母、column_header),无矩阵时返回空表)及 tests/test_table_extractor.py(4 用例:纵向/横向合并、列字母、基本提取);TDD 验证 RED(ImportError: No module named 'genesis.parsers.merge_fill')→ GREEN(4 passed);pytest 全量 28 passed;data_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.py(cell_formatting 检测 strike/字体色/背景色,纯默认单元格返回 None;cell_comment 返回 CellComment;collect_comments 遍历 ws.iter_rows 收集批注)与 tests/test_formatting_detector.py(4 用例);TDD 验证 RED(ImportError)→ GREEN(4 passed);修正 brief 两处 openpyxl 3.1.5 兼容问题(① 字体 Style 对象不可变,测试用 copy 副本设 strike;② 默认 theme 色 .rgb 返回错误文本而非 None,新增 _to_rgb_hex 严格校验仅接受 6/8 位十六进制并排除纯黑);pytest 全量 32 passed;data_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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user