Skip to content

Commit c82966e

Browse files
committed
버전 2.5로 업데이트 및 split_merged_cell 기능 개선: XML 엔진 호환성 문제 해결을 위한 테스트 추가
1 parent b782b77 commit c82966e

3 files changed

Lines changed: 192 additions & 2 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "python-hwpx"
7-
version = "2.4"
7+
version = "2.5"
88
description = "Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음"
99
readme = { file = "README.md", content-type = "text/markdown" }
1010
license = { file = "LICENSE" }

src/hwpx/oxml/document.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2541,7 +2541,12 @@ def split_merged_cell(
25412541
existing_target.set_size(col_width, row_height)
25422542
continue
25432543

2544-
new_cell_element = ET.Element(f"{_HP}tc", dict(template_attrs))
2544+
# Use makeelement() so the new cell matches the XML engine
2545+
# of the existing tree (stdlib ET or lxml). ET.Element()
2546+
# always produces stdlib elements which cannot be appended to
2547+
# an lxml tree (and vice-versa), causing TypeError at runtime
2548+
# when splitting cells in documents parsed via lxml.
2549+
new_cell_element = row_element.makeelement(f"{_HP}tc", dict(template_attrs))
25452550
for child in preserved_children:
25462551
new_cell_element.append(deepcopy(child))
25472552

tests/test_split_merged_cell.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""Regression tests for split_merged_cell – ET / lxml mixing fix.
2+
3+
The root cause of the original crash (TypeError: append() argument 1
4+
must be xml.etree.ElementTree.Element, not lxml.etree._Element) was that
5+
``split_merged_cell`` created new cell elements with stdlib
6+
``ET.Element()`` while the existing document tree consisted of lxml
7+
elements (parsed via ``lxml.etree.fromstring``). The fix uses
8+
``row_element.makeelement()`` so that new cells always match the XML
9+
engine of the surrounding tree.
10+
11+
Choice A was applied: *all runtime element creation inside
12+
``split_merged_cell`` is now engine-agnostic* by delegating to
13+
``makeelement`` / ``SubElement`` (which itself delegates to
14+
``makeelement``), so the code works identically with both stdlib ET
15+
and lxml trees.
16+
"""
17+
18+
from __future__ import annotations
19+
20+
import io
21+
22+
import pytest
23+
24+
from hwpx.document import HwpxDocument
25+
26+
27+
# --------------------------------------------------------------------------- #
28+
# Helpers
29+
# --------------------------------------------------------------------------- #
30+
31+
32+
def _new_doc_with_table(rows: int = 3, cols: int = 3) -> tuple[HwpxDocument, object]:
33+
"""Return (document, table) backed by lxml (via HwpxDocument.new())."""
34+
doc = HwpxDocument.new()
35+
table = doc.add_table(rows, cols)
36+
return doc, table
37+
38+
39+
# --------------------------------------------------------------------------- #
40+
# Scenario 1 – horizontal merge then split
41+
# --------------------------------------------------------------------------- #
42+
43+
44+
def test_split_horizontal_merge_no_type_error() -> None:
45+
"""Splitting a horizontally merged cell must not raise TypeError.
46+
47+
This is the exact code-path that triggered the original crash when
48+
an lxml-backed table was modified with stdlib ET elements.
49+
"""
50+
doc, table = _new_doc_with_table(3, 3)
51+
52+
# Merge (0,0)–(0,1) horizontally
53+
table.merge_cells(0, 0, 0, 1)
54+
merged = table.cell(0, 0)
55+
assert merged.span == (1, 2), "pre-condition: cell should be merged"
56+
57+
# Split – this used to crash with TypeError
58+
result = table.split_merged_cell(0, 0)
59+
assert result is not None
60+
61+
# Master cell span reset to (1, 1)
62+
assert table.cell(0, 0).span == (1, 1)
63+
# Restored cell exists and is independent
64+
assert table.cell(0, 1).span == (1, 1)
65+
assert table.cell(0, 0).element is not table.cell(0, 1).element
66+
67+
68+
# --------------------------------------------------------------------------- #
69+
# Scenario 2 – vertical merge then split
70+
# --------------------------------------------------------------------------- #
71+
72+
73+
def test_split_vertical_merge_no_type_error() -> None:
74+
"""Splitting a vertically merged cell must not raise TypeError."""
75+
doc, table = _new_doc_with_table(3, 3)
76+
77+
# Merge (0,0)–(1,0) vertically
78+
table.merge_cells(0, 0, 1, 0)
79+
assert table.cell(0, 0).span == (2, 1)
80+
81+
result = table.split_merged_cell(0, 0)
82+
assert result is not None
83+
84+
assert table.cell(0, 0).span == (1, 1)
85+
assert table.cell(1, 0).span == (1, 1)
86+
assert table.cell(0, 0).element is not table.cell(1, 0).element
87+
88+
89+
# --------------------------------------------------------------------------- #
90+
# Scenario 3 – 2×2 block merge then split
91+
# --------------------------------------------------------------------------- #
92+
93+
94+
def test_split_block_merge_restores_all_cells() -> None:
95+
"""A 2×2 block merge should produce 4 independent cells after split."""
96+
doc, table = _new_doc_with_table(3, 3)
97+
98+
table.merge_cells(0, 0, 1, 1)
99+
assert table.cell(0, 0).span == (2, 2)
100+
101+
table.split_merged_cell(0, 0)
102+
103+
for r in range(2):
104+
for c in range(2):
105+
cell = table.cell(r, c)
106+
assert cell.span == (1, 1), f"cell ({r},{c}) span should be (1,1)"
107+
108+
109+
# --------------------------------------------------------------------------- #
110+
# Scenario 4 – save → reopen round-trip after split
111+
# --------------------------------------------------------------------------- #
112+
113+
114+
def test_split_then_save_reopen_roundtrip(tmp_path) -> None:
115+
"""After splitting, the document must survive save → reopen."""
116+
doc, table = _new_doc_with_table(3, 3)
117+
118+
# Write identifiable text before merge
119+
table.set_cell_text(0, 0, "A")
120+
table.set_cell_text(0, 1, "B")
121+
table.set_cell_text(0, 2, "C")
122+
123+
# Merge (0,0)–(0,1) then split
124+
table.merge_cells(0, 0, 0, 1)
125+
table.split_merged_cell(0, 0)
126+
127+
# Set text in the restored cell
128+
table.cell(0, 1).text = "B-restored"
129+
130+
# Save to bytes and reopen
131+
buf = io.BytesIO()
132+
doc.save(buf)
133+
buf.seek(0)
134+
135+
reopened = HwpxDocument.open(buf.getvalue())
136+
# Collect tables from all paragraphs
137+
rt_tables = [
138+
t
139+
for para in reopened.paragraphs
140+
for t in para.tables
141+
]
142+
assert len(rt_tables) >= 1
143+
144+
rt_table = rt_tables[0]
145+
assert rt_table.cell(0, 0).span == (1, 1)
146+
assert rt_table.cell(0, 1).span == (1, 1)
147+
# Master cell kept its original text
148+
assert rt_table.cell(0, 0).text == "A"
149+
# Restored cell has the text we set
150+
assert rt_table.cell(0, 1).text == "B-restored"
151+
# Untouched cell is intact
152+
assert rt_table.cell(0, 2).text == "C"
153+
154+
155+
# --------------------------------------------------------------------------- #
156+
# Scenario 5 – split via set_cell_text logical API
157+
# --------------------------------------------------------------------------- #
158+
159+
160+
def test_set_cell_text_split_merged_flag() -> None:
161+
"""``set_cell_text(split_merged=True)`` must trigger split correctly."""
162+
doc, table = _new_doc_with_table(3, 3)
163+
164+
table.merge_cells(0, 0, 0, 1)
165+
# Write to the covered column with split_merged=True
166+
table.set_cell_text(0, 1, "Split-Write", logical=True, split_merged=True)
167+
168+
assert table.cell(0, 0).span == (1, 1)
169+
assert table.cell(0, 1).text == "Split-Write"
170+
assert table.cell(0, 1).span == (1, 1)
171+
172+
173+
# --------------------------------------------------------------------------- #
174+
# Scenario 6 – splitting an already-unmerged cell is a no-op
175+
# --------------------------------------------------------------------------- #
176+
177+
178+
def test_split_unmerged_cell_is_noop() -> None:
179+
"""Splitting a cell that is not merged should return it unchanged."""
180+
doc, table = _new_doc_with_table(2, 2)
181+
182+
cell_before = table.cell(0, 0)
183+
cell_after = table.split_merged_cell(0, 0)
184+
assert cell_before.element is cell_after.element
185+
assert cell_after.span == (1, 1)

0 commit comments

Comments
 (0)