Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/docx/oxml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
register_element_cls("w:tc", CT_Tc)
register_element_cls("w:tcPr", CT_TcPr)
register_element_cls("w:tcW", CT_TblWidth)
register_element_cls("w:tblInd", CT_TblWidth)
register_element_cls("w:tr", CT_Row)
register_element_cls("w:trHeight", CT_Height)
register_element_cls("w:trPr", CT_TrPr)
Expand Down
18 changes: 18 additions & 0 deletions src/docx/oxml/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,10 +300,12 @@ class CT_TblPr(BaseOxmlElement):

get_or_add_bidiVisual: Callable[[], CT_OnOff]
get_or_add_jc: Callable[[], CT_Jc]
get_or_add_tblInd: Callable[[], CT_TblWidth]
get_or_add_tblLayout: Callable[[], CT_TblLayoutType]
_add_tblStyle: Callable[[], CT_String]
_remove_bidiVisual: Callable[[], None]
_remove_jc: Callable[[], None]
_remove_tblInd: Callable[[], None]
_remove_tblStyle: Callable[[], None]

_tag_seq = (
Expand Down Expand Up @@ -335,6 +337,9 @@ class CT_TblPr(BaseOxmlElement):
jc: CT_Jc | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"w:jc", successors=_tag_seq[8:]
)
tblInd: CT_TblWidth | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"w:tblInd", successors=_tag_seq[10:]
)
tblLayout: CT_TblLayoutType | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"w:tblLayout", successors=_tag_seq[13:]
)
Expand All @@ -356,6 +361,19 @@ def alignment(self, value: WD_TABLE_ALIGNMENT | None):
jc = self.get_or_add_jc()
jc.val = cast("WD_ALIGN_PARAGRAPH", value)

@property
def left_indent(self) -> Length | None:
"""Direct table indentation, or |None| when inherited or not expressed in twips."""
tbl_ind = self.tblInd
return None if tbl_ind is None else tbl_ind.width

@left_indent.setter
def left_indent(self, value: Length | None) -> None:
if value is None:
self._remove_tblInd()
else:
self.get_or_add_tblInd().width = value

@property
def autofit(self) -> bool:
"""|False| when there is a `w:tblLayout` child with `@w:type="fixed"`.
Expand Down
13 changes: 13 additions & 0 deletions src/docx/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,19 @@ def alignment(self) -> WD_TABLE_ALIGNMENT | None:
def alignment(self, value: WD_TABLE_ALIGNMENT | None):
self._tblPr.alignment = value

@property
def left_indent(self) -> Length | None:
"""Direct distance from the table's left edge to the page margin.

|None| means the indentation is inherited from the table style. This setting
applies to left-aligned tables. Assign |None| to remove the direct setting.
"""
return self._tblPr.left_indent

@left_indent.setter
def left_indent(self, value: Length | None) -> None:
self._tblPr.left_indent = value

@property
def autofit(self) -> bool:
"""|True| if column widths can be automatically adjusted to improve the fit of
Expand Down
31 changes: 31 additions & 0 deletions tests/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,37 @@ def it_can_change_its_alignment_setting(
table.alignment = new_value
assert table._tbl.xml == xml(expected_cxml)

@pytest.mark.parametrize(
("tbl_cxml", "expected"),
[
("w:tbl/w:tblPr", None),
("w:tbl/w:tblPr/w:tblInd{w:type=dxa,w:w=720}", Inches(0.5)),
("w:tbl/w:tblPr/w:tblInd{w:type=pct,w:w=5000}", None),
],
)
def it_knows_its_left_indent(self, tbl_cxml: str, expected: Length | None, document_: Mock):
table = Table(cast(CT_Tbl, element(tbl_cxml)), document_)
assert table.left_indent == expected

@pytest.mark.parametrize(
("tbl_cxml", "value", "expected_cxml"),
[
("w:tbl/w:tblPr", Inches(0.5), "w:tbl/w:tblPr/w:tblInd{w:type=dxa,w:w=720}"),
(
"w:tbl/w:tblPr/w:tblInd{w:type=dxa,w:w=720}",
Inches(1),
"w:tbl/w:tblPr/w:tblInd{w:type=dxa,w:w=1440}",
),
("w:tbl/w:tblPr/w:tblInd{w:type=dxa,w:w=720}", None, "w:tbl/w:tblPr"),
],
)
def it_can_change_its_left_indent(
self, tbl_cxml: str, value: Length | None, expected_cxml: str, document_: Mock
):
table = Table(cast(CT_Tbl, element(tbl_cxml)), document_)
table.left_indent = value
assert table._tbl.xml == xml(expected_cxml)

@pytest.mark.parametrize(
("tbl_cxml", "expected_value"),
[
Expand Down
34 changes: 34 additions & 0 deletions tests/test_table_left_indent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Round-trip behavior for direct table indentation."""

from __future__ import annotations

from io import BytesIO

from docx import Document
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.shared import Inches


def test_table_left_indent_round_trips_without_changing_other_properties():
document = Document()
table = document.add_table(rows=2, cols=2)
table.style = "Table Grid"
table.alignment = WD_TABLE_ALIGNMENT.LEFT
table.rows[0].cells[0].text = "Header"
table.rows[1].cells[0].text = "Body"
table.left_indent = Inches(0.75)

stream = BytesIO()
document.save(stream)
stream.seek(0)
reopened_document = Document(stream)
reopened = reopened_document.tables[0]
assert reopened.left_indent == Inches(0.75)
assert reopened.alignment == WD_TABLE_ALIGNMENT.LEFT
assert reopened.rows[1].cells[0].text == "Body"

reopened.left_indent = None
stream = BytesIO()
reopened_document.save(stream)
stream.seek(0)
assert Document(stream).tables[0].left_indent is None