Skip to content

Commit 64ef552

Browse files
author
Steve Canny
committed
tbl: add CT_Tc.top, .left, .bottom, and .right
1 parent 538ed19 commit 64ef552

File tree

2 files changed

+167
-2
lines changed

2 files changed

+167
-2
lines changed

docx/oxml/table.py

Lines changed: 115 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,14 @@ def tc_at_grid_col(self, idx):
2929
The ``<w:tc>`` element appearing at grid column *idx*. Raises
3030
|ValueError| if no ``w:tc`` element begins at that grid column.
3131
"""
32-
raise NotImplementedError
32+
grid_col = 0
33+
for tc in self.tc_lst:
34+
if grid_col == idx:
35+
return tc
36+
grid_col += tc.grid_span
37+
if grid_col > idx:
38+
raise ValueError('no cell on grid column %d' % idx)
39+
raise ValueError('index out of bounds')
3340

3441
@property
3542
def tr_idx(self):
@@ -207,6 +214,20 @@ class CT_Tc(BaseOxmlElement):
207214
p = OneOrMore('w:p')
208215
tbl = OneOrMore('w:tbl')
209216

217+
@property
218+
def bottom(self):
219+
"""
220+
The row index that marks the bottom extent of the vertical span of
221+
this cell. This is one greater than the index of the bottom-most row
222+
of the span, similar to how a slice of the cell's rows would be
223+
specified.
224+
"""
225+
if self.vMerge is not None:
226+
tc_below = self._tc_below
227+
if tc_below is not None and tc_below.vMerge == ST_Merge.CONTINUE:
228+
return tc_below.bottom
229+
return self._tr_idx + 1
230+
210231
def clear_content(self):
211232
"""
212233
Remove all content child elements, preserving the ``<w:tcPr>``
@@ -232,6 +253,13 @@ def grid_span(self):
232253
return 1
233254
return tcPr.grid_span
234255

256+
@property
257+
def left(self):
258+
"""
259+
The grid column index at which this ``<w:tc>`` element appears.
260+
"""
261+
return self._grid_col
262+
235263
def merge(self, other_tc):
236264
"""
237265
Return the top-left ``<w:tc>`` element of a new span formed by
@@ -255,6 +283,25 @@ def new(cls):
255283
'</w:tc>' % nsdecls('w')
256284
)
257285

286+
@property
287+
def right(self):
288+
"""
289+
The grid column index that marks the right-side extent of the
290+
horizontal span of this cell. This is one greater than the index of
291+
the right-most column of the span, similar to how a slice of the
292+
cell's columns would be specified.
293+
"""
294+
return self._grid_col + self.grid_span
295+
296+
@property
297+
def top(self):
298+
"""
299+
The top-most row index in the vertical span of this cell.
300+
"""
301+
if self.vMerge is None or self.vMerge == ST_Merge.RESTART:
302+
return self._tr_idx
303+
return self._tc_above.top
304+
258305
@property
259306
def vMerge(self):
260307
"""
@@ -282,6 +329,16 @@ def width(self, value):
282329
tcPr = self.get_or_add_tcPr()
283330
tcPr.width = value
284331

