athena-python-pptx 0.13.0__tar.gz → 0.14.0__tar.gz

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 (90) hide show
  1. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/AGENTS.md +5 -0
  2. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/CHANGELOG.md +177 -2
  3. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/PKG-INFO +1 -1
  4. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/docs/API_PARITY_EXCEPTIONS.md +279 -45
  5. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/__init__.py +3 -2
  6. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/batching.py +54 -1
  7. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/client.py +104 -57
  8. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/commands.py +14 -0
  9. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/presentation.py +14 -10
  10. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shapes/__init__.py +1310 -163
  11. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/slides.py +1321 -381
  12. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/text/__init__.py +138 -15
  13. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/typing.py +6 -0
  14. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pyproject.toml +1 -1
  15. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/.gitignore +0 -0
  16. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/API_PARITY_REPORT.md +0 -0
  17. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/CLAUDE.md +0 -0
  18. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/DEV-GUIDE.md +0 -0
  19. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/PARITY_QUESTIONS.md +0 -0
  20. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/PUBLISHING.md +0 -0
  21. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/README.md +0 -0
  22. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/docs/athena-api.json +0 -0
  23. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/docs/athena-api.md +0 -0
  24. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/_athena_extension.py +0 -0
  25. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/_citations.py +0 -0
  26. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/_ptc.py +0 -0
  27. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/_references.py +0 -0
  28. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/action.py +0 -0
  29. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/__init__.py +0 -0
  30. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/axis.py +0 -0
  31. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/category.py +0 -0
  32. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/chart.py +0 -0
  33. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/data.py +0 -0
  34. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/datalabel.py +0 -0
  35. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/legend.py +0 -0
  36. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/marker.py +0 -0
  37. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/plot.py +0 -0
  38. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/point.py +0 -0
  39. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/series.py +0 -0
  40. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/chart/xlsx.py +0 -0
  41. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/decorators.py +0 -0
  42. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/dml/__init__.py +0 -0
  43. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/dml/chtfmt.py +0 -0
  44. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/dml/color.py +0 -0
  45. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/dml/effect.py +0 -0
  46. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/dml/fill.py +0 -0
  47. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/dml/line.py +0 -0
  48. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/docgen.py +0 -0
  49. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/enum/__init__.py +0 -0
  50. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/enum/action.py +0 -0
  51. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/enum/chart.py +0 -0
  52. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/enum/dml.py +0 -0
  53. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/enum/lang.py +0 -0
  54. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/enum/shapes.py +0 -0
  55. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/enum/text.py +0 -0
  56. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/errors.py +0 -0
  57. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/exc.py +0 -0
  58. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/kit.py +0 -0
  59. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/media.py +0 -0
  60. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/oxml/__init__.py +0 -0
  61. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/oxml/ns.py +0 -0
  62. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/package.py +0 -0
  63. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/parts/__init__.py +0 -0
  64. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/parts/_base.py +0 -0
  65. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/parts/chart.py +0 -0
  66. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/parts/coreprops.py +0 -0
  67. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/parts/embeddedpackage.py +0 -0
  68. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/parts/image.py +0 -0
  69. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/parts/media.py +0 -0
  70. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/parts/presentation.py +0 -0
  71. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/parts/slide.py +0 -0
  72. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shapes/autoshape.py +0 -0
  73. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shapes/base.py +0 -0
  74. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shapes/connector.py +0 -0
  75. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shapes/freeform.py +0 -0
  76. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shapes/graphfrm.py +0 -0
  77. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shapes/group.py +0 -0
  78. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shapes/picture.py +0 -0
  79. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shapes/placeholder.py +0 -0
  80. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shapes/shapetree.py +0 -0
  81. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/shared.py +0 -0
  82. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/slide.py +0 -0
  83. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/spec.py +0 -0
  84. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/table.py +0 -0
  85. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/text/fonts.py +0 -0
  86. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/text/layout.py +0 -0
  87. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/text/text.py +0 -0
  88. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/types.py +0 -0
  89. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/units.py +0 -0
  90. {athena_python_pptx-0.13.0 → athena_python_pptx-0.14.0}/pptx/util.py +0 -0
@@ -59,6 +59,11 @@ A small number of REST-SDK-specific departures are documented in
59
59
  - **`_BaseSeriesData.index`** raises `NotImplementedError` (raising
60
60
  rather than silently returning 0).
61
61
  - **`Package.open()`** raises `UnsupportedFeatureError`.
62
+ - **Axis-title text frames** are one paragraph with one font: runs join
63
+ into one title string in the first run's font, with a one-time
64
+ `UserWarning` when that drops styling.
65
+ - **Table-cell text frames** are one paragraph with one run (bold, italic,
66
+ size, name, color); `font.underline` raises.
62
67
 
63
68
  ### Intentionally omitted (REST SDK limitations)
64
69
 
@@ -2,7 +2,182 @@
2
2
 
3
3
  All notable changes to `athena-python-pptx` are documented in this file.
4
4
 
