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.
Files changed (59) hide show
  1. {python_hwpx-2.4 → python_hwpx-2.5}/PKG-INFO +1 -1
  2. {python_hwpx-2.4 → python_hwpx-2.5}/pyproject.toml +1 -1
  3. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/document.py +6 -1
  4. {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/PKG-INFO +1 -1
  5. {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
  6. python_hwpx-2.5/tests/test_split_merged_cell.py +185 -0
  7. {python_hwpx-2.4 → python_hwpx-2.5}/LICENSE +0 -0
  8. {python_hwpx-2.4 → python_hwpx-2.5}/README.md +0 -0
  9. {python_hwpx-2.4 → python_hwpx-2.5}/setup.cfg +0 -0
  10. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/__init__.py +0 -0
  11. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/data/Skeleton.hwpx +0 -0
  12. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/document.py +0 -0
  13. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/opc/package.py +0 -0
  14. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/opc/xml_utils.py +0 -0
  15. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/__init__.py +0 -0
  16. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/body.py +0 -0
  17. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/common.py +0 -0
  18. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/header.py +0 -0
  19. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/header_part.py +0 -0
  20. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/memo.py +0 -0
  21. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/namespaces.py +0 -0
  22. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/paragraph.py +0 -0
  23. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/parser.py +0 -0
  24. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/schema.py +0 -0
  25. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/section.py +0 -0
  26. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/table.py +0 -0
  27. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/oxml/utils.py +0 -0
  28. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/package.py +0 -0
  29. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/py.typed +0 -0
  30. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/templates.py +0 -0
  31. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/__init__.py +0 -0
  32. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/_schemas/header.xsd +0 -0
  33. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/_schemas/section.xsd +0 -0
  34. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/exporter.py +0 -0
  35. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/object_finder.py +0 -0
  36. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/text_extractor.py +0 -0
  37. {python_hwpx-2.4 → python_hwpx-2.5}/src/hwpx/tools/validator.py +0 -0
  38. {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
  39. {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/entry_points.txt +0 -0
  40. {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/requires.txt +0 -0
  41. {python_hwpx-2.4 → python_hwpx-2.5}/src/python_hwpx.egg-info/top_level.txt +0 -0
  42. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_coverage_targets.py +0 -0
  43. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_document_context_manager.py +0 -0
  44. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_document_formatting.py +0 -0
  45. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_document_save_api.py +0 -0
  46. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_inline_models.py +0 -0
  47. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_integration_hwpx_compatibility.py +0 -0
  48. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_integration_roundtrip.py +0 -0
  49. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_memo_and_style_editing.py +0 -0
  50. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_new_features.py +0 -0
  51. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_opc_package.py +0 -0
  52. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_oxml_parsing.py +0 -0
  53. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_packaging_py_typed.py +0 -0
  54. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_paragraph_section_management.py +0 -0
  55. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_repr_snapshots.py +0 -0
  56. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_section_headers.py +0 -0
  57. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_tables_default_border.py +0 -0
  58. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_text_extractor_annotations.py +0 -0
  59. {python_hwpx-2.4 → python_hwpx-2.5}/tests/test_version_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 2.4
3
+ Version: 2.5
4
4
  Summary: Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음
5
5
  Author: python-hwpx Maintainers
6
6
  License: Non-Commercial License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-hwpx"
7
- version = "2.4"
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
- 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))
2545
2550
  for child in preserved_children:
2546
2551
  new_cell_element.append(deepcopy(child))
2547
2552
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 2.4
3
+ Version: 2.5
4
4
  Summary: Hancom HWPX 패키지를 로드하고 편집하기 위한 Python 유틸리티 모음
5
5
  Author: python-hwpx Maintainers
6
6
  License: Non-Commercial License
@@ -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