athena-python-pptx 0.16.1__tar.gz → 0.17.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 (109) hide show
  1. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/AGENTS.md +29 -2
  2. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/API_PARITY_REPORT.md +1 -1
  3. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/CHANGELOG.md +132 -0
  4. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/PKG-INFO +2 -1
  5. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/docs/API_PARITY_EXCEPTIONS.md +355 -8
  6. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/__init__.py +2 -2
  7. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/_unsupported.py +85 -43
  8. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/batching.py +179 -4
  9. athena_python_pptx-0.17.0/pptx/oxml/__init__.py +114 -0
  10. athena_python_pptx-0.17.0/pptx/oxml/_mirror.py +471 -0
  11. athena_python_pptx-0.17.0/pptx/oxml/_tbl.py +1152 -0
  12. athena_python_pptx-0.17.0/pptx/oxml/_txbody.py +1782 -0
  13. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/oxml/ns.py +8 -0
  14. athena_python_pptx-0.17.0/pptx/oxml/presentation.py +378 -0
  15. athena_python_pptx-0.17.0/pptx/oxml/shapes/__init__.py +68 -0
  16. athena_python_pptx-0.17.0/pptx/oxml/shapes/autoshape.py +32 -0
  17. athena_python_pptx-0.17.0/pptx/oxml/shapes/connector.py +31 -0
  18. athena_python_pptx-0.17.0/pptx/oxml/shapes/graphfrm.py +32 -0
  19. athena_python_pptx-0.17.0/pptx/oxml/shapes/groupshape.py +402 -0
  20. athena_python_pptx-0.17.0/pptx/oxml/shapes/picture.py +31 -0
  21. athena_python_pptx-0.17.0/pptx/oxml/shapes/shared.py +871 -0
  22. athena_python_pptx-0.17.0/pptx/oxml/simpletypes.py +727 -0
  23. athena_python_pptx-0.17.0/pptx/oxml/table.py +583 -0
  24. athena_python_pptx-0.17.0/pptx/oxml/text.py +652 -0
  25. athena_python_pptx-0.17.0/pptx/oxml/xmlchemy.py +716 -0
  26. athena_python_pptx-0.17.0/pptx/parts/presentation.py +54 -0
  27. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/presentation.py +24 -2
  28. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shapes/__init__.py +837 -84
  29. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/slides.py +69 -4
  30. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/text/__init__.py +169 -2
  31. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/units.py +6 -0
  32. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pyproject.toml +5 -1
  33. athena_python_pptx-0.16.1/pptx/oxml/__init__.py +0 -43
  34. athena_python_pptx-0.16.1/pptx/parts/presentation.py +0 -15
  35. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/.gitignore +0 -0
  36. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/CLAUDE.md +0 -0
  37. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/DEV-GUIDE.md +0 -0
  38. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/PARITY_QUESTIONS.md +0 -0
  39. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/README.md +0 -0
  40. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/docs/athena-api.json +0 -0
  41. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/docs/athena-api.md +0 -0
  42. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/_athena_extension.py +0 -0
  43. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/_citations.py +0 -0
  44. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/_ptc.py +0 -0
  45. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/_references.py +0 -0
  46. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/action.py +0 -0
  47. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/__init__.py +0 -0
  48. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/axis.py +0 -0
  49. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/category.py +0 -0
  50. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/chart.py +0 -0
  51. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/data.py +0 -0
  52. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/datalabel.py +0 -0
  53. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/legend.py +0 -0
  54. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/marker.py +0 -0
  55. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/plot.py +0 -0
  56. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/point.py +0 -0
  57. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/series.py +0 -0
  58. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/chart/xlsx.py +0 -0
  59. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/client.py +0 -0
  60. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/commands.py +0 -0
  61. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/decorators.py +0 -0
  62. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/dml/__init__.py +0 -0
  63. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/dml/chtfmt.py +0 -0
  64. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/dml/color.py +0 -0
  65. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/dml/effect.py +0 -0
  66. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/dml/fill.py +0 -0
  67. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/dml/line.py +0 -0
  68. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/docgen.py +0 -0
  69. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/enum/__init__.py +0 -0
  70. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/enum/action.py +0 -0
  71. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/enum/chart.py +0 -0
  72. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/enum/dml.py +0 -0
  73. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/enum/lang.py +0 -0
  74. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/enum/shapes.py +0 -0
  75. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/enum/text.py +0 -0
  76. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/errors.py +0 -0
  77. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/exc.py +0 -0
  78. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/generated/__init__.py +0 -0
  79. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/generated/autoshape_types.py +0 -0
  80. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/kit.py +0 -0
  81. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/media.py +0 -0
  82. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/package.py +0 -0
  83. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/parts/__init__.py +0 -0
  84. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/parts/_base.py +0 -0
  85. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/parts/chart.py +0 -0
  86. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/parts/coreprops.py +0 -0
  87. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/parts/embeddedpackage.py +0 -0
  88. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/parts/image.py +0 -0
  89. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/parts/media.py +0 -0
  90. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/parts/slide.py +0 -0
  91. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shapes/autoshape.py +0 -0
  92. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shapes/base.py +0 -0
  93. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shapes/connector.py +0 -0
  94. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shapes/freeform.py +0 -0
  95. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shapes/graphfrm.py +0 -0
  96. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shapes/group.py +0 -0
  97. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shapes/picture.py +0 -0
  98. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shapes/placeholder.py +0 -0
  99. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shapes/shapetree.py +0 -0
  100. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/shared.py +0 -0
  101. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/slide.py +0 -0
  102. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/spec.py +0 -0
  103. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/table.py +0 -0
  104. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/text/fonts.py +0 -0
  105. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/text/layout.py +0 -0
  106. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/text/text.py +0 -0
  107. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/types.py +0 -0
  108. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/typing.py +0 -0
  109. {athena_python_pptx-0.16.1 → athena_python_pptx-0.17.0}/pptx/util.py +0 -0
@@ -69,7 +69,8 @@ A small number of REST-SDK-specific departures are documented in
69
69
 
70
70
  These standard python-pptx members are omitted because a REST SDK has no
71
71
  local XML / OPC parts:
72
- - `Shape.element`, `Shape.part` — XML element access.
72
+ - `Shape.part` — package part access. (`Shape.element` is implemented: the
73
+ shape's element, for python-pptx's shape-tree recipes; see Architecture.)
73
74
  - `TextFrame.part`, `Paragraph.part`, `Run.part` — package part access.
74
75
  - `FillFormat.from_fill_parent()`, `ColorFormat.from_colorchoice_parent()` — internal XML constructors.
75
76
  - `Font.fill` — Font fill format (uncommon; use `font.color` for text color).
@@ -119,7 +120,33 @@ install fails the parity tests rather than skipping them.
119
120
 
120
121
  This is a **REST API client** that mimics the python-pptx API. Instead of manipulating XML directly, method calls are translated to commands sent to the PPTX Studio API server. Key differences from python-pptx internals:
121
122
 
