python-pptx2 2.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. pptx2/__init__.py +152 -0
  2. pptx2/_color.py +75 -0
  3. pptx2/_slide_importer.py +597 -0
  4. pptx2/_svg.py +155 -0
  5. pptx2/_template_applier.py +292 -0
  6. pptx2/_textstyle.py +187 -0
  7. pptx2/accessibility.py +365 -0
  8. pptx2/action.py +270 -0
  9. pptx2/animation.py +2237 -0
  10. pptx2/api.py +49 -0
  11. pptx2/audit.py +258 -0
  12. pptx2/chart/__init__.py +0 -0
  13. pptx2/chart/analytics.py +381 -0
  14. pptx2/chart/axis.py +543 -0
  15. pptx2/chart/category.py +200 -0
  16. pptx2/chart/chart.py +670 -0
  17. pptx2/chart/data.py +864 -0
  18. pptx2/chart/datalabel.py +406 -0
  19. pptx2/chart/legend.py +86 -0
  20. pptx2/chart/marker.py +70 -0
  21. pptx2/chart/palettes.py +129 -0
  22. pptx2/chart/plot.py +462 -0
  23. pptx2/chart/point.py +101 -0
  24. pptx2/chart/quick_layouts.py +325 -0
  25. pptx2/chart/series.py +334 -0
  26. pptx2/chart/xlsx.py +272 -0
  27. pptx2/chart/xmlwriter.py +1845 -0
  28. pptx2/compose/__init__.py +28 -0
  29. pptx2/compose/from_spec.py +1094 -0
  30. pptx2/design/__init__.py +8 -0
  31. pptx2/design/components.py +607 -0
  32. pptx2/design/figures.py +389 -0
  33. pptx2/design/layout.py +370 -0
  34. pptx2/design/recipes.py +1967 -0
  35. pptx2/design/style.py +209 -0
  36. pptx2/design/tokens.py +915 -0
  37. pptx2/diagrams.py +754 -0
  38. pptx2/dml/__init__.py +0 -0
  39. pptx2/dml/chtfmt.py +40 -0
  40. pptx2/dml/color.py +496 -0
  41. pptx2/dml/effect.py +909 -0
  42. pptx2/dml/fill.py +691 -0
  43. pptx2/dml/line.py +287 -0
  44. pptx2/dml/picture.py +212 -0
  45. pptx2/dml/three_d.py +381 -0
  46. pptx2/enum/__init__.py +0 -0
  47. pptx2/enum/action.py +71 -0
  48. pptx2/enum/animation.py +31 -0
  49. pptx2/enum/base.py +218 -0
  50. pptx2/enum/chart.py +574 -0
  51. pptx2/enum/dml.py +740 -0
  52. pptx2/enum/lang.py +685 -0
  53. pptx2/enum/presentation.py +133 -0
  54. pptx2/enum/shapes.py +1029 -0
  55. pptx2/enum/text.py +230 -0
  56. pptx2/exc.py +42 -0
  57. pptx2/formats.py +139 -0
  58. pptx2/geometry.py +420 -0
  59. pptx2/inherit.py +109 -0
  60. pptx2/lint.py +2256 -0
  61. pptx2/math.py +177 -0
  62. pptx2/media.py +197 -0
  63. pptx2/opc/__init__.py +0 -0
  64. pptx2/opc/constants.py +332 -0
  65. pptx2/opc/oxml.py +188 -0
  66. pptx2/opc/package.py +762 -0
  67. pptx2/opc/packuri.py +109 -0
  68. pptx2/opc/serialized.py +296 -0
  69. pptx2/opc/shared.py +20 -0
  70. pptx2/opc/spec.py +45 -0
  71. pptx2/oxml/__init__.py +555 -0
  72. pptx2/oxml/action.py +53 -0
  73. pptx2/oxml/chart/__init__.py +0 -0
  74. pptx2/oxml/chart/axis.py +337 -0
  75. pptx2/oxml/chart/chart.py +481 -0
  76. pptx2/oxml/chart/datalabel.py +253 -0
  77. pptx2/oxml/chart/legend.py +72 -0
  78. pptx2/oxml/chart/marker.py +61 -0
  79. pptx2/oxml/chart/plot.py +365 -0
  80. pptx2/oxml/chart/series.py +425 -0
  81. pptx2/oxml/chart/shared.py +220 -0
  82. pptx2/oxml/coreprops.py +288 -0
  83. pptx2/oxml/dml/__init__.py +0 -0
  84. pptx2/oxml/dml/color.py +135 -0
  85. pptx2/oxml/dml/effect.py +213 -0
  86. pptx2/oxml/dml/fill.py +316 -0
  87. pptx2/oxml/dml/line.py +12 -0
  88. pptx2/oxml/dml/three_d.py +110 -0
  89. pptx2/oxml/ns.py +135 -0
  90. pptx2/oxml/presentation.py +313 -0
  91. pptx2/oxml/shapes/__init__.py +19 -0
  92. pptx2/oxml/shapes/autoshape.py +467 -0
  93. pptx2/oxml/shapes/connector.py +107 -0
  94. pptx2/oxml/shapes/graphfrm.py +347 -0
  95. pptx2/oxml/shapes/groupshape.py +329 -0
  96. pptx2/oxml/shapes/picture.py +270 -0
  97. pptx2/oxml/shapes/shared.py +577 -0
  98. pptx2/oxml/simpletypes.py +1027 -0
  99. pptx2/oxml/slide.py +563 -0
  100. pptx2/oxml/table.py +650 -0
  101. pptx2/oxml/text.py +815 -0
  102. pptx2/oxml/theme.py +36 -0
  103. pptx2/oxml/xmlchemy.py +717 -0
  104. pptx2/package.py +222 -0
  105. pptx2/parts/__init__.py +0 -0
  106. pptx2/parts/chart.py +95 -0
  107. pptx2/parts/coreprops.py +167 -0
  108. pptx2/parts/diagram.py +37 -0
  109. pptx2/parts/embeddedpackage.py +93 -0
  110. pptx2/parts/image.py +275 -0
  111. pptx2/parts/media.py +37 -0
  112. pptx2/parts/presentation.py +136 -0
  113. pptx2/parts/slide.py +371 -0
  114. pptx2/presentation.py +408 -0
  115. pptx2/py.typed +0 -0
  116. pptx2/render.py +586 -0
  117. pptx2/section.py +272 -0
  118. pptx2/shapes/__init__.py +26 -0
  119. pptx2/shapes/autoshape.py +442 -0
  120. pptx2/shapes/base.py +1078 -0
  121. pptx2/shapes/connector.py +297 -0
  122. pptx2/shapes/freeform.py +337 -0
  123. pptx2/shapes/graphfrm.py +316 -0
  124. pptx2/shapes/group.py +264 -0
  125. pptx2/shapes/picture.py +422 -0
  126. pptx2/shapes/placeholder.py +468 -0
  127. pptx2/shapes/shapetree.py +2027 -0
  128. pptx2/shared.py +82 -0
  129. pptx2/skill/SKILL.md +450 -0
  130. pptx2/skill/__init__.py +78 -0
  131. pptx2/skill/__main__.py +64 -0
  132. pptx2/skill/references/animations.md +189 -0
  133. pptx2/skill/references/basics.md +421 -0
  134. pptx2/skill/references/charts.md +254 -0
  135. pptx2/skill/references/compose.md +234 -0
  136. pptx2/skill/references/design.md +366 -0
  137. pptx2/skill/references/effects.md +249 -0
  138. pptx2/skill/references/end-to-end-deck.md +231 -0
  139. pptx2/skill/references/geometry-and-arrows.md +334 -0
  140. pptx2/skill/references/lint.md +275 -0
  141. pptx2/skill/references/math.md +86 -0
  142. pptx2/skill/references/picture-effects.md +129 -0
  143. pptx2/skill/references/render.md +151 -0
  144. pptx2/skill/references/smart-art.md +75 -0
  145. pptx2/skill/references/space-aware-authoring.md +249 -0
  146. pptx2/skill/references/tables.md +244 -0
  147. pptx2/skill/references/theme.md +127 -0
  148. pptx2/skill/references/three-d.md +109 -0
  149. pptx2/skill/references/transitions.md +100 -0
  150. pptx2/slide.py +1244 -0
  151. pptx2/smart_art.py +220 -0
  152. pptx2/spec.py +633 -0
  153. pptx2/table.py +1181 -0
  154. pptx2/table_styles.py +184 -0
  155. pptx2/templates/default.pptx +0 -0
  156. pptx2/templates/docx-icon.emf +0 -0
  157. pptx2/templates/generic-icon.emf +0 -0
  158. pptx2/templates/notes.xml +23 -0
  159. pptx2/templates/notesMaster.xml +352 -0
  160. pptx2/templates/pptx-icon.emf +0 -0
  161. pptx2/templates/theme.xml +321 -0
  162. pptx2/templates/xlsx-icon.emf +0 -0
  163. pptx2/text/__init__.py +0 -0
  164. pptx2/text/fonts.py +482 -0
  165. pptx2/text/layout.py +374 -0
  166. pptx2/text/text.py +1272 -0
  167. pptx2/theme.py +721 -0
  168. pptx2/types.py +36 -0
  169. pptx2/util.py +263 -0
  170. python_pptx2-2.13.0.dist-info/METADATA +351 -0
  171. python_pptx2-2.13.0.dist-info/RECORD +175 -0
  172. python_pptx2-2.13.0.dist-info/WHEEL +5 -0
  173. python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
  174. python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
  175. python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/table.py ADDED
