Skip to content

Commit b0061eb

Browse files
author
Steve Canny
committed
tbl: add CT_Tc._swallow_next_tc()
1 parent cd09fab commit b0061eb

File tree

3 files changed

+117
-5
lines changed

3 files changed

+117
-5
lines changed

docx/oxml/table.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,11 @@ def grid_span(self):
254254
return 1
255255
return tcPr.grid_span
256256

257+
@grid_span.setter
258+
def grid_span(self, value):
259+
tcPr = self.get_or_add_tcPr()
260+
tcPr.grid_span = value
261+
257262
def iter_block_items(self):
258263
"""
259264
Generate a reference to each of the block-level content elements in
@@ -345,6 +350,14 @@ def width(self, value):
345350
tcPr = self.get_or_add_tcPr()
346351
tcPr.width = value
347352

353+
def _add_width_of(self, other_tc):
354+
"""
355+
Add the width of *other_tc* to this cell. Does nothing if either this
356+
tc or *other_tc* does not have a specified width.
357+
"""
358+
if self.width and other_tc.width:
359+
self.width += other_tc.width
360+
348361
@property
349362
def _grid_col(self):
350363
"""
@@ -414,6 +427,21 @@ def _move_content_to(self, other_tc):
414427
def _new_tbl(self):
415428
return CT_Tbl.new()
416429

430+
@property
431+
def _next_tc(self):
432+
"""
433+
The `w:tc` element immediately following this one in this row, or
434+
|None| if this is the last `w:tc` element in the row.
435+
"""
436+
following_tcs = self.xpath('./following-sibling::w:tc')
437+
return following_tcs[0] if following_tcs else None
438+
439+
def _remove(self):
440+
"""
441+
Remove this `w:tc` element from the XML tree.
442+
"""
443+
self.getparent().remove(self)
444+
417445
def _remove_trailing_empty_p(self):
418446
"""
419447
Remove the last content element from this cell if it is an empty
@@ -485,7 +513,18 @@ def _swallow_next_tc(self, grid_width, top_tc):
485513
|InvalidSpanError| if the width of the resulting cell is greater than
486514
*grid_width* or if there is no next `<w:tc>` element in the row.
487515
"""
488-
raise NotImplementedError
516+
def raise_on_invalid_swallow(next_tc):
517+
if next_tc is None:
518+
raise InvalidSpanError('not enough grid columns')
519+
if self.grid_span + next_tc.grid_span > grid_width:
520+
raise InvalidSpanError('span is not rectangular')
521+
522+
next_tc = self._next_tc
523+
raise_on_invalid_swallow(next_tc)
524+
next_tc._move_content_to(top_tc)
525+
self._add_width_of(next_tc)
526+
self.grid_span += next_tc.grid_span
527+
next_tc._remove()
489528

490529
@property
491530
def _tbl(self):
@@ -577,6 +616,12 @@ def grid_span(self):
577616
return 1
578617
return gridSpan.val
579618

