python-hwpx 2.4__tar.gz → 2.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_hwpx-2.4 → python_hwpx-2.5}/PKG-INFO +1 -1
- {python_hwpx-2.4 → python_hwpx-2.5}/pyproject.toml +1 -1
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/document.py +6 -1
- {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/PKG-INFO +1 -1
- {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
- python_hwpx-2.5/tests/test_split_merged_cell.py +185 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/LICENSE +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/README.md +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/setup.cfg +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/__init__.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/document.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/paragraph.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/package.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/py.typed +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/templates.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_document_formatting.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_document_save_api.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_inline_models.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_new_features.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_opc_package.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_section_headers.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_version_metadata.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.5"
|
|
8
8
|
description = "Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -2541,7 +2541,12 @@ class HwpxOxmlTable:
|
|
|
2541
2541
|
existing_target.set_size(col_width, row_height)
|
|
2542
2542
|
continue
|
|
2543
2543
|
|
|
2544
|
-
|
|
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))
|
|
2545
2550
|
for child in preserved_children:
|
|
2546
2551
|
new_cell_element.append(deepcopy(child))
|
|
2547
2552
|
|
|
@@ -51,6 +51,7 @@ tests/test_packaging_py_typed.py
|
|
|
51
51
|
tests/test_paragraph_section_management.py
|
|
52
52
|
tests/test_repr_snapshots.py
|
|
53
53
|
tests/test_section_headers.py
|
|
54
|
+
tests/test_split_merged_cell.py
|
|
54
55
|
tests/test_tables_default_border.py
|
|
55
56
|
tests/test_text_extractor_annotations.py
|
|
56
57
|
tests/test_version_metadata.py
|
|
@@ -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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|