Skip to content

Commit 1f52c20

Browse files
author
Steve Canny
committed
tbl: add CT_Tc._span_dimensions()
1 parent 64ef552 commit 1f52c20

File tree

4 files changed

+154
-7
lines changed

4 files changed

+154
-7
lines changed

docx/exceptions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ class PythonDocxError(Exception):
1313
"""
1414

1515

16+
class InvalidSpanError(PythonDocxError):
17+
"""
18+
Raised when an invalid merge region is specified in a request to merge
19+
table cells.
20+
"""
21+
22+
1623
class InvalidXmlError(PythonDocxError):
1724
"""
1825
Raised when invalid XML is encountered, such as on attempt to access a

docx/oxml/table.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from __future__ import absolute_import, print_function, unicode_literals
88

99
from . import parse_xml
10+
from ..exceptions import InvalidSpanError
1011
from .ns import nsdecls
1112
from ..shared import Emu, Twips
1213
from .simpletypes import (
@@ -365,7 +366,30 @@ def _span_dimensions(self, other_tc):
365366
the merged cell formed by using this tc and *other_tc* as opposite
366367
corner extents.
367368
"""
368-
raise NotImplementedError
369+
def raise_on_inverted_L(a, b):
370+
if a.top == b.top and a.bottom != b.bottom:
371+
raise InvalidSpanError('requested span not rectangular')
372+
if a.left == b.left and a.right != b.right:
373+
raise InvalidSpanError('requested span not rectangular')
374+
375+
def raise_on_tee_shaped(a, b):
376+
top_most, other = (a, b) if a.top < b.top else (b, a)
377+
if top_most.top < other.top and top_most.bottom > other.bottom:
378+
raise InvalidSpanError('requested span not rectangular')
379+
380+
left_most, other = (a, b) if a.left < b.left else (b, a)
381+
if left_most.left < other.left and left_most.right > other.right:
382+
raise InvalidSpanError('requested span not rectangular')
383+
384+
raise_on_inverted_L(self, other_tc)
385+
raise_on_tee_shaped(self, other_tc)
386+
387+
top = min(self.top, other_tc.top)
388+
left = min(self.left, other_tc.left)
389+
bottom = max(self.bottom, other_tc.bottom)
390+
right = max(self.right, other_tc.right)
391+
392+
return top, left, bottom - top, right - left
369393

370394
@property
371395
def _tbl(self):

tests/oxml/test_table.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import pytest
1212

13+
from docx.exceptions import InvalidSpanError
1314
from docx.oxml import parse_xml
1415
from docx.oxml.table import CT_Row, CT_Tc
1516

@@ -50,6 +51,16 @@ def it_knows_its_extents_to_help(self, extents_fixture):
5051
extent = getattr(tc, attr_name)
5152
assert extent == expected_value
5253

54+
def it_calculates_the_dimensions_of_a_span_to_help(self, span_fixture):
55+
tc, other_tc, expected_dimensions = span_fixture
56+
dimensions = tc._span_dimensions(other_tc)
57+
assert dimensions == expected_dimensions
58+
59+
def it_raises_on_invalid_span(self, span_raise_fixture):
60+
tc, other_tc = span_raise_fixture
61+
with pytest.raises(InvalidSpanError):
62+
tc._span_dimensions(other_tc)
63+
5364
def it_raises_on_tr_above(self, tr_above_raise_fixture):
5465
tc = tr_above_raise_fixture
5566
with pytest.raises(ValueError):
@@ -69,7 +80,7 @@ def it_raises_on_tr_above(self, tr_above_raise_fixture):
6980
])
7081
def extents_fixture(self, request):
7182
snippet_idx, row, col, attr_name, expected_value = request.param
72-
tbl = parse_xml(snippet_seq('tbl-cells')[snippet_idx])
83+
tbl = self._snippet_tbl(snippet_idx)
7384
tc = tbl.tr_lst[row].tc_lst[col]
7485
return tc, attr_name, expected_value
7586

@@ -83,6 +94,42 @@ def merge_fixture(
8394
tr_.tc_at_grid_col.return_value = top_tc_
8495
return tc, other_tc, tr_, top_tc_, left, height, width
8596

97+
@pytest.fixture(params=[
98+
(0, 0, 0, 0, 1, (0, 0, 1, 2)),
99+
(0, 0, 1, 2, 1, (0, 1, 3, 1)),
100+
(0, 2, 2, 1, 1, (1, 1, 2, 2)),
101+
(0, 1, 2, 1, 0, (1, 0, 1, 3)),
102+
(1, 0, 0, 1, 1, (0, 0, 2, 2)),
103+
(1, 0, 1, 0, 0, (0, 0, 1, 3)),
104+
(2, 0, 1, 2, 1, (0, 1, 3, 1)),
105+
(2, 0, 1, 1, 0, (0, 0, 2, 2)),
106+
(2, 1, 2, 0, 1, (0, 1, 2, 2)),
107+
(4, 0, 1, 0, 0, (0, 0, 1, 3)),
108+
])
109+
def span_fixture(self, request):
110+
snippet_idx, row, col, row_2, col_2, expected_value = request.param
111+
tbl = self._snippet_tbl(snippet_idx)
112+
tc = tbl.tr_lst[row].tc_lst[col]
113+
tc_2 = tbl.tr_lst[row_2].tc_lst[col_2]
114+
return tc, tc_2, expected_value
115+
116+
@pytest.fixture(params=[
117+
(1, 0, 0, 1, 0), # inverted-L horz
118+
(1, 1, 0, 0, 0), # same in opposite order
119+
(2, 0, 2, 0, 1), # inverted-L vert
120+
(5, 0, 1, 1, 0), # tee-shape horz bar
121+
(5, 1, 0, 2, 1), # same, opposite side
122+
(6, 1, 0, 0, 1), # tee-shape vert bar
123+
(6, 0, 1, 1, 2), # same, opposite side
124+
])
125+
def span_raise_fixture(self, request):
126+
snippet_idx, row, col, row_2, col_2 = request.param
127+
tbl = self._snippet_tbl(snippet_idx)
128+
tc = tbl.tr_lst[row].tc_lst[col]
129+
tc_2 = tbl.tr_lst[row_2].tc_lst[col_2]
130+
print(tc.top, tc_2.top, tc.bottom, tc_2.bottom)
131+
return tc, tc_2
132+
86133
@pytest.fixture(params=[(0, 0, 0), (4, 0, 0)])
87134
def tr_above_raise_fixture(self, request):
88135
snippet_idx, row_idx, col_idx = request.param
@@ -100,6 +147,13 @@ def _grow_to_(self, request):
100147
def _span_dimensions_(self, request):
101148
return method_mock(request, CT_Tc, '_span_dimensions')
102149

150+
def _snippet_tbl(self, idx):
151+
"""
152+
Return a <w:tbl> element for snippet at *idx* in 'tbl-cells' snippet
153+
file.
154+
"""
155+
return parse_xml(snippet_seq('tbl-cells')[idx])
156+
103157
@pytest.fixture
104158
def _tbl_(self, request):
105159
return property_mock(request, CT_Tc, '_tbl')

tests/test_files/snippets/tbl-cells.txt

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!-- 9 x 9 uniform table -->
1+
<!-- 3 x 3 uniform table -->
22
<w:tbl xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
33
<w:tblGrid>
44
<w:gridCol/>
@@ -22,7 +22,7 @@
2222
</w:tr>
2323
</w:tbl>
2424

25-
<!-- 9 x 9 with horizontal span -->
25+
<!-- 3 x 3 with horizontal span -->
2626
<w:tbl xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
2727
<w:tblGrid>
2828
<w:gridCol/>
@@ -49,7 +49,7 @@
4949
</w:tr>
5050
</w:tbl>
5151

52-
<!-- 9 x 9 with vertical span -->
52+
<!-- 3 x 3 with vertical span -->
5353
<w:tbl xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
5454
<w:tblGrid>
5555
<w:gridCol/>
@@ -81,7 +81,7 @@
8181
</w:tr>
8282
</w:tbl>
8383

84-
<!-- 9 x 9 with combo span -->
84+
<!-- 3 x 3 with combo span -->
8585
<w:tbl xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
8686
<w:tblGrid>
8787
<w:gridCol/>
@@ -113,7 +113,7 @@
113113
</w:tr>
114114
</w:tbl>
115115

116-
<!-- 9 x 9 with horz, vert, and combo span -->
116+
<!-- 3 x 3 with horz, vert, and combo span -->
117117
<w:tbl xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
118118
<w:tblGrid>
119119
<w:gridCol/>
@@ -155,3 +155,65 @@
155155
</w:tc>
156156
</w:tr>
157157
</w:tbl>
158+
159+
<!-- 3 x 3 with middle row spanned -->
160+
<w:tbl xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
161+
<w:tblGrid>
162+
<w:gridCol/>
163+
<w:gridCol/>
164+
<w:gridCol/>
165+
</w:tblGrid>
166+
<w:tr>
167+
<w:tc/>
168+
<w:tc/>
169+
<w:tc/>
170+
</w:tr>
171+
<w:tr>
172+
<w:tc>
173+
<w:tcPr>
174+
<w:gridSpan w:val="3"/>
175+
</w:tcPr>
176+
</w:tc>
177+
</w:tr>
178+
<w:tr>
179+
<w:tc/>
180+
<w:tc/>
181+
<w:tc/>
182+
</w:tr>
183+
</w:tbl>
184+
185+
<!-- 3 x 3 with middle column spanned -->
186+
<w:tbl xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
187+
<w:tblGrid>
188+
<w:gridCol/>
189+
<w:gridCol/>
190+
<w:gridCol/>
191+
</w:tblGrid>
192+
<w:tr>
193+
<w:tc/>
194+
<w:tc>
195+
<w:tcPr>
196+
<w:vMerge w:val="restart"/>
197+
</w:tcPr>
198+
</w:tc>
199+
<w:tc/>
200+
</w:tr>
201+
<w:tr>
202+
<w:tc/>
203+
<w:tc>
204+
<w:tcPr>
205+
<w:vMerge w:val="continue"/>
206+
</w:tcPr>
207+
</w:tc>
208+
<w:tc/>
209+
</w:tr>
210+
<w:tr>
211+
<w:tc/>
212+
<w:tc>
213+
<w:tcPr>
214+
<w:vMerge w:val="continue"/>
215+
</w:tcPr>
216+
</w:tc>
217+
<w:tc/>
218+
</w:tr>
219+
</w:tbl>

0 commit comments

Comments
 (0)