122
- - No `_element`, `_sp`, `_spTree` XML attributes — these don't exist
123
+ - No local OOXML package. python-pptx's private XML attributes (`_tbl`,
124
+ `_tc`, `series._element`, ...) raise `UnsupportedAttributeError` naming the
125
+ supported API (`pptx/_unsupported.py`), except where common recipes map onto
126
+ commands. Those edits are held until complete
127
+ (`CommandBuffer.hold_pending_edit`):
128
+ - `prs.slides._sldIdLst` and `prs.part.drop_rel()` for slide delete and move
129
+ (`pptx/oxml/presentation.py`, tests in `tests/test_sldidlst_compat.py`);
130
+ - `shape._element` / `shape.element` (with `_sp`, `_pic`, `_graphicFrame`,
131
+ `_grpSp`) and `slide.shapes._spTree` for shape delete, z-order and
132
+ duplicate, sent as `DeleteShape`, `SetShapeZOrder` and `CloneShape`
133
+ (`pptx/oxml/shapes/`, tests in `tests/test_shape_element_compat.py`);
134
+ - a shape's text: `text_frame._txBody` / `_bodyPr`, `paragraph._p`,
135
+ `run._r` and `font._rPr` are real lxml elements of python-pptx's classes
136
+ (`pptx/oxml/text.py` on the ported `xmlchemy` / `simpletypes`), a mirror of
137
+ the deck that is diffed into the public API's commands when the change
138
+ resolves. `pptx/oxml/_mirror.py` holds the reusable lifecycle (hold,
139
+ resolve, in-place re-sync, late edits, the access hooks that resolve before
140
+ a public call), `pptx/oxml/_txbody.py` the text translation; tests in
141
+ `tests/test_text_xml_compat.py`. A change no command carries raises
142
+ naming the XML path; never drop one silently.
143
+ - a table: `table._tbl`, `cell._tc`, `row._tr` and `column._gridCol`
144
+ (`pptx/oxml/table.py`), the same kind of mirror (`pptx/oxml/_tbl.py`):
145
+ cell borders, margins, anchor and fill, row heights, column widths, the
146
+ look flags, and row / column removal. It captures rows by their
147
+ serialization and re-syncs only the cells an API write touched, so a
148
+ recipe that walks a large table cell by cell stays linear; tests in
149
+ `tests/test_table_xml_compat.py`.
123
150
  - Operations are batched via `with prs.batch():` context manager
124
151
  - State is synchronized via the PPTX Studio collaboration layer (Yjs)
125
152
  - **Programmatic Tool Calling (PTC):** every `CommandBuffer.add` and
@@ -91,7 +91,7 @@ via Tranches R–W:
91
91
  | SlideLayout | PASS (+`index`) |
92
92
  | SlideMaster | PASS |
93
93
  | SlideShapes | PASS (+SDK REST helpers; `add_connector` accepts optional `begin_shape`/`end_shape`) |