5
- ## Unreleased — `pptx.kit` composition kit
5
+ ## 0.14.0 — waterfall charts render and export as waterfalls
6
+
7
+ Requires a studio that authors `AddChart2016` waterfalls and accepts the
8
+ `SetWaterfallSubtotals` chart patch (older servers reject the batch).
9
+
10
+ - `add_chart(XL_CHART_TYPE.WATERFALL, ...)` now makes a real waterfall instead
11
+ of a clustered column chart: one series of deltas, bars floating on the
12
+ running total, data labels on. More than one series raises
13
+ `ValidationError`.
14
+ - New Athena extension `chart.plots[0].subtotals = [0, 5]` (also
15
+ `set_subtotals([...])`, a `WaterfallPlot`) marks the bars drawn from zero as
16
+ totals; a total left `None` shows the running total.
17
+ - `chart.chart_type` reads back `XL_CHART_TYPE.WATERFALL`.
18
+ - `chart.plots` on a reopened deck picks the plot class from the snapshot's
19
+ plot type, so `chart.plots[0]` is a `WaterfallPlot` (or `BarPlot`,
20
+ `LinePlot`, ...) instead of the bare base plot.
21
+ - `chart.replace_data()` keeps the plot's other settings (subtotals, gap
22
+ width, data labels) in the local mirror, as the server does; subtotal
23
+ indices past the new points are dropped on both sides.
24
+ - The exported deck carries a stacked-column waterfall (invisible base
25
+ series), so it opens in any PowerPoint version and stays editable; the studio
26
+ preview matches it (centred labels, no connector lines).
27
+
28
+ ## 0.14.0 — chart series line and no-fill formatting reach the deck
29
+
30
+ Requires a studio with the `SetSeriesLine` / `SetSeriesFillVisible` /
31
+ `SetPointFillVisible` chart patch ops (older servers reject the batch).
32
+
33
+ - `series.format.line.color.rgb`, `.width`, `.dash_style` and
34
+ `.fill.background()` / `.fill.solid()` now emit `SetSeriesLine`: the plotted
35
+ line of a line / XY / radar series, the outline of a bar / area / pie /
36
+ bubble series. Previously these were local-state-only and a line chart kept
37
+ its palette colours. `dash_style` accepts `MSO_LINE_DASH_STYLE` members,
38
+ snake_case line tokens and `<a:prstDash>` presets, and reads back the enum
39
+ member (or the preset string for `dot` / `sysDashDot` / `sysDashDotDot`,
40
+ which have none); `None` clears width / dash.
41
+ - `series.format.fill.background()` emits `SetSeriesFillVisible` (the invisible
42
+ base series of a stacked-column waterfall now stays invisible); `solid()`
43
+ on a hidden fill, or assigning `fore_color`, shows it again.
44
+ `series.points[i].format.fill.background()` emits `SetPointFillVisible`; the
45
+ hidden state is kept on the series, so `solid()` through a new `format`
46
+ object (or after a refresh) shows the point again.
47
+ - Chart format writes that still have no patch op (axis line / fill, axis-title
48
+ and gridline fills, per-point line, marker hollow fill / line width / dash,
49
+ line `theme_color`, data-label borders) keep their local value and emit a
50
+ one-time `UserWarning`: `"<property> is not supported by the Athena studio
51
+ renderer and was ignored"`. They never raise. Data-label border setters no
52
+ longer send fields the server silently dropped.
53
+ - Gridline `dash_style` accepts `MSO_LINE_DASH_STYLE` members (previously only
54
+ preset strings).
55
+
56
+ ## 0.14.0 — axis-title paragraphs and runs, per-cell font family
57
+
58
+ Two python-pptx idioms crashed agent scripts partway through a deck
59
+ (staging session thread_3de488f7), after earlier batches had committed:
60
+
61
+ - `axis.axis_title.text_frame.paragraphs[0].runs` raised `AttributeError`
62
+ (`AxisTitleTextFrame` exposed only `.text`). It is now a python-pptx
63
+ `TextFrame`: `paragraphs` holds one paragraph whose `text`, `runs`,
64
+ `font` and `add_run()` work, and each run's `text` / `font`. Text goes to
65
+ the title text and fonts to the `SetAxisTitle` font fields (size, bold,
66
+ `color.rgb`, name). The patch model is one string and one font, so several
67
+ runs join into one title string drawn in the first run's font over the
68
+ paragraph font; a `UserWarning` is emitted once per axis title when that
69
+ drops a run's styling or when a font property the patch can't carry
70
+ (italic, underline, strike, theme color, …) is set. An existing title's
71
+ text is now read from the deck snapshot.
72
+ - `cell.text_frame.paragraphs[0].runs[0].font.name = "Calibri"` raised
73
+ `UnsupportedFeatureError`. `SetTableCell` now carries `font_family` (wire
74
+ `fontFamily`) and `italic`; `font.name` / `font.italic` set them, the
75
+ studio draws them and the export writes `<a:latin typeface>` / `i="1"` on
76
+ the cell's runs, for SDK-authored and uploaded tables alike.
77
+ `paragraphs[0].font` is the cell's run font. Assigning `None` cannot clear
78
+ a value the session wrote (no inherit marker on the wire) and warns.
79
+ `font.underline` still raises.
80
+
81
+ Needs a PPTX Studio server with `SetTableCell.fontFamily` / `italic`
82
+ (older servers drop the fields without error).
83
+
84
+ ## 0.14.0 — `add_run()` on an empty paragraph returns `runs[0]`
85
+
86
+ Fixes the empty run agent-built decks stored ahead of the real text in
87
+ almost every text box (staging `asset_e41e33eb`: 224 of 226), and the
88
+ doubled, default-styled titles and labels that later `runs[0].text = ...`
89
+ edits produced by writing that empty run.
90
+
91
+ - The first `paragraph.add_run()` on a paragraph whose only run is the SDK's
92
+ untouched empty seed returns that run as `runs[0]` instead of appending
93
+ after it, matching python-pptx. This covers new text boxes and autoshapes,
94
+ blank lines from `text_frame.text = ...`, and paragraphs emptied by
95
+ `paragraph.text = ""`. Repeated `add_run()` calls still create distinct
96
+ runs, and a seed run the caller wrote directly is kept.
97
+ - After a text reset the returned run is bare, as in python-pptx: the SDK
98
+ sends `SetRunText` with the new `resetFormatting` flag so the look the
99
+ server's `SetText` kept on that empty run is dropped. The same command
100
+ writes run 0 into snapshot paragraphs stored with no runs (ingested empty
101
+ placeholders), so a style set before the text resolves.
102
+ - Snapshot-loaded paragraphs keep their stored run indices, so decks written
103
+ before this fix keep their stored empty runs. `text_frame.clear()` +
104
+ `add_run()` is unchanged.
105
+ - Release after the pptx-studio server with `SetRunText.resetFormatting` is
106
+ deployed. Older servers ignore the flag, and the run returned after a text
107
+ reset then keeps the replaced text's look.
108
+ - Documented in `docs/API_PARITY_EXCEPTIONS.md`.
109
+
110
+ ## 0.14.0 — `validate()` judges text by its ink and flags table rows only when they grow
111
+
112
+ Cuts the false layout warnings an agent had to reason about and dismiss
113
+ (staging session `thread_3de488f7`: collisions for value labels on bubbles,
114
+ captions inside wide title boxes and table cells in rows sized for two
115
+ lines). Same signatures.
116
+
117
+ - **Text is compared by its ink, not its frame.** Each text frame is laid out
118
+ on the glyph-width estimate — greedy word wrap at the inner width minus the
119
+ paragraph indent (the first-line indent on the first line; a bullet moves
120
+ the text at least 1.5em in and an unindented outline level 0.375in, as in
121
+ the studio renderer), per-paragraph size and line spacing — and every line is
122
+ placed by its paragraph's alignment and the frame's vertical anchor
123
+ (autoshapes centre by default, text boxes anchor at the top). Lines are
124
+ widened 10% within their frame so an under-estimate cannot hide a real
125
+ collision; a placeholder's alignment or anchor inherited from its layout
126
+ widens the ink to every place the text could be. Text whose ink sits
127
+ entirely on a shape (a label on a card or bubble, a caption on a picture)
128
+ counts as containment unless that shape is drawn over it with an opaque
129
+ fill; frames that overlap while their texts do not are not reported; and
130
+ text that runs over the edge of a visible shape (fill or outline — plain
131
+ text boxes show no edge) or partly under a later opaque shape gets its own
132
+ message. Hidden text is judged by the share of the *text* under the cover,
133
+ and a cover that carries text of its own still hides what is beneath it.
134
+ Overflowing text now spills out of its frame the way PowerPoint draws it,
135
+ so a paragraph running onto the box below is reported even though the two
136
+ frames never touch; a studio "fits" verdict pulls that ink back inside.
137
+ - **Table cells warn only when the row grows**: wrapped lines x line height
138
+ plus the cell's top/bottom margins taller than the row (every row a vertical
139
+ merge spans) — the case where PowerPoint grows the row, measured at the
140
+ weight and family the snapshot resolved (table-style headers are bold). A single word wider
141
+ than its column (a number that will break mid-word) keeps a warning of its
142
+ own. Both are confirmed with `POST /measure-text-fit` like text overflow,
143
+ within the same 12-call budget.
144
+ - **Readable overflow message**: `needs ~2 lines at 20pt but only ~1 fits
145
+ (~0.67in of text in a 0.40in-tall box)`, `needs ~1 line at 32pt but not
146
+ even one line fits (…)`, paragraph spacing called out when it alone tips
147
+ the box over, `12–40pt` for mixed sizes, and correct plurals.
148
+ - **Measurement sends the real inner box.** `/measure-text-fit` subtracts
149
+ PowerPoint's default insets from the box it gets; `validate()` sent the box
150
+ with the frame margins already removed, so the studio measured a box 0.2in
151
+ narrower and 0.1in shorter than the text's and over-confirmed overflow. The
152
+ box now goes out grown by those insets.
153
+
154
+ ## 0.13.1 — in-batch `add_slide()` carries layout placeholders, proxies survive `refresh()`
155
+
156
+ Fixes the `'NoneType' object has no attribute 'text'` failure agents hit
157
+ with the textbook idiom `slide = prs.slides.add_slide(layout);
158
+ slide.shapes.title.text = ...` inside `with prs.batch():`, and the
159
+ follow-on where `prs.refresh()` left the held `slide` orphaned.
160
+
161
+ - A slide added inside a batch now projects its layout's placeholders
162
+ locally, from the snapshot's `themeHierarchy.layouts[].placeholders`
163
+ (new server projection), under the same `__inherited__:<slide id>:<layout
164
+ element id>` ids the server materializes on `AddSlide`. `shapes.title` /
165
+ `placeholders` resolve immediately with no flush or refresh, and writes to
166
+ them are sent in the same request as the `AddSlide`, so batch atomicity is
167
+ preserved. Decks whose snapshot lacks layout placeholder data keep the
168
+ previous behaviour (`title` is `None` until the slide is committed).
169
+ - `CommandBuffer.flush()` records `client_id -> server slide id` for every
170
+ `AddSlide` / `CopySlideFromDeck` it sends, from the response's
171
+ `created.slideIds` (same order). `Slides._update_from_snapshot()` uses
172
+ only those authoritative aliases to re-key the *existing* `Slide` object
173
+ (and its projected placeholder shape ids) to the server id instead of
174
+ dropping it and appending a fresh one. Pending proxies without an alias
175
+ are never paired positionally with unseen snapshot slides, which may
176
+ belong to other collaborators.
177
+ - No public API change; `slide.delete()` remains intentionally absent
178
+ (python-pptx parity) — use `prs.slides.delete(slide)`.
179
+
180
+ ## 0.13.0 — `pptx.kit` composition kit
6
181
 