@@ -0,0 +1,1181 @@
1
+ """Table-related objects such as Table and Cell."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any, Iterable, Iterator, Sequence, Union
6
+
7
+ from pptx2._color import coerce_color
8
+ from pptx2._textstyle import (
9
+ apply_body_defaults,
10
+ apply_text_style,
11
+ coerce_anchor,
12
+ coerce_length,
13
+ )
14
+ from pptx2.dml.fill import FillFormat
15
+ from pptx2.dml.line import LineFormat
16
+ from pptx2.oxml.table import TcRange
17
+ from pptx2.shapes import Subshape
18
+ from pptx2.text.text import TextFrame
19
+ from pptx2.util import Emu, lazyproperty
20
+
21
+ if TYPE_CHECKING:
22
+ from pptx2.dml.color import RGBColor
23
+ from pptx2.enum.text import MSO_VERTICAL_ANCHOR
24
+ from pptx2.oxml.shapes.shared import CT_LineProperties
25
+ from pptx2.oxml.table import CT_Table, CT_TableCell, CT_TableCellProperties, CT_TableCol, CT_TableRow
26
+ from pptx2.parts.slide import BaseSlidePart
27
+ from pptx2.shapes.graphfrm import GraphicFrame
28
+ from pptx2.types import ProvidesPart
29
+ from pptx2.util import Length
30
+
31
+ # A colour accepted anywhere in the library: hex string, (r, g, b) tuple,
32
+ # or RGBColor. Matches what LineFormat.color.rgb coercion accepts.
33
+ _ColorLike = str | tuple[int, int, int] | RGBColor
34
+ else:
35
+ # Runtime fallback so ``typing.get_type_hints()`` on the public border
36
+ # helpers resolves ``_ColorLike`` instead of raising ``NameError`` (the
37
+ # precise union above is type-checker-only, since RGBColor is not imported
38
+ # at runtime here).
39
+ _ColorLike = Any
40
+
41
+
42
+ #: What `Table.format_cells` accepts for its `rows` / `cols` arguments.
43
+ _CellSelector = Union[None, int, slice, Iterable[int]]
44
+
45
+
46
+ def _resolve_selector(selector: _CellSelector, count: int, name: str) -> list[int]:
47
+ """Return the concrete indices `selector` picks out of `count` rows/columns.
48
+
49
+ ``None`` means all of them; an ``int`` picks one (negative counts from the
50
+ end); an iterable of ints picks several. An out-of-range index raises
51
+ :class:`IndexError` rather than silently selecting nothing — a typo'd row
52
+ number should not read as "styled zero cells, all good".
53
+
54
+ A ``slice`` is the exception: it follows ordinary Python slicing, so
55
+ ``slice(1, None)`` on a one-row table yields no cells rather than raising,
56
+ the same as ``rows[1:]`` would.
57
+ """
58
+ if selector is None:
59
+ return list(range(count))
60
+ if isinstance(selector, slice):
61
+ return list(range(count))[selector]
62
+ idxs = [selector] if isinstance(selector, int) else list(selector)
63
+ out: list[int] = []
64
+ for idx in idxs:
65
+ i = int(idx)
66
+ if i < 0:
67
+ i += count
68
+ if not 0 <= i < count:
69
+ raise IndexError(f"{name} index {idx} out of range for table with {count} {name}")
70
+ out.append(i)
71
+ return out
72
+
73
+
74
+ def _apply_cell_margins(
75
+ cell: "_Cell", margin: "float | Length | Sequence[float | Length]"
76
+ ) -> None:
77
+ """Set a cell's four insets from a scalar or ``(top, right, bottom, left)``."""
78
+ if isinstance(margin, (tuple, list)):
79
+ if len(margin) != 4:
80
+ raise ValueError(
81
+ "margin tuple must have 4 elements (top, right, bottom, left); "
82
+ f"got {len(margin)}"
83
+ )
84
+ top, right, bottom, left = (coerce_length(v) for v in margin)
85
+ else:
86
+ top = right = bottom = left = coerce_length(margin)
87
+ cell.margin_top, cell.margin_right = top, right
88
+ cell.margin_bottom, cell.margin_left = bottom, left
89
+
90
+
91
+ class Table(object):
92
+ """A DrawingML table object.
93
+
94
+ Not intended to be constructed directly, use
95
+ :meth:`.Slide.shapes.add_table` to add a table to a slide.
96
+ """
97
+
98
+ def __init__(self, tbl: CT_Table, graphic_frame: GraphicFrame):
99
+ super(Table, self).__init__()
100
+ self._tbl = tbl
101
+ self._graphic_frame = graphic_frame
102
+
103
+ def cell(self, row_idx: int, col_idx: int) -> _Cell:
104
+ """Return cell at `row_idx`, `col_idx`.
105
+
106
+ Return value is an instance of |_Cell|. `row_idx` and `col_idx` are zero-based, e.g.
107
+ cell(0, 0) is the top, left cell in the table.
108
+ """
109
+ return _Cell(self._tbl.tc(row_idx, col_idx), self)
110
+
111
+ @lazyproperty
112
+ def columns(self) -> _ColumnCollection:
113
+ """|_ColumnCollection| instance for this table.
114
+
115
+ Provides access to |_Column| objects representing the table's columns. |_Column| objects
116
+ are accessed using list notation, e.g. `col = tbl.columns[0]`.
117
+ """
118
+ return _ColumnCollection(self._tbl, self)
119
+
120
+ @property
121
+ def first_col(self) -> bool:
122
+ """When `True`, indicates first column should have distinct formatting.
123
+
124
+ Read/write. Distinct formatting is used, for example, when the first column contains row
125
+ headings (is a side-heading column).
126
+ """
127
+ return self._tbl.firstCol
128
+
129
+ @first_col.setter
130
+ def first_col(self, value: bool):
131
+ self._tbl.firstCol = value
132
+
133
+ @property
134
+ def first_row(self) -> bool:
135
+ """When `True`, indicates first row should have distinct formatting.
136
+
137
+ Read/write. Distinct formatting is used, for example, when the first row contains column
138
+ headings.
139
+ """
140
+ return self._tbl.firstRow
141
+
142
+ @first_row.setter
143
+ def first_row(self, value: bool):
144
+ self._tbl.firstRow = value
145
+
146
+ @property
147
+ def horz_banding(self) -> bool:
148
+ """When `True`, indicates rows should have alternating shading.
149
+
150
+ Read/write. Used to allow rows to be traversed more easily without losing track of which
151
+ row is being read.
152
+ """
153
+ return self._tbl.bandRow
154
+
155
+ @horz_banding.setter
156
+ def horz_banding(self, value: bool):
157
+ self._tbl.bandRow = value
158
+
159
+ # Friendlier aliases — match the OOXML ``bandRow`` / ``bandCol``
160
+ # vocabulary that PowerPoint's UI uses ("banded rows / columns").
161
+ @property
162
+ def banded_rows(self) -> bool:
163
+ """Alias for :attr:`horz_banding` — alternating row shading."""
164
+ return self._tbl.bandRow
165
+
166
+ @banded_rows.setter
167
+ def banded_rows(self, value: bool):
168
+ self._tbl.bandRow = value
169
+
170
+ @property
171
+ def banded_cols(self) -> bool:
172
+ """Alias for :attr:`vert_banding` — alternating column shading."""
173
+ return self._tbl.bandCol
174
+
175
+ @banded_cols.setter
176
+ def banded_cols(self, value: bool):
177
+ self._tbl.bandCol = value
178
+
179
+ def iter_cells(self) -> Iterator[_Cell]:
180
+ """Generate _Cell object for each cell in this table.
181
+
182
+ Each grid cell is generated in left-to-right, top-to-bottom order.
183
+ """
184
+ return (_Cell(tc, self) for tc in self._tbl.iter_tcs())
185
+
186
+ @property
187
+ def last_col(self) -> bool:
188
+ """When `True`, indicates the rightmost column should have distinct formatting.
189
+
190
+ Read/write. Used, for example, when a row totals column appears at the far right of the
191
+ table.
192
+ """
193
+ return self._tbl.lastCol
194
+
195
+ @last_col.setter
196
+ def last_col(self, value: bool):
197
+ self._tbl.lastCol = value
198
+
199
+ @property
200
+ def last_row(self) -> bool:
201
+ """When `True`, indicates the bottom row should have distinct formatting.
202
+
203
+ Read/write. Used, for example, when a totals row appears as the bottom row.
204
+ """
205
+ return self._tbl.lastRow
206
+
207
+ @last_row.setter
208
+ def last_row(self, value: bool):
209
+ self._tbl.lastRow = value
210
+
211
+ def notify_height_changed(self) -> None:
212
+ """Called by a row when its height changes.
213
+
214
+ Triggers the graphic frame to recalculate its total height (as the sum of the row
215
+ heights).
216
+ """
217
+ new_table_height = Emu(sum([row.height for row in self.rows]))
218
+ self._graphic_frame.height = new_table_height
219
+
220
+ def notify_width_changed(self) -> None:
221
+ """Called by a column when its width changes.
222
+
223
+ Triggers the graphic frame to recalculate its total width (as the sum of the column
224
+ widths).
225
+ """
226
+ new_table_width = Emu(sum([col.width for col in self.columns]))
227
+ self._graphic_frame.width = new_table_width
228
+
229
+ @property
230
+ def part(self) -> BaseSlidePart:
231
+ """The package part containing this table."""
232
+ return self._graphic_frame.part
233
+
234
+ @lazyproperty
235
+ def rows(self):
236
+ """|_RowCollection| instance for this table.
237
+
238
+ Provides access to |_Row| objects representing the table's rows. |_Row| objects are
239
+ accessed using list notation, e.g. `col = tbl.rows[0]`.
240
+ """
241
+ return _RowCollection(self._tbl, self)
242
+
243
+ def fit_to_box(
244
+ self,
245
+ *,
246
+ font_family: str = "Calibri",
247
+ max_font_pt: int = 18,
248
+ min_font_pt: int = 8,
249
+ bold: bool = False,
250
+ italic: bool = False,
251
+ font_file: str | None = None,
252
+ ) -> int:
253
+ """Shrink cell text font size until every cell fits within its bounds.
254
+
255
+ Walks every populated cell, computes the per-cell best-fit font
256
+ size against the cell's *own* width and row height (margins
257
+ respected), and applies the **smallest** of those sizes uniformly
258
+ to every cell — so the table reads as a single coherent grid
259
+ rather than each cell at its own size.
260
+
261
+ Returns the chosen size in points (clamped to ``min_font_pt``).
262
+
263
+ Useful for runtime-driven tables where row counts and string
264
+ lengths aren't known up front.
265
+
266
+ Parameters mirror :meth:`TextFrame.fit_text`.
267
+ """
268
+ from pptx2.text.fonts import find_font_file
269
+ from pptx2.text.layout import TextFitter
270
+ from pptx2.util import Emu, Pt
271
+
272
+ if min_font_pt <= 0 or max_font_pt < min_font_pt:
273
+ raise ValueError(
274
+ "min_font_pt must be > 0 and max_font_pt must be >= min_font_pt"
275
+ )
276
+
277
+ if font_file is None:
278
+ font_file = find_font_file(font_family, bold, italic)
279
+
280
+ # Default cell margins per OOXML: 0.1" left/right, 0.05" top/bottom.
281
+ DEFAULT_MARG_LR = 91440
282
+ DEFAULT_MARG_TB = 45720
283
+
284
+ cols = list(self.columns)
285
+ rows = list(self.rows)
286
+
287
+ per_cell_sizes: list[int] = []
288
+ for r_idx, row in enumerate(rows):
289
+ for c_idx, col in enumerate(cols):
290
+ cell = self.cell(r_idx, c_idx)
291
+ if not cell.text.strip():
292
+ continue
293
+ marL = cell.margin_left if cell.margin_left is not None else DEFAULT_MARG_LR
294
+ marR = cell.margin_right if cell.margin_right is not None else DEFAULT_MARG_LR
295
+ marT = cell.margin_top if cell.margin_top is not None else DEFAULT_MARG_TB
296
+ marB = cell.margin_bottom if cell.margin_bottom is not None else DEFAULT_MARG_TB
297
+ cx = max(1, int(col.width) - int(marL) - int(marR))
298
+ cy = max(1, int(row.height) - int(marT) - int(marB))
299
+ try:
300
+ size = TextFitter.best_fit_font_size(
301
+ cell.text, (Emu(cx), Emu(cy)), max_font_pt, font_file
302
+ )
303
+ except Exception:
304
+ # If measurement fails for a populated cell, treat it as
305
+ # the worst case so the final uniform size remains safe
306
+ # for every populated cell. Skipping the cell would let
307
+ # ``target = min(...)`` stay artificially high and other
308
+ # cells could end up still overflowing.
309
+ per_cell_sizes.append(int(min_font_pt))
310
+ continue
311
+ if size is None:
312
+ # Text genuinely does not fit at any size in this cell;
313
+ # treat as ``min_font_pt``.
314
+ per_cell_sizes.append(int(min_font_pt))
315
+ else:
316
+ per_cell_sizes.append(int(size))
317
+
318
+ target = min(per_cell_sizes) if per_cell_sizes else max_font_pt
319
+ target = max(target, min_font_pt)
320
+
321
+ for cell in self.iter_cells():
322
+ for paragraph in cell.text_frame.paragraphs:
323
+ for run in paragraph.runs:
324
+ run.font.size = Pt(target)
325
+
326
+ return int(target)
327
+
328
+ def format_cells(
329
+ self,
330
+ rows: "_CellSelector" = None,
331
+ cols: "_CellSelector" = None,
332
+ **style: Any,
333
+ ) -> "Table":
334
+ """Apply cell styling to a rectangular selection of cells; return self.
335
+
336
+ `rows` and `cols` each accept ``None`` (every row / column), an ``int``
337
+ (negative counts from the end), a ``slice``, or any iterable of ints.
338
+ The remaining keyword arguments are those of :meth:`_Cell.format`, so
339
+ the whole of a table's look is a handful of calls rather than a nest of
340
+ loops over ``cell.fill.fore_color.rgb``::
341
+
342
+ table.format_cells(rows=0, fill="#1F2937", color="#FFFFFF", bold=True)
343
+ table.format_cells(rows=slice(1, None), size_pt=11, anchor="middle")
344
+ table.format_cells(rows=range(2, len(table.rows), 2), fill="#F6F7F9")
345
+ table.format_cells(cols=-1, align="right")
346
+
347
+ Merged cells are styled through their origin cell only; spanned cells
348
+ carry no formatting of their own.
349
+ """
350
+ row_idxs = _resolve_selector(rows, len(self.rows), "rows")
351
+ col_idxs = _resolve_selector(cols, len(self.columns), "cols")
352
+ for r in row_idxs:
353
+ for c in col_idxs:
354
+ cell = self.cell(r, c)
355
+ if cell.is_spanned:
356
+ continue
357
+ cell.format(**style)
358
+ return self
359
+
360
+ @property
361
+ def vert_banding(self) -> bool:
362
+ """When `True`, indicates columns should have alternating shading.
363
+
364
+ Read/write. Used to allow columns to be traversed more easily without losing track of
365
+ which column is being read.
366
+ """
367
+ return self._tbl.bandCol
368
+
369
+ @vert_banding.setter
370
+ def vert_banding(self, value: bool):
371
+ self._tbl.bandCol = value
372
+
373
+ @property
374
+ def style(self) -> str | None:
375
+ """Built-in table style applied to this table, or |None|.
376
+
377
+ Read/write. PowerPoint ships a fixed gallery of named built-in
378
+ table styles ("Table Grid", "Medium Style 2 - Accent 1", "No Style,
379
+ No Grid", …); each is identified by a GUID stored in
380
+ ``<a:tblPr><a:tableStyleId>``.
381
+
382
+ Reading returns the friendly name when the GUID is a recognized
383
+ built-in (see :data:`pptx2.table_styles.TABLE_STYLES`), the raw
384
+ ``{GUID}`` string when it isn't, or |None| when no style id is
385
+ present.
386
+
387
+ Assigning accepts either a friendly name *or* a raw ``{GUID}``
388
+ string. An unknown friendly name raises :class:`ValueError` with a
389
+ "did you mean" hint. Assigning |None| detaches the table from any
390
+ built-in style (equivalent to :meth:`clear_style`)::
391
+
392
+ table.style = "Medium Style 2 - Accent 1"
393
+ table.style = "{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}"
394
+ table.style = None
395
+ """
396
+ tblPr = self._tbl.tblPr
397
+ if tblPr is None:
398
+ return None
399
+ guid = tblPr.tableStyleId_val
400
+ if guid is None:
401
+ return None
402
+ from pptx2.table_styles import name_for_guid
403
+
404
+ return name_for_guid(guid) or guid
405
+
406
+ @style.setter
407
+ def style(self, value: str | None) -> None:
408
+ if value is None:
409
+ self.clear_style()
410
+ return
411
+
412
+ from pptx2.table_styles import guid_for_name
413
+
414
+ text = value.strip()
415
+ is_raw_guid = text.startswith("{") and text.endswith("}")
416
+ guid = text if is_raw_guid else guid_for_name(text)
417
+
418
+ tblPr = self._tbl.get_or_add_tblPr()
419
+ tblPr.tableStyleId_val = guid
420
+
421
+ def clear_style(self) -> None:
422
+ """Detach this table from any built-in table style.
423
+
424
+ Removes the ``<a:tableStyleId>`` element from ``a:tblPr``. By
425
+ default, every table created via ``slide.shapes.add_table(...)``
426
+ is attached to PowerPoint's "Medium Style 2 — Accent 1"
427
+ (``{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}``), which paints
428
+ alternating-row banding even when :attr:`horz_banding` is set
429
+ to ``False`` — the toggles only control *bandRow/bandCol*
430
+ attributes, not the style's own banded-row overlay (which
431
+ LibreOffice and PowerPoint apply independently of the toggle).
432
+
433
+ Call this when "I'll style every cell myself" — every fill,
434
+ border, and font is set explicitly — so the style's defaults
435
+ don't bleed through any cells the caller didn't paint. See
436
+ IMPROVEMENTS item 4.
437
+ """
438
+ tblPr = self._tbl.tblPr
439
+ if tblPr is None:
440
+ return
441
+ from pptx2.oxml.ns import qn
442
+
443
+ for style_id in tblPr.findall(qn("a:tableStyleId")):
444
+ tblPr.remove(style_id)
445
+
446
+
447
+ class _Cell(Subshape):
448
+ """Table cell"""
449
+
450
+ def __init__(self, tc: CT_TableCell, parent: ProvidesPart):
451
+ super(_Cell, self).__init__(parent)
452
+ self._tc = tc
453
+
454
+ def __eq__(self, other: object) -> bool:
455
+ """|True| if this object proxies the same element as `other`.
456
+
457
+ Equality for proxy objects is defined as referring to the same XML element, whether or not
458
+ they are the same proxy object instance.
459
+ """
460
+ if not isinstance(other, type(self)):
461
+ return False
462
+ return self._tc is other._tc
463
+
464
+ def __ne__(self, other: object) -> bool:
465
+ if not isinstance(other, type(self)):
466
+ return True
467
+ return self._tc is not other._tc
468
+
469
+ @lazyproperty
470
+ def borders(self) -> _Borders:
471
+ """|_Borders| value object exposing per-edge border line formatting.
472
+
473
+ Each border edge is a |LineFormat| reachable as `borders.left`,
474
+ `borders.right`, `borders.top`, `borders.bottom`, `borders.diagonal_down`,
475
+ and `borders.diagonal_up`. Convenience helpers `borders.all(...)`,
476
+ `borders.outer(...)`, and `borders.none()` apply settings across
477
+ multiple edges in one call.
478
+ """
479
+ return _Borders(self._tc)
480
+
481
+ @lazyproperty
482
+ def fill(self) -> FillFormat:
483
+ """|FillFormat| instance for this cell.
484
+
485
+ Provides access to fill properties such as foreground color.
486
+ """
487
+ tcPr = self._tc.get_or_add_tcPr()
488
+ return FillFormat.from_fill_parent(tcPr)
489
+
490
+ def format(
491
+ self,
492
+ *,
493
+ fill: "_ColorLike | str | None" = None,
494
+ color: "_ColorLike | None" = None,
495
+ font: str | None = None,
496
+ size_pt: float | None = None,
497
+ bold: bool | None = None,
498
+ italic: bool | None = None,
499
+ align: str | None = None,
500
+ anchor: str | None = None,
501
+ margin: "float | Length | Sequence[float | Length] | None" = None,
502
+ word_wrap: bool | None = None,
503
+ ) -> "_Cell":
504
+ """Style this cell's fill and text in one call; return self.
505
+
506
+ Every argument is optional and ``None`` means "leave alone", so calls
507
+ layer. The keyword vocabulary is the same as
508
+ :meth:`ShapeTree.add_text`, and colours accept anything the rest of the
509
+ library does — hex string, ``(r, g, b)`` tuple, or ``RGBColor``::
510
+
511
+ table.cell(0, 0).format(fill="#1F2937", color="#FFFFFF", bold=True)
512
+ table.cell(3, 2).format(align="right", size_pt=11, margin=(2, 8, 2, 8))
513
+
514
+ `fill` also accepts the string ``"none"`` for a transparent cell.
515
+ `margin` is in points — a scalar for all four insets, or a
516
+ ``(top, right, bottom, left)`` 4-sequence.
517
+
518
+ Formatting is recorded as the cell's text-body defaults as well as on
519
+ its current text, so either order works — style an empty cell and then
520
+ assign ``cell.text``, or populate first and style afterwards.
521
+ """
522
+ if fill is not None:
523
+ if isinstance(fill, str) and fill.lower() == "none":
524
+ self.fill.background()
525
+ else:
526
+ self.fill.solid()
527
+ self.fill.fore_color.rgb = coerce_color(fill)
528
+ # A cell's anchor and insets live on `<a:tcPr>`, not on the text
529
+ # frame's `<a:bodyPr>` — PowerPoint reads the cell properties and
530
+ # ignores the body ones, so route those two through `_Cell`.
531
+ if anchor is not None:
532
+ self.vertical_anchor = coerce_anchor(anchor)
533
+ if margin is not None:
534
+ _apply_cell_margins(self, margin)
535
+ text_frame = self.text_frame
536
+ apply_text_style(
537
+ text_frame,
538
+ font=font,
539
+ size_pt=size_pt,
540
+ bold=bold,
541
+ italic=italic,
542
+ color=color,
543
+ align=align,
544
+ word_wrap=word_wrap,
545
+ paragraph_defaults=True,
546
+ )
547
+ # Also record the styling as the text body's defaults, so a cell
548
+ # formatted *before* it is populated keeps that styling when
549
+ # `cell.text = ...` replaces its paragraphs.
550
+ apply_body_defaults(
551
+ text_frame,
552
+ font=font,
553
+ size_pt=size_pt,
554
+ bold=bold,
555
+ italic=italic,
556
+ color=color,
557
+ align=align,
558
+ )
559
+ return self
560
+
561
+ @property
562
+ def is_merge_origin(self) -> bool:
563
+ """True if this cell is the top-left grid cell in a merged cell."""
564
+ return self._tc.is_merge_origin
565
+
566
+ @property
567
+ def is_spanned(self) -> bool:
568
+ """True if this cell is spanned by a merge-origin cell.
569
+
570
+ A merge-origin cell "spans" the other grid cells in its merge range, consuming their area
571
+ and "shadowing" the spanned grid cells.
572
+
573
+ Note this value is |False| for a merge-origin cell. A merge-origin cell spans other grid
574
+ cells, but is not itself a spanned cell.
575
+ """
576
+ return self._tc.is_spanned
577
+
578
+ @property
579
+ def margin_left(self) -> Length:
580
+ """Left margin of cells.
581
+
582
+ Read/write. If assigned |None|, the default value is used, 0.1 inches for left and right
583
+ margins and 0.05 inches for top and bottom.
584
+ """
585
+ return self._tc.marL
586
+
587
+ @margin_left.setter
588
+ def margin_left(self, margin_left: Length | None):
589
+ self._validate_margin_value(margin_left)
590
+ self._tc.marL = margin_left
591
+
592
+ @property
593
+ def margin_right(self) -> Length:
594
+ """Right margin of cell."""
595
+ return self._tc.marR
596
+
597
+ @margin_right.setter
598
+ def margin_right(self, margin_right: Length | None):
599
+ self._validate_margin_value(margin_right)
600
+ self._tc.marR = margin_right
601
+
602
+ @property
603
+ def margin_top(self) -> Length:
604
+ """Top margin of cell."""
605
+ return self._tc.marT
606
+
607
+ @margin_top.setter
608
+ def margin_top(self, margin_top: Length | None):
609
+ self._validate_margin_value(margin_top)
610
+ self._tc.marT = margin_top
611
+
612
+ @property
613
+ def margin_bottom(self) -> Length:
614
+ """Bottom margin of cell."""
615
+ return self._tc.marB
616
+
617
+ @margin_bottom.setter
618
+ def margin_bottom(self, margin_bottom: Length | None):
619
+ self._validate_margin_value(margin_bottom)
620
+ self._tc.marB = margin_bottom
621
+
622
+ def merge(self, other_cell: _Cell) -> None:
623
+ """Create merged cell from this cell to `other_cell`.
624
+
625
+ This cell and `other_cell` specify opposite corners of the merged cell range. Either
626
+ diagonal of the cell region may be specified in either order, e.g. self=bottom-right,
627
+ other_cell=top-left, etc.
628
+
629
+ Raises |ValueError| if the specified range already contains merged cells anywhere within
630
+ its extents or if `other_cell` is not in the same table as `self`.
631
+ """
632
+ tc_range = TcRange(self._tc, other_cell._tc)
633
+
634
+ if not tc_range.in_same_table:
635
+ raise ValueError("other_cell from different table")
636
+ if tc_range.contains_merged_cell:
637
+ raise ValueError("range contains one or more merged cells")
638
+
639
+ tc_range.move_content_to_origin()
640
+
641
+ row_count, col_count = tc_range.dimensions
642
+
643
+ for tc in tc_range.iter_top_row_tcs():
644
+ tc.rowSpan = row_count
645
+ for tc in tc_range.iter_left_col_tcs():
646
+ tc.gridSpan = col_count
647
+ for tc in tc_range.iter_except_left_col_tcs():
648
+ tc.hMerge = True
649
+ for tc in tc_range.iter_except_top_row_tcs():
650
+ tc.vMerge = True
651
+
652
+ @property
653
+ def span_height(self) -> int:
654
+ """int count of rows spanned by this cell.
655
+
656
+ The value of this property may be misleading (often 1) on cells where `.is_merge_origin`
657
+ is not |True|, since only a merge-origin cell contains complete span information. This
658
+ property is only intended for use on cells known to be a merge origin by testing
659
+ `.is_merge_origin`.
660
+ """
661
+ return self._tc.rowSpan
662
+
663
+ @property
664
+ def span_width(self) -> int:
665
+ """int count of columns spanned by this cell.
666
+
667
+ The value of this property may be misleading (often 1) on cells where `.is_merge_origin`
668
+ is not |True|, since only a merge-origin cell contains complete span information. This
669
+ property is only intended for use on cells known to be a merge origin by testing
670
+ `.is_merge_origin`.
671
+ """
672
+ return self._tc.gridSpan
673
+
674
+ def split(self) -> None:
675
+ """Remove merge from this (merge-origin) cell.
676
+
677
+ The merged cell represented by this object will be "unmerged", yielding a separate
678
+ unmerged cell for each grid cell previously spanned by this merge.
679
+
680
+ Raises |ValueError| when this cell is not a merge-origin cell. Test with
681
+ `.is_merge_origin` before calling.
682
+ """
683
+ if not self.is_merge_origin:
684
+ raise ValueError("not a merge-origin cell; only a merge-origin cell can be sp" "lit")
685
+
686
+ tc_range = TcRange.from_merge_origin(self._tc)
687
+
688
+ for tc in tc_range.iter_tcs():
689
+ tc.rowSpan = tc.gridSpan = 1
690
+ tc.hMerge = tc.vMerge = False
691
+
692
+ @property
693
+ def text(self) -> str:
694
+ """Textual content of cell as a single string.
695
+
696
+ The returned string will contain a newline character (`"\\n"`) separating each paragraph
697
+ and a vertical-tab (`"\\v"`) character for each line break (soft carriage return) in the
698
+ cell's text.
699
+
700
+ Assignment to `text` replaces all text currently contained in the cell. A newline
701
+ character (`"\\n"`) in the assigned text causes a new paragraph to be started. A
702
+ vertical-tab (`"\\v"`) character in the assigned text causes a line-break (soft
703
+ carriage-return) to be inserted. (The vertical-tab character appears in clipboard text
704
+ copied from PowerPoint as its encoding of line-breaks.)
705
+ """
706
+ return self.text_frame.text
707
+
708
+ @text.setter
709
+ def text(self, text: str):
710
+ self.text_frame.text = text
711
+
712
+ @property
713
+ def text_frame(self) -> TextFrame:
714
+ """|TextFrame| containing the text that appears in the cell."""
715
+ txBody = self._tc.get_or_add_txBody()
716
+ return TextFrame(txBody, self)
717
+
718
+ @property
719
+ def width(self) -> Length:
720
+ """Width of this cell in EMU (the parent column's width).
721
+
722
+ Exposed so that :meth:`TextFrame.fit_text` can measure against the
723
+ cell's bounds rather than the whole table when called on
724
+ ``cell.text_frame``.
725
+ """
726
+ tr = self._tc.getparent()
727
+ if tr is None:
728
+ return Emu(0)
729
+ try:
730
+ col_idx = list(tr).index(self._tc)
731
+ except ValueError:
732
+ return Emu(0)
733
+ tbl = tr.getparent()
734
+ if tbl is None:
735
+ return Emu(0)
736
+ try:
737
+ gridCol = tbl.tblGrid.gridCol_lst[col_idx]
738
+ except IndexError:
739
+ return Emu(0)
740
+ return Emu(int(gridCol.w))
741
+
742
+ @property
743
+ def height(self) -> Length:
744
+ """Height of this cell in EMU (the parent row's height).
745
+
746
+ Exposed so that :meth:`TextFrame.fit_text` can measure against the
747
+ cell's bounds rather than the whole table when called on
748
+ ``cell.text_frame``.
749
+ """
750
+ tr = self._tc.getparent()
751
+ if tr is None:
752
+ return Emu(0)
753
+ return Emu(int(tr.h or 0))
754
+
755
+ # Friendly short-string ↔ ST_TextVerticalType (`a:tcPr@vert`) mapping.
756
+ # The XSD default is ``horz`` (attribute may also be absent), so reading a
757
+ # cell with no explicit direction returns ``"horizontal"``.
758
+ _TEXT_DIRECTION_TO_VERT = {
759
+ "horizontal": "horz",
760
+ "rotate90": "vert",
761
+ "rotate270": "vert270",
762
+ "stacked": "wordArtVert",
763
+ }
764
+ _VERT_TO_TEXT_DIRECTION = {
765
+ "horz": "horizontal",
766
+ "vert": "rotate90",
767
+ "vert270": "rotate270",
768
+ "wordArtVert": "stacked",
769
+ }
770
+
771
+ @property
772
+ def text_direction(self) -> str | None:
773
+ """Text direction (rotation/stacking) of this cell.
774
+
775
+ Read/write. Maps the `<a:tcPr vert="...">` attribute to friendly short
776
+ strings: ``"horizontal"`` (the default), ``"rotate90"``,
777
+ ``"rotate270"``, and ``"stacked"``. This is what rotated / matrix
778
+ column headers need.
779
+
780
+ Reading returns the friendly string. When the attribute is absent the
781
+ value ``"horizontal"`` is returned (its effective default). Assigning
782
+ ``"horizontal"`` or |None| clears any explicit setting and restores the
783
+ default. A ``vert`` value not covered by the friendly mapping (e.g.
784
+ ``eaVert``) is returned verbatim.
785
+ """
786
+ tcPr = self._tc.tcPr
787
+ if tcPr is None:
788
+ return "horizontal"
789
+ vert = tcPr.vert
790
+ if vert is None:
791
+ return "horizontal"
792
+ return self._VERT_TO_TEXT_DIRECTION.get(vert, vert)
793
+
794
+ @text_direction.setter
795
+ def text_direction(self, value: str | None):
796
+ if value is None or value == "horizontal":
797
+ if self._tc.tcPr is not None:
798
+ self._tc.tcPr.vert = None
799
+ return
800
+ try:
801
+ vert = self._TEXT_DIRECTION_TO_VERT[value]
802
+ except KeyError:
803
+ raise ValueError(
804
+ "text_direction must be one of 'horizontal', 'rotate90', "
805
+ "'rotate270', 'stacked' or None, got %r" % (value,)
806
+ )
807
+ self._tc.get_or_add_tcPr().vert = vert
808
+
809
+ @property
810
+ def vertical_anchor(self) -> MSO_VERTICAL_ANCHOR | None:
811
+ """Vertical alignment of this cell.
812
+
813
+ This value is a member of the :ref:`MsoVerticalAnchor` enumeration or |None|. A value of
814
+ |None| indicates the cell has no explicitly applied vertical anchor setting and its
815
+ effective value is inherited from its style-hierarchy ancestors.
816
+
817
+ Assigning |None| to this property causes any explicitly applied vertical anchor setting to
818
+ be cleared and inheritance of its effective value to be restored.
819
+ """
820
+ return self._tc.anchor
821
+
822
+ @vertical_anchor.setter
823
+ def vertical_anchor(self, mso_anchor_idx: MSO_VERTICAL_ANCHOR | None):
824
+ self._tc.anchor = mso_anchor_idx
825
+
826
+ @staticmethod
827
+ def _validate_margin_value(margin_value: Length | None) -> None:
828
+ """Raise ValueError if `margin_value` is not a positive integer value or |None|."""
829
+ if not isinstance(margin_value, int) and margin_value is not None:
830
+ tmpl = "margin value must be integer or None, got '%s'"
831
+ raise TypeError(tmpl % margin_value)
832
+
833
+
834
+ class _Column(Subshape):
835
+ """Table column"""
836
+
837
+ def __init__(self, gridCol: CT_TableCol, parent: _ColumnCollection):
838
+ super(_Column, self).__init__(parent)
839
+ self._parent = parent
840
+ self._gridCol = gridCol
841
+ self._tbl = getattr(parent, "_tbl", None)
842
+
843
+ @property
844
+ def width(self) -> Length:
845
+ """Width of column in EMU."""
846
+ return self._gridCol.w
847
+
848
+ @width.setter
849
+ def width(self, width: Length):
850
+ self._gridCol.w = width
851
+ self._parent.notify_width_changed()
852
+
853
+ @lazyproperty
854
+ def borders(self) -> _LineGroup:
855
+ """Convenience helper for setting borders on every cell in this column.
856
+
857
+ Mirrors :class:`_Borders` on a single cell, but applied across the
858
+ whole column. Examples::
859
+
860
+ col.borders.left(width=Pt(2), color=RGBColor(0, 0, 0))
861
+ col.borders.outer(width=Pt(1))
862
+ col.borders.none()
863
+ """
864
+ if self._tbl is None:
865
+ return _LineGroup([])
866
+ return _LineGroup(_iter_column_cells(self._tbl, self._gridCol))
867
+
868
+
869
+ class _Row(Subshape):
870
+ """Table row"""
871
+
872
+ def __init__(self, tr: CT_TableRow, parent: _RowCollection):
873
+ super(_Row, self).__init__(parent)
874
+ self._parent = parent
875
+ self._tr = tr
876
+
877
+ @property
878
+ def cells(self):
879
+ """Read-only reference to collection of cells in row.
880
+
881
+ An individual cell is referenced using list notation, e.g. `cell = row.cells[0]`.
882
+ """
883
+ return _CellCollection(self._tr, self)
884
+
885
+ @property
886
+ def height(self) -> Length:
887
+ """Height of row in EMU."""
888
+ return self._tr.h
889
+
890
+ @height.setter
891
+ def height(self, height: Length):
892
+ self._tr.h = height
893
+ self._parent.notify_height_changed()
894
+
895
+ @lazyproperty
896
+ def borders(self) -> _LineGroup:
897
+ """Convenience helper for setting borders on every cell in this row.
898
+
899
+ Mirrors :class:`_Borders` on a single cell, but applied across the
900
+ whole row. Examples::
901
+
902
+ row.borders.bottom(width=Pt(2), color=RGBColor(0, 0, 0))
903
+ row.borders.outer(width=Pt(1))
904
+ row.borders.none()
905
+ """
906
+ return _LineGroup(list(self._tr.tc_lst))
907
+
908
+
909
+ def _iter_column_cells(tbl: CT_Table, gridCol):
910
+ """Return the list of ``CT_TableCell`` elements at this column's grid index."""
911
+ grid = list(tbl.tblGrid.gridCol_lst)
912
+ try:
913
+ col_idx = grid.index(gridCol)
914
+ except ValueError:
915
+ return []
916
+ cells = []
917
+ for tr in tbl.tr_lst:
918
+ tcs = tr.tc_lst
919
+ if col_idx < len(tcs):
920
+ cells.append(tcs[col_idx])
921
+ return cells
922
+
923
+
924
+ class _CellCollection(Subshape):
925
+ """Horizontal sequence of row cells"""
926
+
927
+ def __init__(self, tr: CT_TableRow, parent: _Row):
928
+ super(_CellCollection, self).__init__(parent)
929
+ self._parent = parent
930
+ self._tr = tr
931
+
932
+ def __getitem__(self, idx: int) -> _Cell:
933
+ """Provides indexed access, (e.g. 'cells[0]')."""
934
+ if idx < 0 or idx >= len(self._tr.tc_lst):
935
+ msg = "cell index [%d] out of range" % idx
936
+ raise IndexError(msg)
937
+ return _Cell(self._tr.tc_lst[idx], self)
938
+
939
+ def __iter__(self) -> Iterator[_Cell]:
940
+ """Provides iterability."""
941
+ return (_Cell(tc, self) for tc in self._tr.tc_lst)
942
+
943
+ def __len__(self) -> int:
944
+ """Supports len() function (e.g. 'len(cells) == 1')."""
945
+ return len(self._tr.tc_lst)
946
+
947
+
948
+ class _ColumnCollection(Subshape):
949
+ """Sequence of table columns."""
950
+
951
+ def __init__(self, tbl: CT_Table, parent: Table):
952
+ super(_ColumnCollection, self).__init__(parent)
953
+ self._parent = parent
954
+ self._tbl = tbl
955
+
956
+ def __getitem__(self, idx: int):
957
+ """Provides indexed access, (e.g. 'columns[0]')."""
958
+ if idx < 0 or idx >= len(self._tbl.tblGrid.gridCol_lst):
959
+ msg = "column index [%d] out of range" % idx
960
+ raise IndexError(msg)
961
+ return _Column(self._tbl.tblGrid.gridCol_lst[idx], self)
962
+
963
+ def __len__(self):
964
+ """Supports len() function (e.g. 'len(columns) == 1')."""
965
+ return len(self._tbl.tblGrid.gridCol_lst)
966
+
967
+ def notify_width_changed(self):
968
+ """Called by a column when its width changes. Pass along to parent."""
969
+ self._parent.notify_width_changed()
970
+
971
+
972
+ class _RowCollection(Subshape):
973
+ """Sequence of table rows"""
974
+
975
+ def __init__(self, tbl: CT_Table, parent: Table):
976
+ super(_RowCollection, self).__init__(parent)
977
+ self._parent = parent
978
+ self._tbl = tbl
979
+
980
+ def __getitem__(self, idx: int) -> _Row:
981
+ """Provides indexed access, (e.g. 'rows[0]')."""
982
+ if idx < 0 or idx >= len(self):
983
+ msg = "row index [%d] out of range" % idx
984
+ raise IndexError(msg)
985
+ return _Row(self._tbl.tr_lst[idx], self)
986
+
987
+ def __len__(self):
988
+ """Supports len() function (e.g. 'len(rows) == 1')."""
989
+ return len(self._tbl.tr_lst)
990
+
991
+ def notify_height_changed(self):
992
+ """Called by a row when its height changes. Pass along to parent."""
993
+ self._parent.notify_height_changed()
994
+
995
+
996
+ class _BorderEdge(object):
997
+ """Adapter exposing the |LineFormat| parent contract for one cell-border edge.
998
+
999
+ A cell border (`a:lnL`, `a:lnR`, etc.) is itself an `<a:ln>`-shaped element
1000
+ living inside `<a:tcPr>`. |LineFormat| expects its parent to expose
1001
+ `get_or_add_ln()` and `ln`; this adapter routes those calls to the matching
1002
+ edge-specific accessor on `a:tcPr`, so a single |LineFormat| implementation
1003
+ serves shape lines and table borders alike.
1004
+ """
1005
+
1006
+ def __init__(self, tc: CT_TableCell, edge: str):
1007
+ super(_BorderEdge, self).__init__()
1008
+ self._tc = tc
1009
+ self._edge = edge
1010
+
1011
+ def get_or_add_ln(self) -> CT_LineProperties:
1012
+ tcPr = self._tc.get_or_add_tcPr()
1013
+ return getattr(tcPr, "get_or_add_%s" % self._edge)()
1014
+
1015
+ @property
1016
+ def ln(self) -> CT_LineProperties | None:
1017
+ tcPr = self._tc.tcPr
1018
+ if tcPr is None:
1019
+ return None
1020
+ return getattr(tcPr, self._edge)
1021
+
1022
+
1023
+ class _Borders(object):
1024
+ """Per-edge line formatting for a table cell.
1025
+
1026
+ Returned by `cell.borders`. Each edge is a |LineFormat|; assignments such
1027
+ as `cell.borders.left.color.rgb = RGBColor(...)` materialize the border
1028
+ XML on demand. Convenience helpers act on multiple edges in one call.
1029
+
1030
+ Edge accessors (`left`, `right`, etc.) construct a fresh |LineFormat| on
1031
+ every access rather than caching one. This keeps the common
1032
+ set → ``none()`` → set-again flow correct: after ``none()`` removes the
1033
+ underlying ``<a:ln*>`` element, the next access returns a |LineFormat|
1034
+ that re-creates the element on first write, instead of writing through
1035
+ a stale reference to a detached element.
1036
+ """
1037
+
1038
+ def __init__(self, tc: CT_TableCell):
1039
+ super(_Borders, self).__init__()
1040
+ self._tc = tc
1041
+
1042
+ @property
1043
+ def left(self) -> LineFormat:
1044
+ """|LineFormat| for the left edge (`a:lnL`)."""
1045
+ return LineFormat(_BorderEdge(self._tc, "lnL"))
1046
+
1047
+ @property
1048
+ def right(self) -> LineFormat:
1049
+ """|LineFormat| for the right edge (`a:lnR`)."""
1050
+ return LineFormat(_BorderEdge(self._tc, "lnR"))
1051
+
1052
+ @property
1053
+ def top(self) -> LineFormat:
1054
+ """|LineFormat| for the top edge (`a:lnT`)."""
1055
+ return LineFormat(_BorderEdge(self._tc, "lnT"))
1056
+
1057
+ @property
1058
+ def bottom(self) -> LineFormat:
1059
+ """|LineFormat| for the bottom edge (`a:lnB`)."""
1060
+ return LineFormat(_BorderEdge(self._tc, "lnB"))
1061
+
1062
+ @property
1063
+ def diagonal_down(self) -> LineFormat:
1064
+ """|LineFormat| for the top-left-to-bottom-right diagonal (`a:lnTlToBr`)."""
1065
+ return LineFormat(_BorderEdge(self._tc, "lnTlToBr"))
1066
+
1067
+ @property
1068
+ def diagonal_up(self) -> LineFormat:
1069
+ """|LineFormat| for the bottom-left-to-top-right diagonal (`a:lnBlToTr`)."""
1070
+ return LineFormat(_BorderEdge(self._tc, "lnBlToTr"))
1071
+
1072
+ def all(self, width: Length | None = None, color: _ColorLike | None = None) -> None:
1073
+ """Apply `width` and/or `color` to every border edge (4 sides + 2 diagonals).
1074
+
1075
+ `color` accepts anything the library accepts elsewhere — a hex string
1076
+ (`"1F4E79"`), an `(r, g, b)` 3-tuple, or an |RGBColor|. Either argument
1077
+ may be |None| to leave that aspect alone.
1078
+ """
1079
+ for edge in (self.left, self.right, self.top, self.bottom,
1080
+ self.diagonal_down, self.diagonal_up):
1081
+ self._apply(edge, width, color)
1082
+
1083
+ def outer(self, width: Length | None = None, color: _ColorLike | None = None) -> None:
1084
+ """Apply `width` and/or `color` to the four outer edges (left/right/top/bottom).
1085
+
1086
+ `color` accepts a hex string, an `(r, g, b)` 3-tuple, or an |RGBColor|.
1087
+ """
1088
+ for edge in (self.left, self.right, self.top, self.bottom):
1089
+ self._apply(edge, width, color)
1090
+
1091
+ def none(self) -> None:
1092
+ """Remove all border edge elements from the cell.
1093
+
1094
+ Restores theme/style inheritance for every edge. Diagonal borders are
1095
+ also cleared. Note: |LineFormat| objects retrieved before this call
1096
+ cache an internal reference to the now-detached ``<a:ln*>`` element
1097
+ and should not be reused; re-access via ``cell.borders.left`` (etc.)
1098
+ to get a fresh |LineFormat| over a re-created element.
1099
+ """
1100
+ tcPr = self._tc.tcPr
1101
+ if tcPr is None:
1102
+ return
1103
+ tcPr._remove_lnL()
1104
+ tcPr._remove_lnR()
1105
+ tcPr._remove_lnT()
1106
+ tcPr._remove_lnB()
1107
+ tcPr._remove_lnTlToBr()
1108
+ tcPr._remove_lnBlToTr()
1109
+
1110
+ @staticmethod
1111
+ def _apply(line: LineFormat, width: Length | None, color: _ColorLike | None) -> None:
1112
+ if width is not None:
1113
+ line.width = width
1114
+ if color is not None:
1115
+ # LineFormat.color.rgb coerces hex strings, 3-tuples, and RGBColor;
1116
+ # pass through rather than pre-wrapping (RGBColor(*"1F4E79") would
1117
+ # splat the hex string into 6 positional args and raise).
1118
+ line.color.rgb = color
1119
+
1120
+
1121
+ class _LineGroup(object):
1122
+ """Apply border edges across a group of cells (a row or a column).
1123
+
1124
+ Returned by ``row.borders`` and ``col.borders``. Each edge accessor
1125
+ is callable; calling it with ``width`` and/or ``color`` applies those
1126
+ settings to that edge of every cell in the group.
1127
+ """
1128
+
1129
+ def __init__(self, tcs):
1130
+ self._tcs = tcs
1131
+
1132
+ def _apply_edge(
1133
+ self,
1134
+ edge: str,
1135
+ width: Length | None,
1136
+ color: _ColorLike | None,
1137
+ ) -> None:
1138
+ for tc in self._tcs:
1139
+ line = LineFormat(_BorderEdge(tc, edge))
1140
+ if width is not None:
1141
+ line.width = width
1142
+ if color is not None:
1143
+ # color.rgb coerces hex strings, 3-tuples, and RGBColor — pass
1144
+ # through rather than pre-wrapping (RGBColor(*"1F4E79") raises).
1145
+ line.color.rgb = color
1146
+
1147
+ def left(self, width: Length | None = None, color=None) -> None:
1148
+ """Apply *width* and/or *color* to the left edge of every cell."""
1149
+ self._apply_edge("lnL", width, color)
1150
+
1151
+ def right(self, width: Length | None = None, color=None) -> None:
1152
+ """Apply *width* and/or *color* to the right edge of every cell."""
1153
+ self._apply_edge("lnR", width, color)
1154
+
1155
+ def top(self, width: Length | None = None, color=None) -> None:
1156
+ """Apply *width* and/or *color* to the top edge of every cell."""
1157
+ self._apply_edge("lnT", width, color)
1158
+
1159
+ def bottom(self, width: Length | None = None, color=None) -> None:
1160
+ """Apply *width* and/or *color* to the bottom edge of every cell."""
1161
+ self._apply_edge("lnB", width, color)
1162
+
1163
+ def all(self, width: Length | None = None, color=None) -> None:
1164
+ """Apply *width* and/or *color* to all four outer edges of every cell."""
1165
+ for edge in ("lnL", "lnR", "lnT", "lnB"):
1166
+ self._apply_edge(edge, width, color)
1167
+
1168
+ outer = all # alias for parity with ``cell.borders.outer``
1169
+
1170
+ def none(self) -> None:
1171
+ """Clear every border edge from every cell in the group."""
1172
+ for tc in self._tcs:
1173
+ tcPr = tc.tcPr
1174
+ if tcPr is None:
1175
+ continue
1176
+ tcPr._remove_lnL()
1177
+ tcPr._remove_lnR()
1178
+ tcPr._remove_lnT()
1179
+ tcPr._remove_lnB()
1180
+ tcPr._remove_lnTlToBr()
1181
+ tcPr._remove_lnBlToTr()