Repository navigation
Conversation
table.cell(r, c)·set_cell_text(r, c)가 호출마다 모든 칸 래퍼와 격자를 새로 만들어 칸 수의 제곱으로 느려지던 것을 고친다. - HwpxOxmlTable이 만든 격자를 보관한다. 다시 쓰기 전에 표 XML과 대조한다. 좌표 조회는 행·칸 수와 돌려줄 칸의 주소·병합·크기 속성을 보고, iter_grid· get_cell_map·병합은 모든 칸의 주소·병합·크기(크기 0인 1x1 칸은 글 유무까지)를 본다. 다르면 새로 만든다. 그래서 XML을 직접 고쳤거나 같은 표의 다른 래퍼가 고친 경우에도 낡은 격자를 쓰지 않는다. - 병합·분할·행/열 추가·삭제·칸 크기/병합 설정은 격자를 바로 버린다. - tools/table_navigation의 표 지도는 iter_grid() 한 번으로 돈다. 측정(40x40 표, 3회 중앙값): cell 전 칸 42.5초 -> 0.11초, tables.map 118초 -> 0.14초, set_cell_text 전 칸 43초 -> 0.29초. m2_corpus/form_002.hwpx tables.map 16.0초 -> 0.19초. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
칸 지정마다 표 바이트 전체를 다시 훑어 격자를 만들던 것을 표(table_index)마다 한 번으로 줄이고, 이미 채운 칸 구간(occupied)을 집합으로 둔다. 구역 바이트는 편집을 모두 모은 뒤에야 바뀌므로 같은 표의 지정은 같은 격자를 본다. 결과 바이트와 applied/skipped는 그대로다. 측정(전 칸을 한 번에, 5회 중앙값): 200칸 1.04초 -> 0.09초, 400칸 3.56초 -> 0.15초, 800칸 13.7초 -> 0.33초. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
- _convert_to_lxml: 이미 lxml 요소면 그대로 넘긴다. 표준 라이브러리 직렬기로 쓰고 다시 파싱하던 왕복이 조회마다 있었다. - paragraph_property·paragraph_properties·styles·tab_properties·border_fills· bullets·track_changes·track_change_authors의 파싱 결과를 헤더에 보관한다. mark_dirty()에서 버리고, 쓸 때마다 요소의 직렬화 바이트를 견주므로 dirty 표시 없이 XML을 직접 고친 경우에도 낡은 결과를 주지 않는다. 사전은 호출마다 새로 만들고, 그 안의 모델 객체는 char_property처럼 보관본을 그대로 준다. - paragraph_property의 id -> 요소 색인도 목록의 자식과 id가 같을 동안 보관한다. 달라지는 점: 헤더 목록 안에 XML 주석·처리 명령이 있으면 예전에는 조회가 TypeError(cannot serialize Comment)로 실패했고 이제는 조회된다. 측정(m2_corpus/form_002.hwpx, 조회 1회, 5회 중앙값): paragraph_property 0.47 -> 0.075 ms, styles.get 1.01 -> 0.10 ms, border_fill 2.5 -> 0.17 ms, paragraph_properties 전체 21.7 -> 1.36 ms. lint_layout 963 -> 578 ms. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
- section.add_paragraph: 서식을 물려받을 때 마지막 hp:p 요소 하나만 본다. - set_paragraph_format(paragraphs=[...]): 대상이 8개 이하이면 본문 순번을 그 문단 앞의 형제 문단을 세어 구한다(_body_indexes). 결과의 순번은 그대로다. - bind_outline_level(add_heading): 문단 목록에서 순번을 찾았다가 순번으로 다시 찾던 것을 문단 객체로 바로 넘긴다. doc.paragraphs 자체는 여전히 호출마다 새 목록을 만든다(공개 동작 유지). 측정(새 문서, 중앙값): add_paragraph 2,000번 2.10초 -> 0.16초, 4,000번 8.3초 -> 0.35초. append_markdown 2,000블록 0.45초 -> 0.26초. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
iter_paragraphs가 문단마다 describe_element_path를 불러 조상 단계마다 같은 이름 형제 목록을 새로 만들던 것을, 부모별 순번과 부모 경로를 보관하는 _ElementPaths로 바꾼다. 경로 문자열은 같다. ObjectFinder.iter도 같은 것을 쓴다. describe_element_path(공개 함수)는 그대로 둔다. 측정(extract_text, 5회 중앙값): 합성 2,000문단 948 -> 24 ms, 4,000문단 3,788 -> 59 ms, m2_corpus/gov_donation_report_form.hwpx 278 -> 84 ms. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
save_report는 직렬화에서 검증한 것과 같은 바이트로 validate_editor_open_safety를 한 번 더 돌렸다. 패키지가 통과한 보고서를 그 바이트의 SHA-256과 함께 보관하고, save_report는 다이제스트가 같을 때 그 보고서를 SavePipeline에 넘긴다(파이프라인이 스스로 만들던 것과 같은 ok/summary/detail). 다이제스트가 다르거나 보관본이 없으면 지금처럼 파이프라인이 직접 검증한다. 검증 항목은 그대로다. 범위를 좁힌 부분: to_bytes 안에서 validate_package·validate_document·재열기가 같은 바이트를 각자 풀어 파싱하는 것은 건드리지 않았다(서로 다른 파서와 트리를 쓴다). 측정(문단 1개 추가 뒤 save_report, 5회 중앙값): 작은 72 -> 43 ms, 중간 847 -> 575 ms, 큰 1,886 -> 1,318 ms. to_bytes·save_to_path는 변화 없음. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
가지 제한 없는 push와 pull_request가 함께 걸려 PR 커밋 하나가 두 번 검사됐다 (최근 8.7일에 SHA 55개). push를 main으로 좁히고 concurrency를 건다: 같은 PR의 새 커밋이 앞선 실행을 취소하고, main의 실행은 커밋마다 따로 묶여 취소되지 않는다. 달라지는 점: PR이 없는 가지와 태그의 push에서는 Tests가 돌지 않는다 (릴리스는 release.yml이 따로 검사한다). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
--check 그린, 드리프트 감지, 경로 구분자 테스트가 각자 build_ledger를 돌려 (둘째는 main()에서 한 번 더) 네 번 만들던 것을 모듈 범위 fixture 하나로 줄인다. main()은 그 원장의 사본을 받아 같은 비교 코드를 돈다. 그린 검사는 프로세스 안에서 돌고, 스크립트가 프로세스로 뜨는지는 --help로 따로 본다. 측정(이 PC, 파일 전체): 592초 -> 128초. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
행마다 save_to_path가 검증한 것과 같은 바이트를 validate_editor_open_safety로 한 번 더 검증했다. 쓴 파일의 SHA-256이 저장이 검증한 다이제스트와 같으면 그 보고서를 쓰고, 다르면 지금처럼 파일을 검증한다. 보고서 내용은 같다. 남긴 부분: 행마다 템플릿을 다시 여는 것과, 남은 자리표시자를 보려고 결과물을 다시 여는 것은 그대로다. 측정(10행, 5회 중앙값): 본문 200문단 1,277 -> 765 ms, 800문단 2,410 -> 1,624 ms. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
EFF-01이 table.py를 모듈 크기 검토선(코드 1,300줄) 위로 올리고 격자 생성 함수의 이름을 바꿔 test_oxml_modularization의 두 게이트가 깨졌다. - 보관한 격자가 표 XML과 같은지 보는 TableGridCache를 table_grid.py로 옮긴다. - 격자를 만드는 함수는 _build_cell_grid 이름을 그대로 두고(복잡도 래칫), 보관본을 주는 쪽을 _cell_grid로 둔다. 동작은 같다. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
새 모듈 table_grid.py는 소유 등록·크기 래칫 게이트(test_product_boundary, test_size_ratchet)에 걸렸다. table.py의 줄 예산 때문에 격자 관련 코드를 두는 기존 모듈 table_merge.py로 옮기고 table_grid.py를 지운다. 동작은 같다. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
airmang
left a comment
There was a problem hiding this comment.
Reviewed the table/style/save/extraction optimizations at 25fc358. All eight exact-head checks pass; the Python 3.12 log records 7,704 passed, 18 skipped, 1 xfailed. Local focused checks pass (27 tests), and the integration with #676 passed 44 tests. An independent baseline/head comparison reproduces the loss of the overlapping-grid rejection described inline, so merge is held. The signed-in account owns this PR, so this COMMENT records the required correction rather than an approval.
| entry = self._build_cell_grid().get((row_index, col_index)) | ||
| entry = None | ||
| cache = getattr(self, "_grid_cache", None) | ||
| if cache is not None and cache.fast and cache.same_shape(self.element): |
There was a problem hiding this comment.
[P2] Validate changes to other cells before returning a cached grid entry
The fast path checks only the returned cell. With a warmed 1×2 table, change the second cell's raw hp:cellAddr/@colAddr from 1 to 0, then call table.cell(0, 0). Main 168fe98 raises ValueError: table grid contains overlapping cell spans; this head returns the first cell successfully, while this head's own _build_cell_grid() still raises the same ValueError. same_shape() sees unchanged row/child counts and cell_unchanged() sees an unchanged first cell, so the cache bypasses the existing invalid-grid guard. Raw XML changes are explicitly supported by the new invalidation contract and tests. Please invalidate/revalidate for structural changes to any contributing cell, and add the regression where the changed cell is different from the queried cell, preserving the previous fail-closed lookup behavior.
요약
2026-11-10 점검에서 나온 효율 항목을 고칩니다. 출력은 바뀌지 않습니다. 커밋은 항목별로 나눴습니다.
tables.map()form_002.hwpx(131 KB 양식)의tables.map()fill_cells로 800칸을 한 번에 채우기paragraph_property1회add_paragraph4,000번TextExtractor.extract_text4,000문단save_report(131 KB 양식, 문단 1개 추가 뒤)tests/test_coverage_ledger.pytable.cell()은 같은 래퍼 객체를 돌려줍니다.save_report가 직렬화 때 통과한 검증 결과를 넘겨받습니다(바이트의 SHA-256이 같을 때만). 검증 항목은 줄이지 않았습니다.to_bytes안의 검증기들이 같은 바이트를 각자 푸는 것은 이번에 건드리지 않았습니다.push를 main으로 좁히고 PR 실행에concurrency를 걸었습니다. 전에는 PR 가지의 커밋마다 두 번 돌았습니다.측정은 Windows 11에서 다른 작업이 함께 도는 중에 여러 번 재서 낸 중앙값입니다.
출력이 달라지지 않았다는 확인
tests의 .hwpx 1,210개를 열어 그대로 저장한 바이트의 SHA-256이 수정 전후로 모두 같습니다.fill_cells·문단 추가·추출·스타일 조회·save_report·lint·쪽 추정·검증·마크다운)의 결과 해시가 전후로 같습니다.병합 전에 볼 것
char_property와 같은 방식). 받은 객체를 고쳐 쓰는 코드는src안에서 찾지 못했습니다.cell(r, c))는 행·칸 수와 돌려줄 칸만 대조합니다. 다른 칸을 XML로 직접 고쳐 조회 대상과 겹치게 만든 깨진 표는 전처럼ValueError가 나지 않을 수 있습니다.TypeError로 실패했고 이제는 조회됩니다.관련 이슈
Refs #628
변경 종류
체크리스트
pytest테스트가 통과합니다. — Windows 11에서 7,690 통과, 14 실패. 실패 14건은 이 변경 전 main에서도 같은 이름으로 실패하던 것입니다(cp949, 심볼릭 링크 권한, CRLF). Linux CI 결과는 이 PR의 체크로 확인해 주세요.CHANGELOG.md를 갱신했습니다(해당하는 경우).🤖 Generated with Claude Code