7
182
  - New Athena-only module `pptx.kit` (`from pptx.kit import Kit, Palette,
8
183
  Fonts, Region`; also re-exported from `pptx`). `Kit(prs)` composes slides
@@ -33,7 +208,7 @@ All notable changes to `athena-python-pptx` are documented in this file.
33
208
  Documented in `docs/API_PARITY_EXCEPTIONS.md`; demo in
34
209
  `examples/kit_demo.py`.
35
210
 
36
- ## Unreleased — validate() finds overflow and hidden text, drops containment noise
211
+ ## 0.13.0 — validate() finds overflow and hidden text, drops containment noise
37
212
 
38
213
  Reworks `Slide.validate()` around the defects that actually ship in agent
39
214
  decks (verified 2026-09-14 against two 8-slide agent builds: 167 and 127
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: athena-python-pptx
3
- Version: 0.13.0
3
+ Version: 0.14.0
4
4
  Summary: Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration
5
5
  Project-URL: Homepage, https://github.com/pptx-studio/python-sdk
6
6
  Project-URL: Documentation, https://docs.pptx-studio.com/sdk/python
@@ -98,6 +98,63 @@ adapter. The REST SDK has no OPC; use `Presentation.upload(path)`
98
98
  instead, which uploads the file to the studio backend and returns
99
99
  a `Presentation` adapter.
100
100
 
101
+ ### Axis titles are one paragraph with one font
102
+
103
+ `axis.axis_title.text_frame` is a python-pptx `TextFrame`: `text`,
104
+ `paragraphs`, and each paragraph's `text` / `runs` / `font` / `add_run()`
105
+ and each run's `text` / `font` work. The `SetAxisTitle` patch carries one
106
+ title string and one font (size, bold, `color.rgb`, name), so:
107
+
108
+ - `paragraphs` always holds one paragraph; `add_paragraph()` is absent.
109
+ - Several runs join into one title string drawn in the first run's font,
110
+ layered over `paragraphs[0].font` (which is `axis_title.font`). When a
111
+ later run's own styling differs, the SDK emits one `UserWarning` per axis
112
+ title instead of dropping it silently.
113
+ - `italic`, `underline`, `strike`, `color.theme_color`, `cap`, sub/superscript
114
+ and character spacing are not carried; setting them on the title font emits
115
+ one `UserWarning` per axis title.
116
+ - Replacing the text (`text_frame.text = …`, `paragraph.text = …`) keeps the
117
+ title's font. python-pptx starts an unformatted run, but `SetAxisTitle`
118
+ cannot clear a font field; assign the font fields to change them.
119
+ - An existing title's text is read from the deck snapshot, for SDK-authored and
120
+ uploaded charts (its font is not).
121
+
122
+ ```python
123
+ tf = chart.value_axis.axis_title.text_frame
124
+ tf.text = "Revenue ($M)"
125
+ run = tf.paragraphs[0].runs[0]
126
+ run.font.size = Pt(10)
127
+ run.font.bold = True
128
+ ```
129
+
130
+ `chart.chart_title.text_frame` is not limited this way — its runs keep their
131
+ own fonts through `SetChartTitleRich`.
132
+
133
+ ### Table cells are one paragraph with one run
134
+
135
+ python-pptx cells hold any number of paragraphs and runs. The SDK's cell
136
+ `text_frame` is a single paragraph with a single run whose font is the cell's
137
+ font — `paragraphs[0].font` and `paragraphs[0].runs[0].font` are the same
138
+ object, and `add_run()` / `add_paragraph()` return the existing run /
139
+ paragraph.
140
+
141
+ - Supported run styling: `bold`, `italic`, `size`, `name` (font family) and
142
+ `color.rgb`, sent on `SetTableCell`; the server writes them to every run of
143
+ the cell and the export emits them as `a:rPr` attributes / `<a:latin>`.
144
+ - `font.underline` raises `UnsupportedFeatureError`.
145
+ - `font.name` / `font.italic` read back what this session wrote; a cell's
146
+ own font in an uploaded deck reads as `None`.
147
+ - Assigning `None` to `font.name` / `font.italic` cannot clear a value this
148
+ session wrote (the wire format has no inherit marker): the cell keeps it and
149
+ the SDK emits a `UserWarning`. Assign `False` to turn italic off.
150
+
151
+ ```python
152
+ cell.text = "Revenue"
153
+ run = cell.text_frame.paragraphs[0].runs[0]
154
+ run.font.name = "Georgia"
155
+ run.font.italic = True
156
+ ```
157
+
101
158
  ### `Presentation(pptx=...)` constructor kwarg — exposed via `Presentation.create(pptx=...)`
102
159
 
103
160
  Upstream's `Presentation(pptx=None)` accepts a file path / stream
@@ -271,6 +328,46 @@ explicit `run.hyperlink.address = None` with no slide target still sends
271
328
  `hyperlinkTargetSlideIndex` on the run, and snapshot rich content with
272
329
  that key hydrates back into `run.hyperlink`.
273
330
 
331
+ ### A run-less paragraph shows one empty run; `add_run()` returns it as `runs[0]` (Unreleased)
332
+
333
+ In python-pptx a new text box's or autoshape's paragraph has no runs, and so
334
+ does one emptied by `text_frame.text = ""` (or a blank line of
335
+ `text_frame.text = "a\n\nb"`) or by `paragraph.text = ""`; `add_run()` then
336
+ creates `runs[0]`. This SDK seeds such a paragraph with one empty run so
337
+ code that writes `paragraph.runs[0]` directly keeps working. That seed is the
338
+ remaining departure: `len(paragraph.runs)` is 1, not 0, until a run is added.
339
+
340
+ The first `add_run()` on a paragraph whose only run is that untouched seed
341
+ now returns the seed as `runs[0]`, so the stored element holds the runs
342
+ python-pptx would write and a later `runs[0].text = ...` replaces the text.
343
+ Earlier versions appended the new run at index 1 behind a stored empty run;
344
+ a later `runs[0].text` edit then wrote the empty run and doubled the text.
345
+
346
+ - The seed is returned once. A second `add_run()` appends a new run.
347
+ - A seed the caller already wrote (`runs[0].text`, `runs[0].font.*`,
348
+ `runs[0].hyperlink.*`) is a real run; `add_run()` appends after it.
349
+ - Snapshot-loaded paragraphs keep their stored runs and indices, so decks
350
+ written before this change keep their stored leading empty runs. A lone
351
+ stored empty run with no formatting (how the server stores an empty line)
352
+ is returned like the seed; a lone empty run that carries formatting is
353
+ kept and `add_run()` appends after it, as python-pptx does. The stored
354
+ form cannot tell an empty line from an empty run a caller added on
355
+ purpose, so after a reload such a run is reused as well. Export writes no
356
+ empty-text runs, so stock python-pptx reading the exported file also sees
357
+ that paragraph with no runs.
358
+ - After a text reset the returned run is bare, as in python-pptx. The
359
+ server's `SetText` keeps the replaced text's look on the empty run it
360
+ writes, so `add_run()` first sends `SetRunText` with `resetFormatting` to
361
+ make that run bare. A pptx-studio server older than the field ignores it,
362
+ and the run then keeps the replaced text's look. A paragraph stored with no
363
+ runs (an ingested empty `<a:p/>`) gets its run 0 written the same way, so a
364
+ style set before the text has a run to land on.
365
+ - `text_frame.clear()` is unchanged: it already leaves no local runs, and
366
+ its `add_run()` writes into the run the cleared paragraph keeps on the
367
+ server, which retains the cleared text's look under the `SetText`
368
+ style-retention contract for clear-and-rebuild. python-pptx returns a bare
369
+ run there.
370
+
274
371
  ---
275
372
 
276
373
  ## Agent-Friendly Additions (not in python-pptx)
@@ -429,45 +526,71 @@ slide.shapes.add_textbox(Inches(1), Inches(1), Inches(8), Inches(2))
429
526
 
430
527
  Report the layout defects a reader would notice, without rendering. An
431
528
  empty list means the slide is clean — and a designed slide (cards behind
432
- their labels, icons inside tiles) reaches it. Warnings come back in this
433
- order, at most 15 per slide, then a count line
434
- `"… and N more (M containment overlaps ignored)"`:
435
-
436
- 1. **Text overflow** — `"text in 'X' likely overflows its box (needs ~N
437
- lines of ~M at Spt; …)"` for non-table text whose estimated height
438
- (per-paragraph font-metric width → wrapped lines → lines × font × 1.2 +
439
- spacing) exceeds the box minus its text-frame margins. Word-wrap-off
440
- frames are checked horizontally; auto-fit frames are skipped. When the
441
- presentation has a studio client, each hit whose paragraphs share one
442
- font size and weight is confirmed with one `POST /measure-text-fit`
443
- pinned at that size (`fits: true` drops the warning, at most 12
444
- round-trips per call, network errors keep the heuristic verdict); a
445
- mixed-font frame (60pt heading over 12pt body) keeps the heuristic
446
- verdict because the server lays the frame out at a single font. Table
447
- cells whose single-line text is wider than the column are reported here
448
- too.
449
- 2. **Hidden text** — `"text in 'X' is covered by 'Y'"`: a text shape at
450
- least half covered by a *later* (higher z-order) shape with a solid fill,
451
- no transparency and no text. A solid fill the snapshot reports without
529
+ their labels, value labels on bubbles, icons inside tiles) reaches it.
530
+ Warnings come back in this order, at most 15 per slide, then a count line
531
+ `"… and N more (M containment overlaps ignored)"`.
532
+
533
+ Text is judged by its **ink** — the boxes its laid-out lines occupy — not by
534
+ its frame. Each frame's text is word-wrapped (greedy, at spaces) on the
535
+ glyph-width estimate at the inner width minus the paragraph indent (the
536
+ first-line indent on the first line; a bullet moves the text at least 1.5em
537
+ in, and an outline level without a margin 0.375in, as the studio renders),
538
+ with per-paragraph size and line spacing; every line is placed by its paragraph's
539
+ alignment and the frame's vertical anchor (autoshapes centre their text by
540
+ default, text boxes anchor at the top) and widened 10% within its frame so an
541
+ under-estimate cannot hide a collision. A placeholder's alignment or anchor
542
+ inherited from its layout is unknown to the proxy, so its ink covers every
543
+ place the text could be. Text that overflows spills out of its frame the way
544
+ PowerPoint draws it (a studio "fits" verdict pulls it back inside).
545
+
546
+ 1. **Text overflow** — `"text in 'X' likely overflows its box: needs ~2
547
+ lines at 20pt but only ~1 fits (~0.67in of text in a 0.40in-tall box) —
548
+ …"` for non-table text whose laid-out height (lines × font × 1.2 × line
549
+ spacing + paragraph spacing) exceeds the box minus its text-frame
550
+ margins (`"… but not even one line fits"` when a single line is too
551
+ tall). Word-wrap-off frames are checked horizontally; auto-fit frames are
552
+ skipped. When the presentation has a studio client, each hit whose
553
+ paragraphs share one font size and weight is confirmed with one
554
+ `POST /measure-text-fit` pinned at that size (`fits: true` drops the
555
+ warning, at most 12 round-trips per call, network errors keep the
556
+ heuristic verdict); a mixed-font frame (60pt heading over 12pt body)
557
+ keeps the heuristic verdict because the server lays the frame out at a
558
+ single font. The endpoint subtracts PowerPoint's default insets from the
559
+ box it gets, so the inner box goes out grown by exactly those insets.
560
+ Table cells are reported here too, when their wrapped text (lines × line
561
+ height + the cell's top/bottom margins) is taller than the row — every
562
+ row a vertical merge spans — which is when PowerPoint grows the row, or
563
+ when one word is wider than the column and PowerPoint breaks it mid-word.
564
+ Both are confirmed by the same endpoint within the same budget.
565
+ 2. **Hidden text** — `"text in 'X' is covered by 'Y' (N% of its text …)"`:
566
+ text whose ink is at least half covered by a *later* (higher z-order)
567
+ shape with a solid fill and no transparency — whether or not that shape
568
+ carries text of its own. A solid fill the snapshot reports without
452
569
  transparency data is taken as opaque (PowerPoint's default); any
453
570
  transparency set on the proxy or carried as `fillTransparency` disables
454
- the rule for that shape. `send_to_back()` / `bring_to_front()` now mirror
455
- the move on `slide.shapes` order so the rule sees the intended stack
456
- without a refresh.
457
- 3. **Text on text** — two text-bearing shapes (tables count) whose bboxes
458
- overlap or nest.
459
- 4. **Other** — off-slide shapes; partial overlaps where exactly one shape
460
- carries text (a title running under a card); empty slide-level
571
+ the rule for that shape. `send_to_back()` / `bring_to_front()` mirror the
572
+ move on `slide.shapes` order so the rule sees the intended stack without
573
+ a refresh.
574
+ 3. **Text on text** — `"text in 'X' and text in 'Y' overlap by ~0.40x0.20in
575
+ — the two texts collide"`: two texts (tables count, by their frame) whose
576
+ inks overlap.
577
+ 4. **Other** — off-slide shapes; text that runs over the edge of a visible
578
+ shape (`"… runs over the edge of 'Y' …"`) or sits partly under a later
579
+ opaque one (`"… is partly hidden by 'Y' …"`); empty slide-level
461
580
  placeholders.
462
581
 
463
- Not reported: one shape fully inside another unless both carry text,
464
- overlaps between two text-less shapes, and hairline overlaps thinner than
465
- a text-frame inset (0.05in). Rotated shapes near 90°/270° use their swapped
466
- visual bbox.
582
+ Not reported: text whose ink sits entirely on a shape (a label on a card or
583
+ bubble, a caption on a picture) unless that shape is drawn over it with an
584
+ opaque fill; a shape sitting in the empty part of a text box;
585
+ frames that overlap while their texts do not; text crossing the edge of a
586
+ plain text box that paints neither fill nor outline; overlaps between two
587
+ text-less shapes; and hairline overlaps thinner than a text-frame inset
588
+ (0.05in). Rotated shapes near 90°/270° use their swapped visual bbox, and
589
+ their ink turns with them.
467
590
 
468
- The overflow estimate uses average glyph widths, so offline it can be off
469
- by a line either way; render the slide before rebuilding a layout around a
470
- single warning.
591
+ The estimate uses average glyph widths, so offline it can be off by a line
592
+ either way; render the slide before rebuilding a layout around a single
593
+ warning.
471
594
 
472
595
  ```python
473
596
  warnings = slide.validate()
@@ -730,7 +853,7 @@ charts:
730
853
  | `series.marker.style / size / format.fill.fore_color.rgb / format.line.color.rgb` | `SetMarkerStyle` |
731
854
  | `axis.major_gridlines.format.line.* / minor_gridlines.format.line.*` | `SetGridlineFormat` |
732
855
  | `axis.major_tick_mark / minor_tick_mark / tick_label_position` | `SetAxisTickMarks` |
733
- | `axis.axis_title.text_frame.text + axis_title.font.*` | `SetAxisTitle` |
856
+ | `axis.axis_title.text_frame.text` / `.paragraphs[0]` (+ `.runs`) and their `font.*` | `SetAxisTitle` (one string, one font — see "Axis titles are one paragraph with one font") |
734
857
  | `chart.legend.position` | `SetLegendPosition` |
735
858
  | `chart.legend.font.*` | `SetLegendFont` |
736
859
 
@@ -794,6 +917,118 @@ picture.line.dash_style = "solid"
794
917
 
795
918
  ---
796
919
 
920
+ ## Chart Series Line + Fill Visibility; Ignored Chart Format Writes (unreleased)
921
+
922
+ `series.format.line` and `series.format.fill.background()` used to be
923
+ local-state-only: the calls succeeded, read back, and never reached the deck
924
+ (a line chart kept its palette colours; the hidden base series of a
925
+ stacked-column waterfall drew as solid bars). They now emit chart patches that
926
+ both the SDK-authored path (`author.ts`) and the ingested path (`patcher.ts`)
927
+ honour, and the studio renderer draws them:
928
+
929
+ | Property | Patch op | Emitted OOXML |
930
+ |---|---|---|
931
+ | `series.format.line.color.rgb = RGBColor(...)` | `SetSeriesLine` (`lineColorHex`) | `c:ser/c:spPr/a:ln/a:solidFill` |
932
+ | `series.format.line.width = Pt(2)` | `SetSeriesLine` (`lineWidthEmu`) | `<a:ln w="25400">` |
933
+ | `series.format.line.dash_style = MSO_LINE_DASH_STYLE.DASH` | `SetSeriesLine` (`dashStyle`) | `<a:prstDash val="dash"/>` |
934
+ | `series.format.line.fill.background()` / `.fill.solid()` | `SetSeriesLine` (`noLine`) | `<a:ln><a:noFill/></a:ln>` / removed |
935
+ | `series.format.fill.background()` / `.fill.solid()` on a hidden fill | `SetSeriesFillVisible` | `c:ser/c:spPr/<a:noFill/>` / removed |
936
+ | `series.points[i].format.fill.background()` / `.fill.solid()` | `SetPointFillVisible` | `<c:dPt><c:spPr><a:noFill/>` / removed |
937
+
938
+ python-pptx semantics: on a line / XY / radar series the line is the plotted
939
+ line; on a bar / area / pie / bubble series it is the outline. Assigning `None` to
940
+ `width` or `dash_style` clears the override; assigning a line colour (or
941
+ `fill.fore_color.rgb`) makes a hidden line or fill visible again.
942
+ `dash_style` accepts `MSO_LINE_DASH_STYLE` members, the SDK's snake_case line
943
+ tokens (`"round_dot"`, `"long_dash"`, …) and the `<a:prstDash>` presets
944
+ (`"sysDot"`, `"lgDash"`, …). Gridline `dash_style` now accepts the same forms.
945
+ Reading `series.format.line.dash_style` returns the `MSO_LINE_DASH_STYLE`
946
+ member, or the preset string for the presets that have none (`"dot"`,
947
+ `"sysDashDot"`, `"sysDashDotDot"`; python-pptx cannot read those at all).
948
+ A point's hidden fill is kept on the series, so a later
949
+ `series.points[i].format.fill.solid()`, through a new `format` object or
950
+ after a refresh, shows it again.
951
+
952
+ **Ignored writes warn instead of vanishing.** Chart format properties that
953
+ still have no patch op keep their value locally (read-after-write works) and
954
+ emit a `UserWarning` — `"<property> is not supported by the Athena studio
955
+ renderer and was ignored"` — once per property per presentation session.
956
+ They never raise, so existing scripts keep running:
957
+
958
+ | Property | Why it is ignored |
959
+ |---|---|
960
+ | `axis.format.line.*` / `axis.format.fill.*` | no axis line / fill op (hide the axis with `axis.visible = False`) |
961
+ | `axis_title.format.line.*` / `.fill.*` | no axis-title shape op |
962
+ | `axis.major_gridlines.format.fill.*` / `.format.line.fill.*` / `.line.color.theme_color` | gridlines carry only colour / width / dash (hide them with `axis.has_major_gridlines = False`) |
963
+ | `series.points[i].format.line.*` | no per-point outline op |
964
+ | `series.format.line.color.theme_color` | `SetSeriesLine` carries an sRGB colour only |
965
+ | `series.marker.format.fill.background()` / `.fill.fore_color.theme_color` | `SetMarkerStyle` has no hollow or theme fill |
966
+ | `series.marker.format.line.width` / `.dash_style` / `.fill.*` / `.color.theme_color` | `SetMarkerStyle` carries the marker line colour only |
967
+ | `DataLabels.border_color_hex` / `border_width` / `border_dash_style` | the data-label ops carry no border (these were previously sent and silently dropped by the server) |
968
+
969
+ Other python-pptx chart setters that stay local-only without a warning
970
+ (they are layout or label flags, not format): `Legend.horz_offset` /
971
+ `include_in_layout`, `DataLabels.show_legend_key` / `number_format_is_linked`,
972
+ and field-level `Trendline` edits.
973
+
974
+ ---
975
+
976
+ ## Waterfall Charts (unreleased)
977
+
978
+ python-pptx has no waterfall chart. `slide.shapes.add_chart(XL_CHART_TYPE.WATERFALL,
979
+ ...)` previously produced a plain clustered column chart with no way to mark
980
+ totals; it now authors a waterfall the studio renders and exports.
981
+
982
+ ```python
983
+ cd = CategoryChartData()
984
+ cd.categories = ["FY24", "Price", "Volume", "FX", "FY25"]
985
+ cd.add_series("EBIT", (120, 18, -9, -4, None)) # one series of deltas
986
+ chart = slide.shapes.add_chart(XL_CHART_TYPE.WATERFALL, x, y, cx, cy, cd).chart
987
+ chart.plots[0].subtotals = [0, 4] # Athena extension
988
+ ```
989
+
990
+ | Surface | Kind | Wire |
991
+ |---|---|---|
992
+ | `chart.plots[0]` on a waterfall | `WaterfallPlot` (a `BarPlot`: `gap_width`, `data_labels`, `has_data_labels` work) | — |
993
+ | `WaterfallPlot.subtotals` (get / set) | Athena extension | `SetWaterfallSubtotals` chart patch |
994
+ | `WaterfallPlot.set_subtotals(indices)` | Athena extension (same as the setter) | `SetWaterfallSubtotals` |
995
+ | `chart.chart_type` | reads back `XL_CHART_TYPE.WATERFALL` | — |
996
+
997
+ Semantics follow Excel's "Set as total": a subtotal point is drawn from zero
998
+ to its own value and resets the running total; a subtotal left `None` shows
999
+ the running total. Every other point floats from the running total by its
1000
+ delta; a `None` delta is a gap. Setting `subtotals` replaces the list (`[]`
1001
+ clears it); an index outside the series raises `IndexError`. Exactly one
1002
+ series is accepted — more raise `ValidationError` before anything is sent.
1003
+ Data labels start on (deltas on floating bars, totals on totals);
1004
+ `plots[0].data_labels.show_value = False` turns them off.
1005
+ Increase / decrease / total bars take theme accent 1 / 2 / 3; a series fill
1006
+ colour paints every bar and `series.points[i].format.fill` overrides one bar.
1007
+ `series.format.fill.background()` hides every bar's fill except the bars
1008
+ with their own point colour. `chart.replace_data(...)` keeps the totals by
1009
+ position and drops any past the new points. A reopened deck's
1010
+ `chart.plots[0]` is a `WaterfallPlot` with its `subtotals`.
1011
+
1012
+ **Export — a stacked-column waterfall, not a chartEx part.** There is no
1013
+ Office-2016 chartEx (`cx:`) author, and a chartEx part only renders in
1014
+ PowerPoint 2016+. The exported chart is the classic stacked-column
1015
+ construction: an invisible base series (`<a:noFill/>`, left out of the
1016
+ legend) plus Increase / Decrease / Total series, with bars that cross zero
1017
+ split into above- and below-zero parts because PowerPoint stacks positive and
1018
+ negative values separately. It renders in every PowerPoint version, Keynote,
1019
+ Google Slides and LibreOffice, stays editable through Edit Data, and a
1020
+ re-imported deck renders as the same waterfall. What that construction cannot
1021
+ carry, the studio does not draw either, so the preview matches the deck: no
1022
+ connector lines, labels centred in the bars (a stacked chart has no
1023
+ outside-end position), and no label on a floating bar that crosses zero.
1024
+ PowerPoint shows it as a stacked column chart rather than its native waterfall
1025
+ type.
1026
+
1027
+ Not yet covered: waterfalls ingested from uploaded decks (Office-2016 chartEx
1028
+ parts) still arrive without series data and render as a placeholder.
1029
+
1030
+ ---
1031
+
797
1032
  ## Table Cell Borders (added in v0.1.78+)
798
1033
 
799
1034
  python-pptx exposes table-cell border styling only through raw OOXML
@@ -1346,11 +1581,11 @@ work in addition to SDK surface and are tracked separately.
1346
1581
  `SUNBURST` (118), `HISTOGRAM` (119), `PARETO` (120),
1347
1582
  `BOX_WHISKER` (121), `WATERFALL` (122), `FUNNEL` (123). Closes
1348
1583
  python-pptx#583 ("New Chart Types in Office 2016"), #944
1349
- (Treemap / Scatter), #651 (Waterfall), #1047 (Box plot). These
1350
- use the cx-namespace OOXML schema instead of the legacy c-schema;
1351
- `slide.shapes.add_chart(...)` dispatches through a separate
1352
- `AddChart2016` wire op so the server can author the cx-flavoured
1353
- chart-part XML.
1584
+ (Treemap / Scatter), #651 (Waterfall), #1047 (Box plot).
1585
+ `slide.shapes.add_chart(...)` dispatches them through a separate
1586
+ `AddChart2016` wire op. `WATERFALL` authors a real waterfall (see
1587
+ "Waterfall Charts" below); the others are still authored as the
1588
+ nearest legacy chart family until a cx-namespace author exists.
1354
1589
 
1355
1590
  ### Slide- and presentation-level additions
1356
1591
 
@@ -1449,11 +1684,10 @@ upstream surface preserved, additions tagged with
1449
1684
  are set.
1450
1685
 
1451
1686
  - **`DataLabels.border_color_hex / border_width / border_dash_style`**
1452
- (closes python-pptx#716, 17-comment thread). Adds outline styling
1453
- to plot- and series-level data labels via the existing
1454
- `SetDataLabelStyle` / `SetSeriesDataLabelStyle` patch envelopes.
1455
- Both writes and reads work consistently with the rest of the
1456
- `DataLabels` surface.
1687
+ (closes python-pptx#716 at the API level, 17-comment thread). The
1688
+ data-label patch ops carry no border fields, so the values read back
1689
+ locally and each setter warns once that it was ignored (see "Chart Series
1690
+ Line + Fill Visibility; Ignored Chart Format Writes").
1457
1691
 
1458
1692
  - **`Chart.set_hyperlink(url=..., target_slide_index=..., tooltip=...)`**
1459
1693
  (closes python-pptx#962). Routes chart-level click-actions through
@@ -205,7 +205,8 @@ def validate_all_touched() -> dict[int, list[str]]:
205
205
  ``ReplaceText`` validate every slide.
206
206
  * Validation uses the local proxies (no snapshot refresh) plus, when the
207
207
  presentation has a studio client, at most 12 text-measurement
208
- round-trips per slide to confirm suspected text overflow.
208
+ round-trips per slide to confirm suspected text overflow and table
209
+ row growth.
209
210
  * When several decks are open, indexes of the LAST deck whose slide
210
211
  carried warnings win on collision; the executor runs one deck per
211
212
  script in practice.
@@ -264,7 +265,7 @@ def validate_all_touched() -> dict[int, list[str]]:
264
265
  return results
265
266
 
266
267
 
267
- __version__ = "0.13.0"
268
+ __version__ = "0.14.0"
268
269
 
269
270
  __all__ = [
270
271
  # Main entry point