619+
@grid_span.setter
620+
def grid_span(self, value):
621+
self._remove_gridSpan()
622+
if value > 1:
623+
self.get_or_add_gridSpan().val = value
624+
580625
@property
581626
def vMerge_val(self):
582627
"""

features/tbl-merge-cells.feature

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ Feature: Merge table cells
33
As a developer using python-docx
44
I need a way to merge a range of cells
55

6-
@wip
76
Scenario Outline: Merge cells
87
Given a 3x3 table having only uniform cells
98
When I merge from cell <origin> to cell <other>
@@ -16,7 +15,6 @@ Feature: Merge table cells
1615
| 5 | 9 | 1 2 3 4 5\6\8\9 5\6\8\9 7 5\6\8\9 5\6\8\9 |
1716

1817

19-
@wip
2018
Scenario Outline: Merge horizontal span with other cell
2119
Given a 3x3 table having a horizontal span
2220
When I merge from cell <origin> to cell <other>
@@ -29,7 +27,6 @@ Feature: Merge table cells
2927
| 2 | 4 | 1\2\4 1\2\4 3 1\2\4 1\2\4 6 7 8 9 |
3028

3129

32-
@wip
3330
Scenario Outline: Merge vertical span with other cell
3431
Given a 3x3 table having a vertical span
3532
When I merge from cell <origin> to cell <other>
@@ -42,7 +39,6 @@ Feature: Merge table cells
4239
| 7 | 5 | 1 2 3 4\5\7 4\5\7 6 4\5\7 4\5\7 9 |
4340

4441

45-
@wip
4642
Scenario Outline: Horizontal span adds cell widths
4743
Given a 3x3 table having <span-state>
4844
When I merge from cell <origin> to cell <other>

tests/oxml/test_table.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,21 @@ def it_can_extend_its_horz_span_to_help_merge(self, span_width_fixture):
7373
assert tc._swallow_next_tc.call_args_list == expected_calls
7474
assert tc.vMerge == vMerge
7575

76+
def it_can_swallow_the_next_tc_help_merge(self, swallow_fixture):
77+
tc, grid_width, top_tc, tr, expected_xml = swallow_fixture
78+
tc._swallow_next_tc(grid_width, top_tc)
79+
assert tr.xml == expected_xml
80+
81+
def it_adds_cell_widths_on_swallow(self, add_width_fixture):
82+
tc, grid_width, top_tc, tr, expected_xml = add_width_fixture
83+
tc._swallow_next_tc(grid_width, top_tc)
84+
assert tr.xml == expected_xml
85+
86+
def it_raises_on_invalid_swallow(self, swallow_raise_fixture):
87+
tc, grid_width, top_tc, tr = swallow_raise_fixture
88+
with pytest.raises(InvalidSpanError):
89+
tc._swallow_next_tc(grid_width, top_tc)
90+
7691
def it_can_move_its_content_to_help_merge(self, move_fixture):
7792
tc, tc_2, expected_tc_xml, expected_tc_2_xml = move_fixture
7893
tc._move_content_to(tc_2)
@@ -86,6 +101,32 @@ def it_raises_on_tr_above(self, tr_above_raise_fixture):
86101

87102
# fixtures -------------------------------------------------------
88103

104+
@pytest.fixture(params=[
105+
# both cells have a width
106+
('w:tr/(w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p),'
107+
'w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p))', 0, 2,
108+
'w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=2880,w:type=dxa},'
109+
'w:gridSpan{w:val=2}),w:p))'),
110+
# neither have a width
111+
('w:tr/(w:tc/w:p,w:tc/w:p)', 0, 2,
112+
'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))'),
113+
# only second one has a width
114+
('w:tr/(w:tc/w:p,'
115+
'w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p))', 0, 2,
116+
'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))'),
117+
# only first one has a width
118+
('w:tr/(w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p),'
119+
'w:tc/w:p)', 0, 2,
120+
'w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=1440,w:type=dxa},'
121+
'w:gridSpan{w:val=2}),w:p))'),
122+
])
123+
def add_width_fixture(self, request):
124+
tr_cxml, tc_idx, grid_width, expected_tr_cxml = request.param
125+
tr = element(tr_cxml)
126+
tc = top_tc = tr[tc_idx]
127+
expected_tr_xml = xml(expected_tr_cxml)
128+
return tc, grid_width, top_tc, tr, expected_tr_xml
129+
89130
@pytest.fixture(params=[
90131
(0, 0, 0, 'top', 0), (2, 0, 1, 'top', 0),
91132
(2, 1, 1, 'top', 0), (4, 2, 1, 'top', 1),
@@ -202,6 +243,36 @@ def span_width_fixture(
202243
]
203244
return tc, grid_width, top_tc_, vMerge, expected_calls
204245

246+
@pytest.fixture(params=[
247+
('w:tr/(w:tc/w:p,w:tc/w:p)', 0, 2,
248+
'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))'),
249+
('w:tr/(w:tc/w:p,w:tc/w:p,w:tc/w:p)', 1, 2,
250+
'w:tr/(w:tc/w:p,w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))'),
251+
('w:tr/(w:tc/w:p/w:r/w:t"a",w:tc/w:p/w:r/w:t"b")', 0, 2,
252+
'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p/w:r/w:t"a",'
253+
'w:p/w:r/w:t"b"))'),
254+
('w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p),w:tc/w:p)', 0, 3,
255+
'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=3},w:p))'),
256+
('w:tr/(w:tc/w:p,w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))', 0, 3,
257+
'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=3},w:p))'),
258+
])
259+
def swallow_fixture(self, request):
260+
tr_cxml, tc_idx, grid_width, expected_tr_cxml = request.param
261+
tr = element(tr_cxml)
262+
tc = top_tc = tr[tc_idx]
263+
expected_tr_xml = xml(expected_tr_cxml)
264+
return tc, grid_width, top_tc, tr, expected_tr_xml
265+
266+
@pytest.fixture(params=[
267+
('w:tr/w:tc/w:p', 0, 2),
268+
('w:tr/(w:tc/w:p,w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p))', 0, 2),
269+
])
270+
def swallow_raise_fixture(self, request):
271+
tr_cxml, tc_idx, grid_width = request.param
272+
tr = element(tr_cxml)
273+
tc = top_tc = tr[tc_idx]
274+
return tc, grid_width, top_tc, tr
275+
205276
@pytest.fixture(params=[(0, 0, 0), (4, 0, 0)])
206277
def tr_above_raise_fixture(self, request):
207278
snippet_idx, row_idx, col_idx = request.param

0 commit comments

Comments
 (0)