94
- | Table | PASS (+SDK REST helpers + inherited `Shape.element` proxy) |
94
+ | Table | PASS (+SDK REST helpers + inherited `Shape.element`, its frame's `p:graphicFrame` element) |
95
95
  | `_Cell` | PASS (+SDK REST helpers) |
96
96
  | PlaceholderFormat | PASS (+convenience type-check booleans + custom-prompt/transform fields) |
97
97
  | Chart | PASS (`category_axis` / `value_axis` / `legend` / `font` / `chart_style` added in v0.1.68; replaces `UnsupportedFeatureError` stubs) |
@@ -2,6 +2,138 @@
2
2
 
3
3
  All notable changes to `athena-python-pptx` are documented in this file.
4
4
 
5
+ ## 0.17.0
6
+
7
+ - python-pptx's slide delete and move recipes work. They edit
8
+ `prs.slides._sldIdLst`, which raised `UnsupportedAttributeError`; it is now
9
+ python-pptx's `p:sldIdLst` (`pptx.oxml.presentation.CT_SlideIdList`):
10
+ - `xml_slides.remove(entry)` (or `del xml_slides[i]`) deletes the slide;
11
+ - re-inserting it (`insert`, `append`, `addnext`, `addprevious`) moves it
12
+ instead, and inserting a slide that is still in the list moves it, as
13
+ lxml does;
14
+ - `prs.part.drop_rel(entry.rId)` then `del` deletes the slide once;
15
+ - an entry's `id` is its slide's `slide_id`, as in python-pptx, so the
16
+ recipe that finds a slide's entry by comparing the two works.
17
+
18
+ Edits are held until the change is complete, then sent as deletes and one
19
+ reorder, so a move never deletes its slide first and "remove every slide,
20
+ then append them in a new order" is a single reorder. Anything that reads
21
+ slide positions sends an open change first (the next command, `prs.slides`,
22
+ a slide's index, a flush, a refresh, a `batch()` or `with prs:` block), so
23
+ later commands address slides by their new positions; a block that raises
24
+ drops the edits made inside it. The bulk-delete guard applies. Other lxml
25
+ members, and other OPC members of `prs.part`, raise naming the supported
26
+ API.
27
+ - A slide proxy held from before its slide was deleted (`prs.slides.delete`
28
+ or the recipes above) raises `ConflictError` on an edit. It wrote to the
29
+ slide that had taken its position.
30
+ - python-pptx's shape delete, z-order and duplicate recipes work.
31
+ `shape._element` and python-pptx's public `shape.element` (both raised or
32
+ answered with a delete-only proxy) are the shape's element, with python-pptx's
33
+ `_sp`, `_pic`, `_graphicFrame` and `_grpSp` aliases, and
34
+ `slide.shapes._spTree` is the slide's shape tree (`pptx.oxml.shapes`:
35
+ `CT_Shape`, `CT_Picture`, `CT_GraphicalObjectFrame`, `CT_Connector`,
36
+ `CT_GroupShape`). As in python-pptx, `group.shapes._spTree` is the group's
37
+ element and the tree holds `p:nvGrpSpPr` and `p:grpSpPr` first.
38
+ - `sp.getparent().remove(sp)` (or `spTree.remove(el)`, `del spTree[i]`)
39
+ deletes the shape or placeholder;
40
+ - putting it back (`insert`, `append`, `addnext`, `addprevious`), or
41
+ inserting a shape that is still in the tree, moves it, as lxml does, sent
42
+ as the fewest `SetShapeZOrder` steps that give exactly the new order:
43
+ `spTree.insert(2, el)` sends it to the back;
44
+ - inserting `copy.deepcopy(shape._element)` into any slide's tree clones the
45
+ shape there in place (`CloneShape` with no offset), and a copy never
46
+ inserted sends nothing.
47
+
48
+ Edits are held until the change is complete, as for `_sldIdLst`, so a move
49
+ never deletes its shape; the next command, a flush or refresh, and reading
50
+ `slide.shapes` send them (clones, then deletes, then z-order). An element's
51
+ tag, parent and siblings, and its `p:cNvPr` id and name, read as in
52
+ python-pptx. Other lxml members, property-level XML (`spPr`, `txBody`),
53
+ moving a shape to another slide or group, and copies changed after
54
+ `deepcopy()` raise naming the supported API.
55
+ - A held XML-recipe change that completes sends its commands without forcing
56
+ the other held change, so a shape move made during a slide move no longer
57
+ turns the slide's removal into a delete, and a clone lands on the slide it
58
+ named.
59
+ - A group member's `bring_to_front()` / `send_to_back()` / `bring_forward()` /
60
+ `send_backward()` and `delete()` update `group.shapes`, which kept the old
61
+ order and the deleted member until a refresh.
62
+ - python-pptx's text XML recipes work: bullets on and off, numbered lists,
63
+ hanging indents, superscript, letter spacing, line spacing and autofit
64
+ through XML. `text_frame._txBody` / `_bodyPr`, `paragraph._p` / `_pPr` /
65
+ `_defRPr`, `run._r` / `_rPr` and `font._rPr` raised
66
+ `UnsupportedAttributeError`; they are now real lxml elements of
67
+ python-pptx's classes (`pptx.oxml.text`), one tree per text frame built from
68
+ the deck, with python-pptx's helpers (`get_or_add_pPr()`,
69
+ `insert_element_before()`, `pPr.lvl`, ...). What a recipe changes is sent
70
+ through the public API's setters:
71
+ - `a:pPr` as `SetParagraphStyle` (indents, level, alignment, spacing, bullet
72
+ color and the `•` / `◦` / `▪`, `a:buNone` and `arabicPeriod` bullets),
73
+ its `a:defRPr` as `paragraph.font`;
74
+ - `a:rPr` as `SetRunStyle` (bold, italic, underline, size, strike,
75
+ baseline, caps, spacing, language, color, highlight, typeface);
76
+ - `a:bodyPr` as `SetTextFrameProperties` (wrap, anchor, insets, autofit);
77
+ - run text, appended runs, breaks and paragraphs, and trailing removals
78
+ through the existing text commands.
79
+
80
+ The change is sent before the next command, explicit flush, refresh or API
81
+ call on that text (never by the idle timer), so a later API write wins and
82
+ reads see the edit; like `_sldIdLst`'s, a `prs.batch()` or `with prs:` block
83
+ that raises drops the changes made inside it. Requested again, the tree shows
84
+ the deck's state in place. Anything no command carries raises
85
+ `UnsupportedAttributeError` naming the XML path and the supported API, and
86
+ is dropped. `pptx.oxml.parse_xml` and `pptx.oxml.xmlchemy.OxmlElement` build
87
+ real elements instead of raising. `lxml` is now a dependency.
88
+ - `str()` of a length (`Emu`, `Pt`, ...) and f-strings give the EMU integer, as
89
+ in python-pptx, so `pPr.set("marL", str(Emu(Pt(18))))` writes `"228600"`
90
+ (it wrote `"Emu(228600)"`); `repr()` is unchanged.
91
+ - A run's color writes merged into one command drop an earlier brightness the
92
+ later write does not set, as the studio does when it applies them one by
93
+ one: `font.color.brightness = 0.4` then `font.color.brightness = 0` (or a
94
+ new `theme_color`) sent the 0.4.
95
+ - python-pptx's table XML recipes work: cell borders, row heights, column
96
+ widths, the look flags, a cell's margins, anchor and fill through XML, and
97
+ the row and column delete recipes. `table._tbl`, `cell._tc`, `row._tr` and
98
+ `column._gridCol` raised `UnsupportedAttributeError`; they are now real lxml
99
+ elements of python-pptx's classes (`pptx.oxml.table`), one tree per table
100
+ built from the deck. What a recipe changes is sent through the public API's
101
+ setters:
102
+ - a cell's `a:lnL` / `a:lnR` / `a:lnT` / `a:lnB` as one `SetCellBorders`
103
+ (`w`, an RGB or no line, the dash), which `cell.border_*` then reads (a
104
+ later `width` or `dash_style` write keeps the no line or theme color);
105
+ - `marL` / `marR` / `marT` / `marB` as `SetCellMargins`, `anchor` as
106
+ `SetCellAnchor`, an RGB `a:solidFill` or `a:noFill` as `SetTableCell`;
107
+ - `a:tr/@h` as `SetRowHeight`, `a:gridCol/@w` as `SetColWidth`, the
108
+ `a:tblPr` flags as one `SetTableLook`;
109
+ - a removed `a:tr` as `DeleteTableRow`, a removed column (its `a:gridCol`
110
+ and its `a:tc` in every row) as `DeleteTableColumn`.
111
+
112
+ The studio cannot change an existing table's style, so an edit to
113
+ `a:tableStyleId` raises `UnsupportedAttributeError` naming what can change;
114
+ so do merges, a cell's text, rows or columns added through XML and the
115
+ other properties no command carries. A border's `cap` is ignored with a
116
+ warning. Walking the table (`table.cell(r, c)`, `row.cells`) does not send a
117
+ pending change, so a recipe that edits every cell sends its edits together.
118
+ - `table.delete_row(i)` / `table.delete_column(i)` drop the row's height (the
119
+ column's width) locally, as the server does: the rows below kept reading
120
+ the height of the row after them.
121
+ - A row or column proxy (`table.rows[i]`, `table.columns[i]`) kept across
122
+ rows or columns inserted or removed before it keeps meaning its own row or
123
+ column, as python-pptx's does: it read and wrote the one that took its
124
+ index. One whose row or column was removed raises `IndexError`.
125
+ - A chart data label's `text_frame` has python-pptx's `paragraphs`: one
126
+ paragraph over the label's text whose runs and font are the label's, so
127
+ `point.data_label.text_frame.paragraphs[0].runs[0].font.bold = True` and
128
+ `paragraphs[0].text = ...` work. `paragraphs` raised `AttributeError`.
129
+ - Table cell text frames have python-pptx's `vertical_anchor` and margins
130
+ (they write the cell's own), and `word_wrap` / `auto_size`; cell paragraphs
131
+ have `line_spacing`, `space_before`, `space_after` and `level`. A
132
+ one-paragraph cell cannot apply those last members, so a write warns once
133
+ per presentation and keeps the value for read-after-write, instead of
134
+ raising `AttributeError`, which production scripts hit. An uploaded cell's
135
+ paragraph reads its own level and spacing (`a:pPr`), as python-pptx does.
136
+
5
137
  ## 0.16.1 — `Presentation.upload()` and `create(pptx=...)` report the deck they create
6
138
 
7
139
  - `Presentation.upload()` emits the `CreatePresentation` tool-call event with
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: athena-python-pptx
3
- Version: 0.16.1
3
+ Version: 0.17.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
@@ -22,6 +22,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.10
24
24
  Requires-Dist: athena-references>=0.1.0
25
+ Requires-Dist: lxml>=4.9.0
25
26
  Requires-Dist: requests>=2.28.0
26
27
  Provides-Extra: dev
27
28
  Requires-Dist: mypy>=1.0; extra == 'dev'
@@ -45,10 +45,8 @@ parity test's known-missing list by `tests/test_no_silent_noops.py`).
45
45
  | `pptx.shapes.DateAxis` | `element` |
46
46
  | `pptx.shapes.DoughnutPlot` | `element` |
47
47
  | `pptx.shapes.FillFormat` | `from_fill_parent` |
48
- | `pptx.shapes.GroupShapes` | `element` |
49
48
  | `pptx.shapes.LayoutPlaceholder` | `get_or_add_ln`, `ln` |
50
49
  | `pptx.shapes.LayoutPlaceholders` | `element` |
51
- | `pptx.shapes.LayoutShapes` | `element` |
52
50
  | `pptx.shapes.Legend` | `element` |
53
51
  | `pptx.shapes.LinePlot` | `element` |
54
52
  | `pptx.shapes.LineSeries` | `element` |
@@ -56,12 +54,10 @@ parity test's known-missing list by `tests/test_no_silent_noops.py`).
56
54
  | `pptx.shapes.Marker` | `element` |
57
55
  | `pptx.shapes.MasterPlaceholder` | `get_or_add_ln`, `ln` |
58
56
  | `pptx.shapes.MasterPlaceholders` | `element` |
59
- | `pptx.shapes.MasterShapes` | `element` |
60
57
  | `pptx.shapes.Movie` | `get_or_add_ln`, `ln` |
61
58
  | `pptx.shapes.NotesMaster` | `element` |
62
59
  | `pptx.shapes.NotesSlidePlaceholder` | `get_or_add_ln`, `ln` |
63
60
  | `pptx.shapes.NotesSlidePlaceholders` | `element` |
64
- | `pptx.shapes.NotesSlideShapes` | `element` |
65
61
  | `pptx.shapes.Picture` | `get_or_add_ln`, `ln` |
66
62
  | `pptx.shapes.PicturePlaceholder` | `get_or_add_ln`, `ln` |
67
63
  | `pptx.shapes.PiePlot` | `element` |
@@ -72,7 +68,6 @@ parity test's known-missing list by `tests/test_no_silent_noops.py`).
72
68
  | `pptx.shapes.RadarPlot` | `element` |
73
69
  | `pptx.shapes.RadarSeries` | `element` |
74
70
  | `pptx.shapes.Shape` | `get_or_add_ln`, `ln` |
75
- | `pptx.shapes.Shapes` | `element` |
76
71
  | `pptx.shapes.SlidePlaceholder` | `get_or_add_ln`, `ln` |
77
72
  | `pptx.shapes.SlidePlaceholders` | `element` |
78
73
  | `pptx.shapes.TableCell` | `element` |
@@ -111,14 +106,13 @@ parity test's known-missing list by `tests/test_no_silent_noops.py`).
111
106
  | `pptx.parts.embeddedpackage.EmbeddedXlsxPart` | `factory`, `load`, `load_rels_from_xml`, `new`, `package`, `part_related_by`, `partname_template`, `relate_to`, `related_part`, `rels`, `target_ref` |
112
107
  | `pptx.parts.image.ImagePart` | `desc`, `ext`, `image`, `load`, `load_rels_from_xml`, `new`, `package`, `part_related_by`, `relate_to`, `related_part`, `rels`, `scale`, `sha1`, `target_ref` |
113
108
  | `pptx.parts.media.MediaPart` | `load`, `load_rels_from_xml`, `new`, `package`, `part_related_by`, `relate_to`, `related_part`, `rels`, `sha1`, `target_ref` |
114
- | `pptx.parts.presentation.PresentationPart` | `add_slide`, `core_properties`, `drop_rel`, `get_slide`, `load`, `load_rels_from_xml`, `notes_master`, `notes_master_part`, `package`, `part`, `part_related_by`, `presentation`, `relate_to`, `related_part`, `related_slide`, `related_slide_master`, `rels`, `rename_slide_parts`, `save`, `slide_id`, `target_ref` |
109
+ | `pptx.parts.presentation.PresentationPart` | `add_slide`, `core_properties`, `get_slide`, `load`, `load_rels_from_xml`, `notes_master`, `notes_master_part`, `package`, `part`, `part_related_by`, `presentation`, `relate_to`, `related_part`, `related_slide`, `related_slide_master`, `rels`, `rename_slide_parts`, `save`, `slide_id`, `target_ref` |
115
110
  | `pptx.parts.slide.BaseSlidePart` | `drop_rel`, `get_image`, `get_or_add_image_part`, `load`, `load_rels_from_xml`, `name`, `package`, `part`, `part_related_by`, `relate_to`, `related_part`, `rels`, `target_ref` |
116
111
  | `pptx.parts.slide.NotesMasterPart` | `create_default`, `drop_rel`, `get_image`, `get_or_add_image_part`, `load`, `load_rels_from_xml`, `name`, `notes_master`, `package`, `part`, `part_related_by`, `relate_to`, `related_part`, `rels`, `target_ref` |
117
112
  | `pptx.parts.slide.NotesSlidePart` | `drop_rel`, `get_image`, `get_or_add_image_part`, `load`, `load_rels_from_xml`, `name`, `new`, `notes_master`, `notes_slide`, `package`, `part`, `part_related_by`, `relate_to`, `related_part`, `rels`, `target_ref` |
118
113
  | `pptx.parts.slide.SlideLayoutPart` | `drop_rel`, `get_image`, `get_or_add_image_part`, `load`, `load_rels_from_xml`, `name`, `package`, `part`, `part_related_by`, `relate_to`, `related_part`, `rels`, `slide_layout`, `slide_master`, `target_ref` |
119
114
  | `pptx.parts.slide.SlideMasterPart` | `drop_rel`, `get_image`, `get_or_add_image_part`, `load`, `load_rels_from_xml`, `name`, `package`, `part`, `part_related_by`, `relate_to`, `related_part`, `related_slide_layout`, `rels`, `slide_master`, `target_ref` |
120
115
  | `pptx.parts.slide.SlidePart` | `add_chart_part`, `add_embedded_ole_object_part`, `drop_rel`, `get_image`, `get_or_add_image_part`, `get_or_add_video_media_part`, `has_notes_slide`, `load`, `load_rels_from_xml`, `name`, `new`, `notes_slide`, `package`, `part`, `part_related_by`, `relate_to`, `related_part`, `rels`, `slide`, `slide_id`, `slide_layout`, `target_ref` |
121
- | `pptx.presentation.Presentation` | `part` |
122
116
  | `pptx.shapes.Area3DPlot` | `part` |
123
117
  | `pptx.shapes.AreaPlot` | `part` |
124
118
  | `pptx.shapes.AreaSeries` | `part` |
@@ -271,6 +265,9 @@ After changing any of these behaviours run
271
265
  <!-- BEGIN GENERATED: unsupported-writes (scripts/generate_parity_tables.py; do not edit by hand) -->
272
266
  | Call | SDK member | Behaviour | Why / what to do instead |
273
267
  |---|---|---|---|
268
+ | `CT_GroupShape item assignment` | `CT_GroupShape.__setitem__` | raises `UnsupportedAttributeError` | replacing a child by assignment is not mirrored: remove() the old element and insert() the new one (spTree.insert(i, el) puts el before child i). |
269
+ | `CT_NonVisualDrawingProps.id` | `CT_NonVisualDrawingProps.id` | raises `UnsupportedAttributeError` | the studio gives every shape its own id, a copy inserted with copy.deepcopy() included, so a recipe's id renumbering is not needed: drop the line. |
270
+ | `CT_NonVisualDrawingProps.name` | `CT_NonVisualDrawingProps.name` | raises `UnsupportedAttributeError` | set the shape's name with shape.name = '...' (for a copy, on the shape it becomes once inserted: slide.shapes[-1].name after spTree.append(copy)). |
274
271
  | `Connector.text` | `Connector.text` | raises `UnsupportedAttributeError` | a Connector has no text frame (has_text_frame is False). Put the text in a text box or an autoshape instead. |
275
272
  | `Font.fill.gradient` | `_FontFillFormat.gradient` | raises `UnsupportedFeatureError` | Gradient text fills aren't yet wired through to the studio exporter. Use Font.color for a solid colour, or render the gradient text to an image and add_picture(...) instead. |
276
273
  | `Font.fill.patterned` | `_FontFillFormat.patterned` | raises `UnsupportedFeatureError` | Pattern text fills aren't yet wired through to the studio exporter. |
@@ -327,7 +324,13 @@ After changing any of these behaviours run
327
324
  | `slide_layout.background.fill.fore_color.rgb` | `_LayoutBackgroundColor.rgb` | raises `UnsupportedFeatureError` | Layout and master backgrounds are read-only in athena-python-pptx (no command writes them). Set slide.background.fill on each slide instead. |
328
325
  | `slide_layout.background.fill.solid` | `_LayoutBackgroundFill.solid` | raises `UnsupportedFeatureError` | Layout and master backgrounds are read-only in athena-python-pptx (no command writes them). Set slide.background.fill on each slide instead. |
329
326
  | `slide_master.background.fill.fore_color.rgb` | `_LayoutBackgroundColor.rgb` | raises `UnsupportedFeatureError` | Layout and master backgrounds are read-only in athena-python-pptx (no command writes them). Set slide.background.fill on each slide instead. |
327
+ | `table cell paragraph.level` | `_TableCellParagraph.level` | warns once; value kept locally | a table cell has one top-level paragraph |
328
+ | `table cell paragraph.line_spacing` | `_TableCellParagraph.line_spacing` | warns once; value kept locally | a table cell is one paragraph that follows the table style's spacing; set the row height (table.rows[i].height) or the cell margins instead |
329
+ | `table cell paragraph.space_after` | `_TableCellParagraph.space_after` | warns once; value kept locally | a table cell is one paragraph that follows the table style's spacing; set the row height (table.rows[i].height) or the cell margins instead |
330
+ | `table cell paragraph.space_before` | `_TableCellParagraph.space_before` | warns once; value kept locally | a table cell is one paragraph that follows the table style's spacing; set the row height (table.rows[i].height) or the cell margins instead |
330
331
  | `table cell run.hyperlink.address` | `_TableCellHyperlink.address` | raises `UnsupportedFeatureError` | Setting a hyperlink on table-cell text is not supported yet. Put the link on a text box run (run.hyperlink.address) or a shape (shape.click_action.hyperlink.address) instead. |
332
+ | `table cell text_frame.auto_size` | `_TableCellTextFrame.auto_size` | warns once; value kept locally | a table row grows to fit its text |
333
+ | `table cell text_frame.word_wrap` | `_TableCellTextFrame.word_wrap` | warns once; value kept locally | table cell text wraps at the cell's width |
331
334
  | `tick_labels.offset` | `TickLabels.offset` | warns once; value kept locally | no command carries it |
332
335
  <!-- END GENERATED: unsupported-writes -->
333
336
  ---
@@ -337,6 +340,340 @@ After changing any of these behaviours run
337
340
  These are documented intentional differences in behavior where the
338
341
  SDK doesn't yet match python-pptx's API one-for-one.
339
342
 
343
+ ### `prs.slides._sldIdLst` and `prs.part.drop_rel()` — the slide delete and move recipes (v0.17.0)
344
+
345
+ python-pptx has no public slide delete or move, so the recipes everyone copies
346
+ edit the slide-list element. They work here, line for line:
347
+
348
+ ```python
349
+ xml_slides = prs.slides._sldIdLst
350
+ slides = list(xml_slides)
351
+ xml_slides.remove(slides[i]) # delete slide i
352
+ xml_slides.insert(j, slides[i]) # ...or, re-inserted, move it to j
353
+
354
+ rId = prs.slides._sldIdLst[i].rId # delete, the other common spelling
355
+ prs.part.drop_rel(rId)
356
+ del prs.slides._sldIdLst[i]
357
+
358
+ id_dict = {sldId.id: [i, sldId.rId] for i, sldId in enumerate(prs.slides._sldIdLst)}
359
+ prs.part.drop_rel(id_dict[slide.slide_id][1]) # ...and keyed by slide id
360
+ del prs.slides._sldIdLst[id_dict[slide.slide_id][0]]
361
+ ```
362
+
363
+ `prs.slides._sldIdLst` is python-pptx's `p:sldIdLst`
364
+ (`pptx.oxml.presentation.CT_SlideIdList`). Each entry is a `CT_SlideId`
365
+ with an `id` and an `rId`.
366
+
367
+ - **`id`** is the slide's `slide_id`, as in python-pptx, so recipes that
368
+ match the two find the slide. python-pptx's is an int (`p:sldId/@id`, from
369
+ 256); this SDK's slide ids are the studio's strings, so `id` is that string.
370
+ - **`rId`** is never reused within the presentation, even after a delete or
371
+ a refresh, so an `rId` read earlier still names the same slide.
372
+ - **List edits order slides as lxml does:** `insert(i, entry)` puts the entry
373
+ before the slide now at `i` (counting the entry itself when it is still in
374
+ the list), or last when `i` is past the end, so on A B C D `insert(3, A)`
375
+ gives B C A D.
376
+ - **What differs from lxml:** the edits are not applied one line at a time.
377
+ - They collect in one pending change, and nothing reaches the studio until
378
+ the change is complete.
379
+ - It commits when no slide is detached, or before anything else reads slide
380
+ positions: the next command, a read of `prs.slides` or of a slide's index,
381
+ an explicit flush, a refresh, or the start of a `batch()` or `with prs:`
382
+ block. It sends the deletes first, then one `ReorderSlides`, so a command
383
+ built after the edit addresses slides by their new positions.
384
+ - The idle auto-flush timer never commits a change, because it could fire
385
+ between a `remove()` and its `insert()`.
386
+ - Because reading `prs.slides` commits it, inserting a removed entry after
387
+ other deck access raises: that slide is already deleted.
388
+ - A `batch()` or `with prs:` block that raises drops the edits made inside
389
+ it, and keeps those made before it.
390
+ - **Deletes** go through `Slides.delete`, so the bulk-delete guard applies. A
391
+ proxy of a deleted slide (`slide = prs.slides[i]` held from before) refuses
392
+ further edits with `ConflictError` instead of writing to the slide that took
393
+ its position.
394
+ - **Supported operations:** `len`, iteration, indexing, `index`, `remove`,
395
+ `del`, `insert`, `append`, `extend`, `getparent`, plus the entries' `id`,
396
+ `rId`, `get`, `addnext` and `addprevious`.
397
+ - **Refused operations:** anything else lxml offers (`xpath`, `set`, a new or
398
+ copied `p:sldId`) raises `UnsupportedAttributeError`. To add a slide use
399
+ `prs.slides.add_slide(layout)`; to copy one, `slide.clone(target_index)`.
400
+ - **`prs.part`:** its only working member is `drop_rel(rId)`. Other OPC members
401
+ raise.
402
+
403
+ ### `shape._element` and `slide.shapes._spTree` — the shape delete, z-order and duplicate recipes (v0.17.0)
404
+
405
+ python-pptx has no public shape delete, z-order or shape copy, so the recipes
406
+ everyone copies edit the shape tree. They work here, line for line:
407
+
408
+ ```python
409
+ sp = shape._element # delete a shape or a placeholder
410
+ sp.getparent().remove(sp)
411
+ shape.element.getparent().remove(shape.element)
412
+
413
+ spTree = slide.shapes._spTree
414
+ spTree.remove(el); spTree.insert(2, el) # send it to the back
415
+ spTree.append(el) # bring it to the front
416
+ slide.shapes[0]._element.addprevious(el) # put it below another shape
417
+
418
+ new_el = copy.deepcopy(shape._element) # duplicate it, on any slide
419
+ other_slide.shapes._spTree.insert_element_before(new_el, "p:extLst")
420
+ ```
421
+
422
+ - **The elements** come from `pptx.oxml.shapes`, as in python-pptx:
423
+ - `shape._element` and `shape.element` (python-pptx's `BaseShape.element`)
424
+ are a `CT_Shape` (`p:sp`: autoshape, text box, freeform, placeholder; also
425
+ `shape._sp`), a `CT_Picture` (`_pic`), a `CT_GraphicalObjectFrame` (table,
426
+ chart, OLE object; `_graphicFrame`), a `CT_Connector` or a `CT_GroupShape`
427
+ (`_grpSp`). A connector has no `_cxnSp` and a picture no `_sp`, as in
428
+ python-pptx.
429
+ - `slide.shapes._spTree` (also `_element`, `_grpSp`, `element`) is the
430
+ slide's `p:spTree`: `p:nvGrpSpPr`, `p:grpSpPr`, then one element per shape,
431
+ back to front, so index 2 is the back. A group's tree is its own element:
432
+ `group.shapes._spTree is group._element`.
433
+ - **What it sends** (the public API's commands):
434
+ - removing an element deletes its shape (`Shape.delete`, `DeleteShape`)
435
+ unless it is put back first, which moves it;
436
+ - moves are `SetShapeZOrder` steps (`to_back`, `to_front`, `forward`,
437
+ `backward`) composed so the order is exactly the one the XML says,
438
+ including `spTree.insert(i, el)` of a shape still in the tree, which lxml
439
+ treats as "move it before the child at `i`";
440
+ - inserting a `copy.deepcopy()` (or `copy.copy()`) of an element clones its
441
+ shape (`Shape.clone`, `CloneShape` with no offset) where python-pptx's copy
442
+ lands: in place, or for a group member at its group coordinates (one
443
+ `SetTransform`), then z-order steps if it is not on top. A copy that is
444
+ never inserted sends nothing.
445
+ - **What differs from lxml:** as for `_sldIdLst`, the edits collect in one
446
+ pending change per presentation.
447
+ - It commits when no shape is left detached, or before the next command,
448
+ an explicit flush, `prs.refresh()`, the start of an outermost
449
+ `prs.batch()` or `with prs:` block, or a read of `slide.shapes` or
450
+ `slide.placeholders`. Reading `slide.shapes._spTree` does not commit, so
451
+ `slide.shapes._spTree.remove(el); slide.shapes._spTree.insert(2, el)` is
452
+ one move.
453
+ - It sends the clones first, then the deletes, then the z-order steps, so a
454
+ copy of a shape the same change deletes is cloned before the delete. A
455
+ held `_sldIdLst` change stays held while it sends, and edits on a slide
456
+ such a change deleted first go with the slide.
457
+ - The idle auto-flush timer never commits a change, and a batch or executor
458
+ block whose body raises drops the edits it held.
459
+ - Because reads commit, inserting a removed element after reading the
460
+ shapes raises: that shape is already deleted.
461
+ - A copy is cloned from its shape as the studio has it when inserted, so a
462
+ copy whose original changed after `deepcopy()` raises instead of
463
+ differing from python-pptx's copy.
464
+ - **Supported operations:** an element's `tag`, `getparent`, `getnext`,
465
+ `getprevious`, `addnext`, `addprevious`, `copy.deepcopy`, `shape_id`,
466
+ `shape_name`, `has_ph_elm`, and its read-only `p:cNvPr` (`sp.nvSpPr.cNvPr`,
467
+ any shape's `sp._nvXxPr.cNvPr`): `get`, `attrib`, `keys`, `values`, `items`,
468
+ `id`, `name`, for the id, name and hidden flag. A tree's `len`, iteration,
469
+ indexing, `index`, `remove`, `del`, `insert`, `append`, `extend`,
470
+ `insert_element_before`, `first_child_found_in`, `iterchildren`,
471
+ `getchildren`, `iter_shape_elms`, `iter_ph_elms`, `nvGrpSpPr` and
472
+ `grpSpPr`.
473
+ - **Ids:** `p:cNvPr/@id` is the studio's string shape id (`shape.shape_id`),
474
+ not python-pptx's integer. A copy gets a new id, so a recipe's id
475
+ renumbering (`cNvPr.id = ...`) is not needed and raises.
476
+ - **Refused operations** raise `UnsupportedAttributeError` naming the
477
+ supported API: property-level XML (`spPr`, `txBody`, `xfrm`), XML built
478
+ elsewhere (`parse_xml`, a new element), writes to `p:cNvPr`, `descr` and
479
+ `title` reads (the studio does not report them), moving a shape to another
480
+ slide or into another group, a copy into a group, and a notes slide's,
481
+ layout's or master's tree. Use `shape.text_frame`, `shape.fill` /
482
+ `shape.line`, `shape.delete()`, `shape.bring_to_front()` /
483
+ `send_to_back()` / `bring_forward()` / `send_backward()`,
484
+ `shape.clone(target_slide=...)` and `slide.shapes.add_group_shape([...])`.
485
+
486
+ ### `paragraph._p`, `run._r`, `text_frame._bodyPr` — python-pptx's text XML recipes (v0.17.0)
487
+
488
+ python-pptx has no public API for bullets, hanging indents, superscript, letter
489
+ spacing or autofit details, so the recipes everyone copies edit the text's lxml
490
+ tree. They work here, line for line:
491
+
492
+ ```python
493
+ from lxml import etree
494
+ from pptx.oxml.ns import qn
495
+ from pptx.oxml.xmlchemy import OxmlElement
496
+
497
+ pPr = paragraph._p.get_or_add_pPr()
498
+ pPr.set("marL", str(Emu(Pt(18)))) # hanging indent
499
+ pPr.set("indent", str(-Emu(Pt(18))))
500
+ pPr.insert(0, pPr.makeelement(qn("a:buChar"), {"char": "•"})) # bullet on
501
+ etree.SubElement(other_paragraph._p.get_or_add_pPr(), qn("a:buNone")) # off
502
+ buAutoNum = OxmlElement("a:buAutoNum") # numbered
503
+ buAutoNum.set("type", "arabicPeriod")
504
+ run.font._rPr.set("baseline", "30000") # superscript
505
+ run._r.get_or_add_rPr().set("spc", "300") # letter spacing (3pt)
506
+ etree.SubElement(text_frame._bodyPr, qn("a:normAutofit"))
507
+ ```
508
+
509
+ `text_frame._txBody` (also `_element`), `text_frame._bodyPr`, `paragraph._p`
510
+ (`_element`, `_pPr`, `_defRPr`), `run._r` and `font._rPr` (`_element`) are real
511
+ lxml elements of python-pptx's classes (`pptx.oxml.text.CT_TextBody`,
512
+ `CT_TextParagraph`, ...), one tree per text frame, built from the deck, with
513
+ python-pptx's helpers (`get_or_add_pPr()`, `get_or_add_rPr()`,
514
+ `insert_element_before()`, `pPr.lvl`, `rPr.sz`, `bodyPr.autofit`, ...).
515
+ `run._rPr` is the run's `a:rPr` too (python-pptx spells it
516
+ `run._r.get_or_add_rPr()`). `pptx.oxml.parse_xml` and
517
+ `pptx.oxml.xmlchemy.OxmlElement` build loose elements of the same classes;
518
+ they reach the deck once attached to such a tree.
519
+
520
+ - **What differs from lxml:** an edit is not applied when its line runs.
521
+ - The edits collect, and the change is sent before the next command,
522
+ before an explicit flush (`flush()`, `refresh()`, a batch's end) and
523
+ before any public call on that text frame's paragraphs, runs and fonts.
524
+ The idle auto-flush never sends it. So a later API write wins
525
+ (`pPr.set("lvl", "2")` then `paragraph.level = 1` leaves level 1), and
526
+ `paragraph.alignment` reads an alignment the XML just set.
527
+ - A `prs.batch()` body (or an executor `with prs:` block) that raises
528
+ drops the XML edits made inside it, as it drops its commands; edits made
529
+ before it are sent when it starts.
530
+ - Requested again after that, the tree shows the deck's state (an API write
531
+ in between included), updated in place: elements a recipe kept stay
532
+ valid. An edit to a kept element after its change was sent is sent too,
533
+ before the next write on that text, the next request or the next
534
+ explicit flush.
535
+ - A change no command carries raises `UnsupportedAttributeError` when it
536
+ resolves (the line that resolved it, or the flush), naming the XML path
537
+ and the supported API. The change is dropped: the tree goes back to the
538
+ deck's state and nothing of it is sent.
539
+ - **What is sent** (through the setters the public API uses, so validation
540
+ and coalescing are the same):
541
+ - `a:pPr` as one `SetParagraphStyle`: `marL`, `indent`, `lvl`, `algn`
542
+ (`l` / `ctr` / `r` / `just`), `a:lnSpc` (`a:spcPct` or `a:spcPts`),
543
+ `a:spcBef` / `a:spcAft` (`a:spcPts`), `a:buClr` (`a:srgbClr`), and the
544
+ bullet: `a:buNone`, `a:buChar` with `•` (U+2022), `◦` (U+25E6) or `▪`
545
+ (U+25AA), `a:buAutoNum type="arabicPeriod"`. A bullet element added next
546
+ to an old one replaces it. Its `a:defRPr` is `paragraph.font`.
547
+ - `a:rPr` as `SetRunStyle`: `b`, `i`, `u`, `sz`, `strike` (`sngStrike` /
548
+ `noStrike`), `baseline`, `cap`, `spc`, `lang` (removing it clears the
549
+ language), `a:solidFill` (`a:srgbClr`, or `a:schemeClr` with python-pptx's
550
+ `a:lumMod` / `a:lumOff` brightness), `a:highlight`, `a:latin`.
551
+ `a:hlinkClick` carries a link of this text frame: a copied run keeps it.
552
+ - `a:bodyPr` as one `SetTextFrameProperties`: `wrap`, `anchor` (`t` /
553
+ `ctr` / `b`), `lIns` / `tIns` / `rIns` / `bIns` (removing one sets
554
+ python-pptx's default) and `a:noAutofit` / `a:normAutofit` /
555
+ `a:spAutoFit`.
556
+ - The text of an `a:r` (`SetRunText`); an `a:r` or `a:br` appended to a
557
+ paragraph and an `a:p` appended to the frame (`add_run()`,
558
+ `add_line_break()`, `add_paragraph()`, with their properties), and
559
+ trailing `a:r` / `a:p` removed (one frame rewrite, as `paragraph.clear()`).
560
+ - **Warns once:** an `a:buFont` with a text font (the studio draws its
561
+ bullets in the text's font), and a `baseline` other than python-pptx's
562
+ 30000 / -25000 / 0 (the studio stores superscript / subscript at the
563
+ default offset).
564
+ - **Refused operations** (the studio's commands cannot carry them):
565
+ - any other bullet character, numbering scheme or `startAt`; bullet size
566
+ (`a:buSzPct` / `a:buSzPts`), `a:buSzTx`, `a:buClrTx`, `a:buFontTx`, a
567
+ symbol-font `a:buFont` and picture bullets; changing a paragraph's
568
+ imported bullet character or numbering;
569
+ - removing an attribute or element whose value the studio cannot clear
570
+ (`algn`, `marL`, `indent`, `b`, `sz`, a color, `a:latin`, the bullet, the
571
+ autofit, ...): set a value instead;
572
+ - spacing in percent for `a:spcBef` / `a:spcAft`, other `algn` values, tab
573
+ stops, an `a:normAutofit` `fontScale` / `lnSpcReduction`, other `a:bodyPr`
574
+ attributes and children (`vert`, `rot`, `numCol`, 3-D, ...), `a:ea` /
575
+ `a:cs` / `a:sym`, text outlines and effects, `a:endParaRPr`, `a:lstStyle`;
576
+ - inserting, moving or removing `a:p` / `a:r` / `a:br` other than at the end,
577
+ editing `a:fld` text fields, and editing an element of a paragraph or run
578
+ that `text_frame.text` / `paragraph.text` replaced since it was read.
579
+ - **Not mirrored:** the text of table cells (read only in `cell._tc`, see
580
+ below), charts and notes.
581
+ - **Lengths in attributes:** as in python-pptx, `str(Emu(...))` and
582
+ f-strings give the EMU integer the recipes write (`repr()` stays `Emu(...)`).
583
+
584
+ ### `cell._tc`, `row._tr`, `table._tbl` — python-pptx's table XML recipes (v0.17.0)
585
+
586
+ python-pptx has no public API for cell borders or for deleting a row or a
587
+ column, so the recipes everyone copies edit the table's lxml tree. They work
588
+ here, line for line:
589
+
590
+ ```python
591
+ from pptx.oxml.xmlchemy import OxmlElement
592
+
593
+ def SubElement(parent, tagname, **kwargs):
594
+ element = OxmlElement(tagname)
595
+ element.attrib.update(kwargs)
596
+ parent.append(element)
597
+ return element
598
+
599
+ tcPr = cell._tc.get_or_add_tcPr() # cell borders
600
+ for edge in ("a:lnL", "a:lnR", "a:lnT", "a:lnB"):
601
+ ln = SubElement(tcPr, edge, w=str(Pt(1)), cmpd="sng", algn="ctr")
602
+ fill = SubElement(ln, "a:solidFill")
603
+ SubElement(fill, "a:srgbClr", val="1F497D")
604
+ SubElement(ln, "a:prstDash", val="solid")
605
+ table.rows[1]._tr.h = Inches(0.8) # row height
606
+ table.columns[0]._gridCol.w = Inches(2.5) # column width
607
+ tbl = table._tbl
608
+ tbl.firstRow = False # look flags
609
+ tbl.remove(table.rows[2]._tr) # delete a row
610
+ ```
611
+
612
+ `table._tbl` (also `table.rows._tbl` / `table.columns._tbl`), `cell._tc`,
613
+ `row._tr` and `column._gridCol` are real lxml elements of python-pptx's
614
+ classes (`pptx.oxml.table.CT_Table`, `CT_TableCell`, `CT_TableRow`,
615
+ `CT_TableCol`, ...), one tree per table, built from the deck, with python-pptx's
616
+ helpers (`get_or_add_tcPr()`, `tc.marL`, `tc.anchor`, `tbl.firstRow`,
617
+ `tcPr.get_or_change_to_solidFill()`, `tbl.tc(row, col)`, `tbl.iter_tcs()`, ...).
618
+
619
+ - **When it is sent:** as for the text XML above. The edits collect and are
620
+ sent before the next command, an explicit flush and any public call that
621
+ reads or writes what the XML can change (a cell's margins, anchor, fill and
622
+ borders, the row heights and column widths, the look flags), never by the
623
+ idle auto-flush; a failed `prs.batch()` or `with prs:` block drops the edits
624
+ made inside it. Walking the table (`table.cell(r, c)`, `table.rows`,
625
+ `row.cells`, a cell's text) does not send them unless rows or columns were
626
+ removed, so a recipe that edits every cell sends its edits together.
627
+ Requested again, the tree shows what the API changed since, updated in
628
+ place; a cell the recipe edited stays as it wrote it (a border's `cap`
629
+ included) until an API write on it, so the elements a recipe holds stay
630
+ valid. Rows the API added or removed leave the other rows' elements valid,
631
+ and an edit to a kept element lands on its cell even after rows were
632
+ inserted above it. A kept `table.rows[i]` / `table.columns[i]` follows its
633
+ row or column the same way (`row._tr`, `row.height`), and raises
634
+ `IndexError` once its row or column was removed.
635
+ - **What is sent** (through the setters the public API uses):
636
+ - a cell's `a:lnL` / `a:lnR` / `a:lnT` / `a:lnB` (`w`, `a:solidFill` with
637
+ an `a:srgbClr`, `a:noFill`, `a:prstDash`) as one `SetCellBorders` per
638
+ cell; removing an edge clears it (`cell.border_left.no_border()`), and
639
+ `cell.border_*` reads what the XML set (a later `width` or `dash_style`
640
+ write keeps an edge's `a:noFill` or theme color). On an uploaded table a
641
+ border's color may also be an `a:schemeClr`.
642
+ - `marL` / `marR` / `marT` / `marB` as one `SetCellMargins` per cell
643
+ (removing one resets it), `anchor` (`t` / `ctr` / `b`) as
644
+ `SetCellAnchor`, and the cell's fill as `SetTableCell`: an `a:solidFill`
645
+ holding an `a:srgbClr` (`cell.fill.fore_color.rgb`) or `a:noFill`
646
+ (`cell.fill.background()`);
647
+ - `a:tr/@h` as `SetRowHeight` and `a:gridCol/@w` as `SetColWidth`;
648
+ - the `a:tblPr` flags `firstRow` / `firstCol` / `lastRow` / `lastCol` /
649
+ `bandRow` / `bandCol` as one `SetTableLook`;
650
+ - an `a:tr` removed as `DeleteTableRow`, and a column removed (its
651
+ `a:gridCol` and its `a:tc` in every row) as `DeleteTableColumn`.
652
+ - **Accepted as the line the studio draws:** `cmpd="sng"`, `algn="ctr"`, the
653
+ line joins (`a:round`, `a:bevel`, `a:miter`) and arrow ends of type `none`.
654
+ A border's `cap` is not stored: it warns once.
655
+ - **Refused operations** (the studio's commands cannot carry them):
656
+ - the table style: an edit to `a:tblPr/a:tableStyleId`. The studio sets a
657
+ table's style when the table is added (`AddTable` / `AddLinkedTable`) and
658
+ has no command to change it; the error names what can change instead (the
659
+ look flags and each cell's fill and borders);
660
+ - other border properties (compound or inset lines, gradient lines, custom
661
+ dashes, arrowheads, a color with modifiers, a theme color on a table the
662
+ SDK added), diagonal borders, `vert`, `anchorCtr` and the other `a:tcPr`
663
+ attributes and children, gradient, picture, pattern and theme-color cell
664
+ fills, and removing a cell's fill (handing it back to the table style);
665
+ - merges (`gridSpan` / `rowSpan` / `hMerge` / `vMerge`: use `cell.merge()` /
666
+ `cell.split()`), a cell's text (use `cell.text` / `cell.text_frame`), rows
667
+ or columns added or moved through XML (use `table.rows.add()` /
668
+ `insert()`, `table.columns.add()`), a column removed from some rows only,
669
+ and removing every row or column;
670
+ - an edit to an element of a row, column or cell the API removed since it
671
+ was read.
672
+ - **Read only:** an uploaded table's tree also shows what its snapshot
673
+ records that no command sets (diagonal borders, `vert`, `anchorCtr`,
674
+ gradient fills); a cell's text is plain `a:p` / `a:r` / `a:t`, without run
675
+ properties.
676
+
340
677
  ### `NotesSlide.shapes` / `.placeholders` / `.notes_placeholder` / `.background` / `.name` — not yet exposed
341
678
 
342
679
  Upstream's `NotesSlide` exposes a full shape tree (`notes_slide.shapes`,
@@ -541,6 +878,17 @@ paragraph.
541
878
  - Assigning `None` to `font.name` / `font.italic` cannot clear a value this
542
879
  session wrote (the wire format has no inherit marker): the cell keeps it and
543
880
  the SDK emits a `UserWarning`. Assign `False` to turn italic off.
881
+ - The text frame's `vertical_anchor` and `margin_left` / `margin_right` /
882
+ `margin_top` / `margin_bottom` are the cell's own (`cell.vertical_anchor`,
883
+ `cell.margin_left`, ...) and send the same command (v0.17.0).
884
+ - `text_frame.word_wrap` / `auto_size` and the paragraph's `line_spacing` /
885
+ `space_before` / `space_after` / `level` exist as in python-pptx, but the
886
+ studio cannot apply them to a one-paragraph cell. A write warns once per
887
+ presentation (table above) instead of raising `AttributeError`, and the value
888
+ is kept for read-after-write. Otherwise an uploaded cell's paragraph reads
889
+ its own `a:pPr` as python-pptx does (`lvl`; `a:lnSpc` as a multiple or a
890
+ pitch; `a:spcBef` / `a:spcAft` in points, a percent reading `None`) until its
891
+ text is replaced, and other reads return `None` (`level` 0) (v0.17.0).
544
892
 
545
893
  ```python
546
894
  cell.text = "Revenue"
@@ -2646,7 +2994,6 @@ python-pptx. Generated from `pptx._athena_extension.iter_athena_extensions()`.
2646
2994
  | `pptx.kit.Kit` | class | 0.13.0 | Athena | Kit — slide composition helpers with overflow-safe, server-measured text. |
2647
2995
  | `pptx.kit.Palette` | class | 0.13.0 | Athena | Palette — named hex colours the kit paints with (Athena defaults). |
2648
2996
  | `pptx.kit.Region` | class | 0.13.0 | Athena | Region — EMU rectangle with inset / split helpers for slide composition. |
2649
- | `pptx.oxml` | module | 0.9.2 | | pptx.oxml import-compatibility shim (REST-SDK only — no local XML tree). |
2650
2997
  | `pptx.presentation.Presentation.asset_id` | property | 0.1.0 | | Presentation.asset_id — Athena asset id alias for deck_id. |
2651
2998
  | `pptx.presentation.Presentation.batch` | method | 0.1.0 | | Presentation.batch — context manager for batched commands. |
2652
2999
  | `pptx.presentation.Presentation.close` | method | 0.1.0 | | Presentation.close — flush pending commands and release resources. |