Table formatting¶
tables/ (v0.5) covers the half of tables python-docx leaves out.
python-docx models table structure well: rows, columns, cells, cell
text, widths, and a working _Cell.merge. It models table
appearance not at all. There is no CT_Border, no CT_TblBorders,
no CT_TcBorders, and no CT_Shd class in the package, and none of
those tags is registered — so a border or fill written by hand
round-trips as an anonymous lxml element.
This is deliberately not part of layout/: every helper
there takes a Section or Document and its docstring scopes it to page
layout.
For the calls, see the tables guide.
Borders and shading¶
Structurally these are set_page_borders again — the same
CT_Border shape from core/borders.py, the same schema-ordered
insertion, the same replace-or-remove idempotence. Tables add the two
inside edges (17.4.39); cells add the two diagonals (17.4.67).
The one non-obvious point is w:space. Border.space defaults to
24, a page value: what Word emits for "Whole document, Box, Default
settings". Word's UI cannot produce a non-zero space on a table border
at all and always writes 0. Reusing the dataclass default blindly
would put a third of an inch between every table edge and its text, so
both writers pin the attribute to 0 and say so.
Row shading needs its own note: CT_TrPr has no w:shd child.
There is no row-level shading in the format. Word implements "shade
this row" by writing the same <w:shd> into every cell, and so does
set_row_shading. It iterates the row's <w:tc> elements rather than
Row.cells, so a cell spanning several grid columns is visited once
rather than once per column it covers.
The two horizontal-merge encodings¶
OOXML can express a horizontal merge two ways:
w:gridSpan(17.4.17) — one<w:tc>widened over several grid columns. This is what Word writes today and the only form python-docx understands.w:hMerge(17.4.22) — one<w:tc>per column, followers marked as continuations. Older Word versions and several converters emit this.
Word renders them identically — verified against Word 2016, where a
converted file rasterises byte-for-byte the same as its original. But
python-docx's grid model ignores hMerge entirely, so Table.cell
hands back cells that look separate and are not. Word's own COM object
model shares the blind spot: it reported six cells for the hMerge
fixture and five after conversion, while laying both out the same way.
normalize_horizontal_merges rewrites the second form as the first.
It refuses by default to drop text held in a continuation cell —
invisible in Word, so keeping it would make hidden content appear and
discarding it silently would lose data.
Note that "has content" cannot be "has a <w:r>": every cell holds at
least one <w:p>, and cell.text = "" leaves an empty run behind, so
that test calls every ordinary cell occupied. The check looks for
non-blank <w:t> text or an embedded object.
Unmerging¶
_Cell.merge is fully implemented in python-docx and is not
re-implemented here — merge_cells only translates InvalidSpanError
into a DocxPlusError subclass per the error
hierarchy. The inverse is what is
missing: nothing in python-docx removes a w:gridSpan or a w:vMerge,
so a merge is one-way.
unmerge_cell resolves the region from any cell in it, including a
vertical continuation, then walks the vertical run before mutating
anything — splitting a cell horizontally shifts the grid offsets of its
right-hand neighbours, so the lookups have to happen first. Widths are
divided evenly, because the individual widths were summed away when the
merge happened and cannot be recovered.
Not covered¶
The cell-formatting cascade (table style → <w:tblStylePr>
conditional branch → direct <w:tcPr>). read_table_formatting
reports direct formatting only, so a Table Grid table reads back with
no borders — true of its XML, not of its appearance. The cascade
resolver covers paragraphs and runs and scopes this out in
the same terms; it is a larger workstream than every writer in this
package put together.