332+
@property
333+
def _grid_col(self):
334+
"""
335+
The grid column at which this cell begins.
336+
"""
337+
tr = self._tr
338+
idx = tr.tc_lst.index(self)
339+
preceding_tcs = tr.tc_lst[:idx]
340+
return sum(tc.grid_span for tc in preceding_tcs)
341+
285342
def _grow_to(self, width, height, top_tc=None):
286343
"""
287344
Grow this cell to *width* grid columns and *height* rows by expanding
@@ -315,7 +372,63 @@ def _tbl(self):
315372
"""
316373
The tbl element this tc element appears in.
317374
"""
318-
raise NotImplementedError
375+
return self.xpath('./ancestor::w:tbl')[0]
376+
377+
@property
378+
def _tc_above(self):
379+
"""
380+
The `w:tc` element immediately above this one in its grid column.
381+
"""
382+
return self._tr_above.tc_at_grid_col(self._grid_col)
383+
384+
@property
385+
def _tc_below(self):
386+
"""
387+
The tc element immediately below this one in its grid column.
388+
"""
389+
tr_below = self._tr_below
390+
if tr_below is None:
391+
return None
392+
return tr_below.tc_at_grid_col(self._grid_col)
393+
394+
@property
395+
def _tr(self):
396+
"""
397+
The tr element this tc element appears in.
398+
"""
399+
return self.xpath('./ancestor::w:tr')[0]
400+
401+
@property
402+
def _tr_above(self):
403+
"""
404+
The tr element prior in sequence to the tr this cell appears in.
405+
Raises |ValueError| if called on a cell in the top-most row.
406+
"""
407+
tr_lst = self._tbl.tr_lst
408+
tr_idx = tr_lst.index(self._tr)
409+
if tr_idx == 0:
410+
raise ValueError('no tr above topmost tr')
411+
return tr_lst[tr_idx-1]
412+
413+
@property
414+
def _tr_below(self):
415+
"""
416+
The tr element next in sequence after the tr this cell appears in, or
417+
|None| if this cell appears in the last row.
418+
"""
419+
tr_lst = self._tbl.tr_lst
420+
tr_idx = tr_lst.index(self._tr)
421+
try:
422+
return tr_lst[tr_idx+1]
423+
except IndexError:
424+
return None
425+
426+
@property
427+
def _tr_idx(self):
428+
"""
429+
The row index of the tr element this tc element appears in.
430+
"""
431+
return self._tbl.tr_lst.index(self._tr)
319432

320433

321434
class CT_TcPr(BaseOxmlElement):

tests/oxml/test_table.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,31 @@
1010

1111
import pytest
1212

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

1516
from ..unitutil.cxml import element
17+
from ..unitutil.file import snippet_seq
1618
from ..unitutil.mock import instance_mock, method_mock, property_mock
1719

1820

21+
class DescribeCT_Row(object):
22+
23+
def it_raises_on_tc_at_grid_col(self, tc_raise_fixture):
24+
tr, idx = tc_raise_fixture
25+
with pytest.raises(ValueError):
26+
tr.tc_at_grid_col(idx)
27+
28+
# fixtures -------------------------------------------------------
29+
30+
@pytest.fixture(params=[(0, 0, 3), (1, 0, 1)])
31+
def tc_raise_fixture(self, request):
32+
snippet_idx, row_idx, col_idx = request.param
33+
tbl = parse_xml(snippet_seq('tbl-cells')[snippet_idx])
34+
tr = tbl.tr_lst[row_idx]
35+
return tr, col_idx
36+
37+
1938
class DescribeCT_Tc(object):
2039

2140
def it_can_merge_to_another_tc(self, merge_fixture):
@@ -26,8 +45,34 @@ def it_can_merge_to_another_tc(self, merge_fixture):
2645
top_tc_._grow_to.assert_called_once_with(width, height)
2746
assert merged_tc is top_tc_
2847

48+
def it_knows_its_extents_to_help(self, extents_fixture):
49+
tc, attr_name, expected_value = extents_fixture
50+
extent = getattr(tc, attr_name)
51+
assert extent == expected_value
52+
53+
def it_raises_on_tr_above(self, tr_above_raise_fixture):
54+
tc = tr_above_raise_fixture
55+
with pytest.raises(ValueError):
56+
tc._tr_above
57+
2958
# fixtures -------------------------------------------------------
3059

60+
@pytest.fixture(params=[
61+
(0, 0, 0, 'top', 0), (2, 0, 1, 'top', 0),
62+
(2, 1, 1, 'top', 0), (4, 2, 1, 'top', 1),
63+
(0, 0, 0, 'left', 0), (1, 0, 1, 'left', 2),
64+
(3, 1, 0, 'left', 0), (3, 1, 1, 'left', 2),
65+
(0, 0, 0, 'bottom', 1), (1, 0, 0, 'bottom', 1),
66+
(2, 0, 1, 'bottom', 2), (4, 1, 1, 'bottom', 3),
67+
(0, 0, 0, 'right', 1), (1, 0, 0, 'right', 2),
68+
(0, 0, 0, 'right', 1), (4, 2, 1, 'right', 3),
69+
])
70+
def extents_fixture(self, request):
71+
snippet_idx, row, col, attr_name, expected_value = request.param
72+
tbl = parse_xml(snippet_seq('tbl-cells')[snippet_idx])
73+
tc = tbl.tr_lst[row].tc_lst[col]
74+
return tc, attr_name, expected_value
75+
3176
@pytest.fixture
3277
def merge_fixture(
3378
self, tr_, _span_dimensions_, _tbl_, _grow_to_, top_tc_):
@@ -38,6 +83,13 @@ def merge_fixture(
3883
tr_.tc_at_grid_col.return_value = top_tc_
3984
return tc, other_tc, tr_, top_tc_, left, height, width
4085

86+
@pytest.fixture(params=[(0, 0, 0), (4, 0, 0)])
87+
def tr_above_raise_fixture(self, request):
88+
snippet_idx, row_idx, col_idx = request.param
89+
tbl = parse_xml(snippet_seq('tbl-cells')[snippet_idx])
90+
tc = tbl.tr_lst[row_idx].tc_lst[col_idx]
91+
return tc
92+
4193
# fixture components ---------------------------------------------
4294

4395
@pytest.fixture

0 commit comments

Comments
 (0)