athena-python-pptx 0.17.0__tar.gz → 0.18.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.
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/API_PARITY_REPORT.md +0 -1
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/CHANGELOG.md +46 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/PKG-INFO +1 -1
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/docs/API_PARITY_EXCEPTIONS.md +60 -17
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/__init__.py +1 -1
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/_unsupported.py +18 -7
- athena_python_pptx-0.18.0/pptx/chart/chart.py +7 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/client.py +48 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/commands.py +3 -2
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/_tbl.py +22 -14
- athena_python_pptx-0.18.0/pptx/oxml/shapes/autoshape.py +75 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/shared.py +23 -6
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shapes/__init__.py +174 -49
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pyproject.toml +1 -1
- athena_python_pptx-0.17.0/pptx/chart/chart.py +0 -7
- athena_python_pptx-0.17.0/pptx/oxml/shapes/autoshape.py +0 -32
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/.gitignore +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/AGENTS.md +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/CLAUDE.md +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/DEV-GUIDE.md +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/PARITY_QUESTIONS.md +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/README.md +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/docs/athena-api.json +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/docs/athena-api.md +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/_athena_extension.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/_citations.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/_ptc.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/_references.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/action.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/batching.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/__init__.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/axis.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/category.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/data.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/datalabel.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/legend.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/marker.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/plot.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/point.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/series.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/chart/xlsx.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/decorators.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/dml/__init__.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/dml/chtfmt.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/dml/color.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/dml/effect.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/dml/fill.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/dml/line.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/docgen.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/enum/__init__.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/enum/action.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/enum/chart.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/enum/dml.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/enum/lang.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/enum/shapes.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/enum/text.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/errors.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/exc.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/generated/__init__.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/generated/autoshape_types.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/kit.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/media.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/__init__.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/_mirror.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/_txbody.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/ns.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/presentation.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/__init__.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/connector.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/groupshape.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/shapes/picture.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/simpletypes.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/table.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/text.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/oxml/xmlchemy.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/package.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/parts/__init__.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/parts/_base.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/parts/chart.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/parts/coreprops.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/parts/embeddedpackage.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/parts/image.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/parts/media.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/parts/presentation.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/parts/slide.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/presentation.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shapes/autoshape.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shapes/base.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shapes/connector.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shapes/freeform.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shapes/graphfrm.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shapes/group.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shapes/picture.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shapes/placeholder.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shapes/shapetree.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/shared.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/slide.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/slides.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/spec.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/table.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/text/__init__.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/text/fonts.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/text/layout.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/text/text.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/types.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/typing.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/units.py +0 -0
- {athena_python_pptx-0.17.0 → athena_python_pptx-0.18.0}/pptx/util.py +0 -0
|
@@ -110,7 +110,6 @@ via Tranches R–W:
|
|
|
110
110
|
| `*.element`, `*.part`, `*.ln`, `*.get_or_add_ln` | XML element / OPC package / direct DML XML access — REST SDK has no local XML or packages |
|
|
111
111
|
| `FillFormat.from_fill_parent()` | Internal XML constructor |
|
|
112
112
|
| `ColorFormat.from_colorchoice_parent()` | Internal XML constructor |
|
|
113
|
-
| `Picture.image.blob` | Image bytes live in the studio's asset store — not materialized into Python |
|
|
114
113
|
|
|
115
114
|
## Closed Parity Gaps (added 2026-05-18)
|
|
116
115
|
|
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `athena-python-pptx` are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.18.0 — chart.chart_area / chart.plot_area format and add_chart(name=)
|
|
6
|
+
|
|
7
|
+
- `chart.chart_area.format` and `chart.plot_area.format` now round-trip fill
|
|
8
|
+
and line edits through chart specs, OOXML export, import and preview
|
|
9
|
+
rendering. Use `fill.background()` for a transparent chart or plot area.
|
|
10
|
+
`slide.shapes.add_chart(..., name=...)` also accepts an Athena-only shape
|
|
11
|
+
name.
|
|
12
|
+
- `Picture.image` returns python-pptx's `Image`, so `picture.image.blob`,
|
|
13
|
+
`.content_type`, `.ext`, `.sha1`, `.size` and `.filename` work. It returned
|
|
14
|
+
None for every picture, and code reading an existing deck's images had to
|
|
15
|
+
fetch `image_url` by hand with the sandbox key. A picture added in this
|
|
16
|
+
session gives back the bytes it was added from; a picture read from the deck
|
|
17
|
+
fetches its bytes once from the studio's `/media` route with the SDK's own
|
|
18
|
+
credentials (redirects refused, 50 MB cap) and caches them on the shape. A
|
|
19
|
+
picture with no media raises `ValueError("no embedded image")`, as
|
|
20
|
+
python-pptx does.
|
|
21
|
+
|
|
22
|
+
## 0.17.1
|
|
23
|
+
|
|
24
|
+
- `Table.table_style` returns the table's style GUID, the text of
|
|
25
|
+
`table._tbl`'s `a:tableStyleId`, which python-pptx code reads: an uploaded
|
|
26
|
+
table's from the deck, or the one `add_table()` / `add_linked_table()` gave a
|
|
27
|
+
table the SDK added; None for a table without one. It returned None for every
|
|
28
|
+
table (it read a snapshot field the studio never sends). It stays read-only:
|
|
29
|
+
the studio has no command that changes an existing table's style.
|
|
30
|
+
- `shape._element.txBody` (and `shape._sp.txBody`) of an autoshape, text box
|
|
31
|
+
or placeholder is its text frame's `_txBody`, the same lxml element
|
|
32
|
+
(`sp.txBody is shape.text_frame._txBody`, as in python-pptx), and
|
|
33
|
+
`sp.get_or_add_txBody()` returns it; it raised `UnsupportedAttributeError`.
|
|
34
|
+
Edits through it are held and sent as the text's commands, as edits through
|
|
35
|
+
`text_frame._txBody` are, so python-pptx's "for every shape, rewrite each
|
|
36
|
+
`a:t` of `shape._element.txBody`" recipe works. As in python-pptx it is None
|
|
37
|
+
for a picture's, graphic frame's, connector's or group's element, and for a
|
|
38
|
+
shape without a text frame.
|
|
39
|
+
- A table cell's cleared edge (`cell.border_left.no_border()`, or
|
|
40
|
+
`SetCellBorders` with `None`) reads in `cell._tc` as an `a:lnX` holding
|
|
41
|
+
`a:noFill` on every table, as the studio now stores and exports it (an
|
|
42
|
+
uploaded table's read as no `a:lnX`, which in PowerPoint is the table
|
|
43
|
+
style's line). A width or dash written after `no_border()` keeps the edge
|
|
44
|
+
without a line, as it does an `a:noFill` an XML recipe wrote, and an RGB
|
|
45
|
+
color draws it again (it sent a width-only edge). Removing a cell's `a:lnX`
|
|
46
|
+
through `cell._tc` raises
|
|
47
|
+
`UnsupportedAttributeError`: in python-pptx that hands the edge back to the
|
|
48
|
+
table style, which no studio command does (it cleared the edge before). For
|
|
49
|
+
no line, keep the `a:lnX` and give it an `a:noFill`, or call `no_border()`.
|
|
50
|
+
|
|
5
51
|
## 0.17.0
|
|
6
52
|
|
|
7
53
|
- python-pptx's slide delete and move recipes work. They edit
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: athena-python-pptx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.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
|
|
@@ -463,7 +463,10 @@ other_slide.shapes._spTree.insert_element_before(new_el, "p:extLst")
|
|
|
463
463
|
differing from python-pptx's copy.
|
|
464
464
|
- **Supported operations:** an element's `tag`, `getparent`, `getnext`,
|
|
465
465
|
`getprevious`, `addnext`, `addprevious`, `copy.deepcopy`, `shape_id`,
|
|
466
|
-
`shape_name`, `has_ph_elm`,
|
|
466
|
+
`shape_name`, `has_ph_elm`, `txBody` (a `p:sp`'s is its text frame's
|
|
467
|
+
`_txBody`, the same element, as in python-pptx, with `get_or_add_txBody()`;
|
|
468
|
+
the other elements' is None, as python-pptx answers; see the text XML
|
|
469
|
+
section below), and its read-only `p:cNvPr` (`sp.nvSpPr.cNvPr`,
|
|
467
470
|
any shape's `sp._nvXxPr.cNvPr`): `get`, `attrib`, `keys`, `values`, `items`,
|
|
468
471
|
`id`, `name`, for the id, name and hidden flag. A tree's `len`, iteration,
|
|
469
472
|
indexing, `index`, `remove`, `del`, `insert`, `append`, `extend`,
|
|
@@ -474,7 +477,7 @@ other_slide.shapes._spTree.insert_element_before(new_el, "p:extLst")
|
|
|
474
477
|
not python-pptx's integer. A copy gets a new id, so a recipe's id
|
|
475
478
|
renumbering (`cNvPr.id = ...`) is not needed and raises.
|
|
476
479
|
- **Refused operations** raise `UnsupportedAttributeError` naming the
|
|
477
|
-
supported API: property-level XML (`spPr`, `
|
|
480
|
+
supported API: property-level XML (`spPr`, `xfrm`), XML built
|
|
478
481
|
elsewhere (`parse_xml`, a new element), writes to `p:cNvPr`, `descr` and
|
|
479
482
|
`title` reads (the studio does not report them), moving a shape to another
|
|
480
483
|
slide or into another group, a copy into a group, and a notes slide's,
|
|
@@ -506,7 +509,8 @@ run._r.get_or_add_rPr().set("spc", "300") # letter spacing (3pt)
|
|
|
506
509
|
etree.SubElement(text_frame._bodyPr, qn("a:normAutofit"))
|
|
507
510
|
```
|
|
508
511
|
|
|
509
|
-
`text_frame._txBody` (also `_element
|
|
512
|
+
`text_frame._txBody` (also `_element`, and a `p:sp`'s `shape._element.txBody`),
|
|
513
|
+
`text_frame._bodyPr`, `paragraph._p`
|
|
510
514
|
(`_element`, `_pPr`, `_defRPr`), `run._r` and `font._rPr` (`_element`) are real
|
|
511
515
|
lxml elements of python-pptx's classes (`pptx.oxml.text.CT_TextBody`,
|
|
512
516
|
`CT_TextParagraph`, ...), one tree per text frame, built from the deck, with
|
|
@@ -635,7 +639,8 @@ helpers (`get_or_add_tcPr()`, `tc.marL`, `tc.anchor`, `tbl.firstRow`,
|
|
|
635
639
|
- **What is sent** (through the setters the public API uses):
|
|
636
640
|
- a cell's `a:lnL` / `a:lnR` / `a:lnT` / `a:lnB` (`w`, `a:solidFill` with
|
|
637
641
|
an `a:srgbClr`, `a:noFill`, `a:prstDash`) as one `SetCellBorders` per
|
|
638
|
-
cell;
|
|
642
|
+
cell; an edge with no line is an `a:lnX` holding `a:noFill`
|
|
643
|
+
(`cell.border_left.no_border()`), and
|
|
639
644
|
`cell.border_*` reads what the XML set (a later `width` or `dash_style`
|
|
640
645
|
write keeps an edge's `a:noFill` or theme color). On an uploaded table a
|
|
641
646
|
border's color may also be an `a:schemeClr`.
|
|
@@ -661,7 +666,10 @@ helpers (`get_or_add_tcPr()`, `tc.marL`, `tc.anchor`, `tbl.firstRow`,
|
|
|
661
666
|
dashes, arrowheads, a color with modifiers, a theme color on a table the
|
|
662
667
|
SDK added), diagonal borders, `vert`, `anchorCtr` and the other `a:tcPr`
|
|
663
668
|
attributes and children, gradient, picture, pattern and theme-color cell
|
|
664
|
-
fills,
|
|
669
|
+
fills, removing a cell's fill (handing it back to the table style), and
|
|
670
|
+
removing a cell's `a:lnL` / `a:lnR` / `a:lnT` / `a:lnB` (which hands that
|
|
671
|
+
edge back to the table style: the studio stores an edge as a line or as no
|
|
672
|
+
line, so for no line keep the `a:lnX` and give it an `a:noFill`);
|
|
665
673
|
- merges (`gridSpan` / `rowSpan` / `hMerge` / `vMerge`: use `cell.merge()` /
|
|
666
674
|
`cell.split()`), a cell's text (use `cell.text` / `cell.text_frame`), rows
|
|
667
675
|
or columns added or moved through XML (use `table.rows.add()` /
|
|
@@ -1277,6 +1285,24 @@ a later `runs[0].text` edit then wrote the empty run and doubled the text.
|
|
|
1277
1285
|
|
|
1278
1286
|
These properties/methods were added because LLM agents (Claude, GPT, etc.) frequently generate code using these patterns. Without them, agent-generated table code fails at runtime.
|
|
1279
1287
|
|
|
1288
|
+
### `Chart.chart_area` and `Chart.plot_area` formatting
|
|
1289
|
+
|
|
1290
|
+
python-pptx does not expose chart-area or plot-area fill and line formatting.
|
|
1291
|
+
The Athena SDK carries these edits through the chart spec and export patches,
|
|
1292
|
+
so a transparent chart background does not require editing private XML:
|
|
1293
|
+
|
|
1294
|
+
```python
|
|
1295
|
+
chart.chart_area.format.fill.background()
|
|
1296
|
+
chart.plot_area.format.fill.background()
|
|
1297
|
+
```
|
|
1298
|
+
|
|
1299
|
+
Both areas also support solid RGB or theme fills, brightness, line color,
|
|
1300
|
+
width and dash style. Use `chart.chart_area` for the whole chart frame and
|
|
1301
|
+
`chart.plot_area` for the plotting region.
|
|
1302
|
+
These edits apply to classic charts (authored or uploaded); Office 2016
|
|
1303
|
+
chartEx charts, such as waterfall and treemap, refuse them
|
|
1304
|
+
(`chartExEditsRefusal`).
|
|
1305
|
+
|
|
1280
1306
|
### `Font.cap` — PowerPoint-native capitalization mode
|
|
1281
1307
|
|
|
1282
1308
|
The SDK exposes PowerPoint's OOXML run capitalization property as
|
|
@@ -1547,10 +1573,13 @@ slide.shapes.add_picture(img.url, Inches(1), Inches(1), width=Inches(6))
|
|
|
1547
1573
|
|
|
1548
1574
|
### `Picture.image_url` — owner-scoped media URL for cross-deck image copy
|
|
1549
1575
|
|
|
1550
|
-
|
|
1551
|
-
`.
|
|
1552
|
-
|
|
1553
|
-
|
|
1576
|
+
`Picture.image` returns python-pptx's `Image` (`.blob`, `.content_type`,
|
|
1577
|
+
`.ext`, `.filename`, `.sha1`, `.size`, `.dpi`): a picture added in this
|
|
1578
|
+
session carries the bytes it was added from, and a picture read from the deck
|
|
1579
|
+
fetches its bytes once from the studio's `/media` route with the SDK's own
|
|
1580
|
+
credentials (it raises `ValueError("no embedded image")`, as python-pptx
|
|
1581
|
+
does, for a picture with no media). **Additionally**, when a
|
|
1582
|
+
picture is loaded from a server snapshot it carries `image_url`: a
|
|
1554
1583
|
stable `{base_url}/media?key=<assetId>` URL that streams the picture's
|
|
1555
1584
|
bytes. It returns `None` for pictures with no backing media asset (an
|
|
1556
1585
|
SDK-added picture not yet flushed/refreshed, a placeholder with no image,
|
|
@@ -1576,12 +1605,13 @@ dst.save()
|
|
|
1576
1605
|
**Portable code that needs to run against stock python-pptx cannot rely
|
|
1577
1606
|
on `image_url`** — it is an Athena-only accessor.
|
|
1578
1607
|
|
|
1579
|
-
### `name=` kwarg on `add_textbox()` / `add_shape()` / `add_picture()`
|
|
1608
|
+
### `name=` kwarg on `add_textbox()` / `add_shape()` / `add_picture()` / `add_chart()`
|
|
1580
1609
|
|
|
1581
1610
|
Optional keyword for setting a stable shape name at creation. python-pptx
|
|
1582
1611
|
upstream auto-assigns names like `"Rectangle 47"` and only exposes
|
|
1583
1612
|
`shape.name = "..."` as a setter; ours additionally accepts `name=` at
|
|
1584
|
-
construction
|
|
1613
|
+
construction on `add_textbox()`, `add_shape()`, `add_picture()` and
|
|
1614
|
+
`add_chart()`. **Portable code that needs to run against stock python-pptx
|
|
1585
1615
|
should omit `name=` and assign after the fact.**
|
|
1586
1616
|
|
|
1587
1617
|
When `name=` is omitted, Athena's generated name skips any identical name in
|
|
@@ -1599,6 +1629,13 @@ card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE,
|
|
|
1599
1629
|
name="kpi_card_revenue")
|
|
1600
1630
|
```
|
|
1601
1631
|
|
|
1632
|
+
Charts accept the same keyword:
|
|
1633
|
+
|
|
1634
|
+
```python
|
|
1635
|
+
frame = slide.shapes.add_chart(chart_type, left, top, width, height, data,
|
|
1636
|
+
name="quarterly_revenue")
|
|
1637
|
+
```
|
|
1638
|
+
|
|
1602
1639
|
### `Presentation.reorder_slides(new_order)`
|
|
1603
1640
|
|
|
1604
1641
|
Reorder slides by passing a permutation of indices. Already shipped in
|
|
@@ -2008,7 +2045,7 @@ cell.border_top.color.rgb = RGBColor(0x1F, 0x49, 0x7D)
|
|
|
2008
2045
|
cell.border_top.width = Pt(1.5)
|
|
2009
2046
|
cell.border_top.dash_style = "solid"
|
|
2010
2047
|
|
|
2011
|
-
cell.border_bottom.no_border() #
|
|
2048
|
+
cell.border_bottom.no_border() # no line on the bottom edge
|
|
2012
2049
|
```
|
|
2013
2050
|
|
|
2014
2051
|
Each edge stores `colorHex`, `widthEmu`, `dashStyle` and the
|
|
@@ -2024,11 +2061,17 @@ indirectly.
|
|
|
2024
2061
|
|
|
2025
2062
|
## `Table.table_style` is read-only
|
|
2026
2063
|
|
|
2027
|
-
python-pptx
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2064
|
+
python-pptx has no `Table.table_style`: python-pptx code reads a table's
|
|
2065
|
+
style GUID from its XML, the text of `table._tbl.tblPr`'s
|
|
2066
|
+
`<a:tableStyleId>`. The SDK's read-only `Table.table_style` returns that
|
|
2067
|
+
same GUID: an uploaded table's style from the deck, or the one the studio
|
|
2068
|
+
gave a table the SDK added (`add_table()` uses python-pptx's default,
|
|
2069
|
+
`{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}`; `add_linked_table()` takes
|
|
2070
|
+
`table_style_id`). It is None for a table without a style, and
|
|
2071
|
+
`table._tbl` shows the same value. The studio has no command that changes
|
|
2072
|
+
an existing table's style, so assigning the property raises
|
|
2073
|
+
`AttributeError`, and an edit to `a:tableStyleId` through `table._tbl`
|
|
2074
|
+
raises `UnsupportedAttributeError`.
|
|
2032
2075
|
|
|
2033
2076
|
---
|
|
2034
2077
|
|
|
@@ -335,7 +335,9 @@ _TEXT_FRAME = (
|
|
|
335
335
|
_SHAPE = "Use the Shape API: text_frame, fill, line, shadow, left / top / width / height, rotation, delete()."
|
|
336
336
|
_CHART = (
|
|
337
337
|
"Use the Chart API: chart.plots, chart.series, chart.replace_data(chart_data), "
|
|
338
|
-
"chart.has_title / chart.chart_title, chart.value_axis, chart.category_axis."
|
|
338
|
+
"chart.has_title / chart.chart_title, chart.value_axis, chart.category_axis. "
|
|
339
|
+
"For the chart's background use chart.chart_area.format / chart.plot_area.format "
|
|
340
|
+
"(fill.background() makes it transparent)."
|
|
339
341
|
)
|
|
340
342
|
# python-pptx's private lxml attributes that recipes reach for (``series._element``,
|
|
341
343
|
# ``chart._chartSpace``, ...) on objects this SDK has no mirror of. Whatever the
|
|
@@ -423,18 +425,27 @@ def keep_getter_error(obj: Any, name: str, error: UnsupportedAttributeError) ->
|
|
|
423
425
|
pass
|
|
424
426
|
|
|
425
427
|
|
|
428
|
+
def take_getter_error(obj: Any, name: str) -> UnsupportedAttributeError | None:
|
|
429
|
+
"""The error the getter of property ``name`` left on ``obj``
|
|
430
|
+
(:func:`keep_getter_error`), taken off it, for a guard's ``__getattr__`` to
|
|
431
|
+
raise; None when it left none. A class whose own ``__getattr__`` does this
|
|
432
|
+
flags it ``__athena_member_guard__``."""
|
|
433
|
+
try:
|
|
434
|
+
stashed = vars(obj).pop(_GETTER_ERROR, None)
|
|
435
|
+
except TypeError:
|
|
436
|
+
return None
|
|
437
|
+
return stashed[1] if stashed is not None and stashed[0] == name else None
|
|
438
|
+
|
|
439
|
+
|
|
426
440
|
def _make_guard(
|
|
427
441
|
cls: type, members: dict[str, str], guessed: dict[str, str]
|
|
428
442
|
) -> Callable[[Any, str], Any]:
|
|
429
443
|
own = cls.__dict__.get("__getattr__")
|
|
430
444
|
|
|
431
445
|
def __getattr__(self: Any, name: str) -> Any:
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
stashed = None
|
|
436
|
-
if stashed is not None and stashed[0] == name:
|
|
437
|
-
raise stashed[1]
|
|
446
|
+
stashed = take_getter_error(self, name)
|
|
447
|
+
if stashed is not None:
|
|
448
|
+
raise stashed
|
|
438
449
|
reason = members.get(name)
|
|
439
450
|
if reason is not None:
|
|
440
451
|
raise UnsupportedAttributeError(f"{type(self).__name__}.{name}", REASONS[reason])
|
|
@@ -808,6 +808,54 @@ class Client:
|
|
|
808
808
|
except requests.RequestException as e:
|
|
809
809
|
raise ConnectionError(f"Failed to download render from {url}: {e}", url=url)
|
|
810
810
|
|
|
811
|
+
def _download_media(self, *, key: str, max_bytes: int) -> bytes:
|
|
812
|
+
"""Download one deck media object's bytes with this client's credentials.
|
|
813
|
+
|
|
814
|
+
``/media`` runs the same deck-access check as every deck op, so the
|
|
815
|
+
bearer stays on the configured API host and a redirect is refused
|
|
816
|
+
rather than followed. Reads stop at ``max_bytes``.
|
|
817
|
+
"""
|
|
818
|
+
path = "/media"
|
|
819
|
+
url = f"{self.base_url.rstrip('/')}{path}?key={quote(key, safe='')}"
|
|
820
|
+
headers = self._get_headers(path)
|
|
821
|
+
headers.pop("Content-Type", None)
|
|
822
|
+
headers["Accept"] = "*/*"
|
|
823
|
+
try:
|
|
824
|
+
response = self._session.get(
|
|
825
|
+
url,
|
|
826
|
+
headers=headers,
|
|
827
|
+
timeout=self.timeout,
|
|
828
|
+
allow_redirects=False,
|
|
829
|
+
stream=True,
|
|
830
|
+
)
|
|
831
|
+
try:
|
|
832
|
+
if 300 <= response.status_code < 400:
|
|
833
|
+
raise RemoteError(
|
|
834
|
+
message="Media download redirected; refusing to follow it",
|
|
835
|
+
status_code=response.status_code,
|
|
836
|
+
error_code="MEDIA_REDIRECT",
|
|
837
|
+
)
|
|
838
|
+
if not response.ok:
|
|
839
|
+
self._handle_response(response)
|
|
840
|
+
chunks: list[bytes] = []
|
|
841
|
+
received = 0
|
|
842
|
+
for chunk in response.iter_content(chunk_size=65536):
|
|
843
|
+
if not chunk:
|
|
844
|
+
continue
|
|
845
|
+
received += len(chunk)
|
|
846
|
+
if received > max_bytes:
|
|
847
|
+
raise RemoteError(
|
|
848
|
+
message=f"Media object exceeds the {max_bytes}-byte read cap",
|
|
849
|
+
status_code=response.status_code,
|
|
850
|
+
error_code="MEDIA_TOO_LARGE",
|
|
851
|
+
)
|
|
852
|
+
chunks.append(chunk)
|
|
853
|
+
return b"".join(chunks)
|
|
854
|
+
finally:
|
|
855
|
+
response.close()
|
|
856
|
+
except requests.RequestException as e:
|
|
857
|
+
raise ConnectionError(f"Failed to download media from {url}: {e}", url=url) from e
|
|
858
|
+
|
|
811
859
|
# -------------------------------------------------------------------------
|
|
812
860
|
# Deck operations
|
|
813
861
|
# -------------------------------------------------------------------------
|
|
@@ -1542,13 +1542,14 @@ class SetCellBorders(Command):
|
|
|
1542
1542
|
``"dashDot"`` / ``"lgDash"`` / ``"lgDashDot"`` / ``"lgDashDotDot"`` /
|
|
1543
1543
|
``"sysDash"`` / ``"sysDot"`` / ``"sysDashDot"`` / ``"sysDashDotDot"``)
|
|
1544
1544
|
|
|
1545
|
-
Passing ``None`` for an edge
|
|
1545
|
+
Passing ``None`` for an edge gives that edge no line (the studio stores
|
|
1546
|
+
and exports it as ``<a:lnX><a:noFill/></a:lnX>``).
|
|
1546
1547
|
|
|
1547
1548
|
Args:
|
|
1548
1549
|
shape_id: ID of the table shape
|
|
1549
1550
|
row: Row index (0-based)
|
|
1550
1551
|
col: Column index (0-based)
|
|
1551
|
-
border_left: Left-edge border spec, or ``None``
|
|
1552
|
+
border_left: Left-edge border spec, or ``None`` for no line
|
|
1552
1553
|
border_right: Right-edge border spec
|
|
1553
1554
|
border_top: Top-edge border spec
|
|
1554
1555
|
border_bottom: Bottom-edge border spec
|
|
@@ -8,7 +8,7 @@ commands the public API sends (:mod:`pptx.oxml._mirror` runs the lifecycle):
|
|
|
8
8
|
|
|
9
9
|
* ``a:tcPr`` borders ``a:lnL`` / ``a:lnR`` / ``a:lnT`` / ``a:lnB`` (``w``,
|
|
10
10
|
``a:solidFill``, ``a:noFill``, ``a:prstDash``) as one ``SetCellBorders`` per
|
|
11
|
-
cell;
|
|
11
|
+
cell; an edge with no line is an ``a:lnX`` holding ``a:noFill``;
|
|
12
12
|
* ``a:tcPr`` ``marL`` / ``marR`` / ``marT`` / ``marB`` as ``SetCellMargins``,
|
|
13
13
|
``anchor`` as ``SetCellAnchor``, and the cell's fill (an RGB ``a:solidFill``
|
|
14
14
|
or ``a:noFill``) as ``SetTableCell``;
|
|
@@ -18,8 +18,9 @@ commands the public API sends (:mod:`pptx.oxml._mirror` runs the lifecycle):
|
|
|
18
18
|
``a:gridCol`` and its ``a:tc`` in every row) as ``DeleteTableColumn``.
|
|
19
19
|
|
|
20
20
|
The studio has no command that changes an existing table's style, so an edit
|
|
21
|
-
to ``a:tblPr/a:tableStyleId`` raises, as does
|
|
22
|
-
(
|
|
21
|
+
to ``a:tblPr/a:tableStyleId`` raises, as does removing an edge's ``a:lnX``
|
|
22
|
+
(which hands the edge back to the table style) and anything else no command
|
|
23
|
+
carries (merges, rows or columns added through XML, the cells' text).
|
|
23
24
|
"""
|
|
24
25
|
|
|
25
26
|
from __future__ import annotations
|
|
@@ -165,6 +166,11 @@ _ADD = (
|
|
|
165
166
|
"column's formatting), and table.delete_row(i) / table.delete_column(i)."
|
|
166
167
|
)
|
|
167
168
|
_MERGE = "Merge and split cells with cell.merge(other_cell) / cell.split()."
|
|
169
|
+
_EDGE_REMOVED = (
|
|
170
|
+
"Removing a cell's a:lnX hands the edge back to the table style, and the studio has no "
|
|
171
|
+
"command for that: it stores an edge as a line or as no line. For no line, keep the "
|
|
172
|
+
"a:lnX and give it an a:noFill (cell.border_left.no_border())."
|
|
173
|
+
)
|
|
168
174
|
_TEXT = (
|
|
169
175
|
"A cell's text is not edited through XML here: use cell.text, or "
|
|
170
176
|
"cell.text_frame.paragraphs[0].runs[0].font for its formatting."
|
|
@@ -207,11 +213,12 @@ def _fill_type(cell: TableCell) -> str | None:
|
|
|
207
213
|
|
|
208
214
|
def _border_spec(cell: TableCell, edge: str, tag: str) -> dict[str, Any] | None:
|
|
209
215
|
"""An edge's border as the studio stores it (``widthEmu``, ``colorHex`` /
|
|
210
|
-
``schemeColor`` / ``noFill``, ``dashStyle``): what this session wrote
|
|
211
|
-
else the snapshot's; None when the cell has
|
|
216
|
+
``schemeColor`` / ``noFill``, ``dashStyle``): what this session wrote (an
|
|
217
|
+
edge it cleared has no line), else the snapshot's; None when the cell has
|
|
218
|
+
none of its own."""
|
|
212
219
|
written = cell._border_specs
|
|
213
220
|
if written is not None and edge in written:
|
|
214
|
-
return written[edge]
|
|
221
|
+
return written[edge] or {"noFill": True}
|
|
215
222
|
spec = (cell._snapshot_borders or {}).get(tag)
|
|
216
223
|
return spec if isinstance(spec, dict) and spec else None
|
|
217
224
|
|
|
@@ -295,8 +302,9 @@ def _render_tblPr(table: Table) -> Any:
|
|
|
295
302
|
for flag, name, _ in _LOOK:
|
|
296
303
|
if getattr(table, name):
|
|
297
304
|
tblPr.set(flag, "1")
|
|
298
|
-
|
|
299
|
-
|
|
305
|
+
style_id = table._style_id()
|
|
306
|
+
if style_id:
|
|
307
|
+
_sub(tblPr, "a:tableStyleId").text = style_id
|
|
300
308
|
return tblPr
|
|
301
309
|
|
|
302
310
|
|
|
@@ -922,6 +930,8 @@ class TableMirror(XmlMirror):
|
|
|
922
930
|
for edge, tag in _EDGES:
|
|
923
931
|
if base.edges.get(tag) == cur.edges.get(tag):
|
|
924
932
|
continue
|
|
933
|
+
if not cur.edges.get(tag):
|
|
934
|
+
raise unsupported(f"{tcpr_path}/a:{tag}", _EDGE_REMOVED)
|
|
925
935
|
spec, cap = self._edge_spec(cur.tcpr, tag, f"{tcpr_path}/a:{tag}")
|
|
926
936
|
edges[edge] = spec
|
|
927
937
|
warn_cap = warn_cap or cap
|
|
@@ -942,14 +952,12 @@ class TableMirror(XmlMirror):
|
|
|
942
952
|
steps.append(self._plan_fill(cell, cur.tcpr, tcpr_path))
|
|
943
953
|
return steps
|
|
944
954
|
|
|
945
|
-
def _edge_spec(self, tcPr: Any, tag: str, path: str) -> tuple[dict[str, Any]
|
|
946
|
-
"""An edge's ``a:lnX`` as the border spec
|
|
947
|
-
|
|
955
|
+
def _edge_spec(self, tcPr: Any, tag: str, path: str) -> tuple[dict[str, Any], bool]:
|
|
956
|
+
"""An edge's ``a:lnX`` (the cell has one) as the border spec
|
|
957
|
+
``SetCellBorders`` sends, and whether it set a line cap."""
|
|
948
958
|
lines = tcPr.findall(qn(f"a:{tag}")) if tcPr is not None else []
|
|
949
959
|
if len(lines) > 1:
|
|
950
960
|
raise unsupported(path, f"Keep one a:{tag} in an a:tcPr.")
|
|
951
|
-
if not lines:
|
|
952
|
-
return None, False
|
|
953
961
|
return _line_spec(lines[0], path, scheme_colors=self._table._source in _uploaded_sources())
|
|
954
962
|
|
|
955
963
|
def _plan_fill(self, cell: TableCell, tcPr: Any, path: str) -> Callable[[], None]:
|
|
@@ -1078,7 +1086,7 @@ def _line_spec(line: Any, path: str, *, scheme_colors: bool) -> tuple[dict[str,
|
|
|
1078
1086
|
if not spec:
|
|
1079
1087
|
raise unsupported(
|
|
1080
1088
|
path,
|
|
1081
|
-
"A border sets a width (w), a fill or a:prstDash;
|
|
1089
|
+
"A border sets a width (w), a fill or a:prstDash; for no line put an a:noFill in it "
|
|
1082
1090
|
"(cell.border_left.no_border()).",
|
|
1083
1091
|
)
|
|
1084
1092
|
return spec, cap
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""``p:sp``, python-pptx's ``pptx.oxml.shapes.autoshape``.
|
|
2
|
+
|
|
3
|
+
The element of an autoshape, a text box, a freeform or a placeholder:
|
|
4
|
+
``shape._element`` / ``shape._sp``. See :mod:`pptx.oxml.shapes.shared` for what
|
|
5
|
+
it mirrors. Its ``p:txBody`` is the shape's text frame's: ``sp.txBody`` is
|
|
6
|
+
``shape.text_frame._txBody`` (:mod:`pptx.oxml._txbody`), so what a recipe changes
|
|
7
|
+
in it is held and sent as that text's commands.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ..._unsupported import keep_getter_errors
|
|
15
|
+
from ...errors import UnsupportedAttributeError
|
|
16
|
+
from ..ns import qn
|
|
17
|
+
from .shared import BaseShapeElement, _NonVisualProps
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CT_ShapeNonVisual(_NonVisualProps):
|
|
21
|
+
"""``p:nvSpPr``: the shape's non-visual properties (its ``cNvPr``)."""
|
|
22
|
+
|
|
23
|
+
_tag = qn("p:nvSpPr")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CT_Shape(BaseShapeElement):
|
|
27
|
+
"""``p:sp``: an autoshape, text box, freeform or placeholder. Its
|
|
28
|
+
``p:nvSpPr`` (the id and name) and its ``p:txBody`` (the text frame's) are
|
|
29
|
+
mirrored; its ``p:spPr`` and ``p:style`` are not."""
|
|
30
|
+
|
|
31
|
+
_tag = qn("p:sp")
|
|
32
|
+
_nv_class = CT_ShapeNonVisual
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def nvSpPr(self) -> CT_ShapeNonVisual:
|
|
36
|
+
"""The ``p:nvSpPr`` child."""
|
|
37
|
+
return self._nvXxPr # type: ignore[return-value]
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
@keep_getter_errors
|
|
41
|
+
def txBody(self) -> Any:
|
|
42
|
+
"""The ``p:txBody`` child: the shape's text frame's ``_txBody``, the same
|
|
43
|
+
element (``sp.txBody is shape.text_frame._txBody``, as in python-pptx).
|
|
44
|
+
What a recipe changes in it is held and sent before the next command, an
|
|
45
|
+
explicit flush or an API call on that text, and it shows the text's
|
|
46
|
+
current state when requested again. None for a shape without a text frame,
|
|
47
|
+
as python-pptx answers for a ``p:sp`` without a ``p:txBody``."""
|
|
48
|
+
shape = self._shape
|
|
49
|
+
if shape is None:
|
|
50
|
+
raise UnsupportedAttributeError(
|
|
51
|
+
"CT_Shape.txBody",
|
|
52
|
+
"this is a copy (copy.deepcopy()) that is not in a shape tree yet, and the "
|
|
53
|
+
"studio clones a shape with the text its original has when the copy is "
|
|
54
|
+
"inserted: insert the copy, then edit the text of the shape it becomes "
|
|
55
|
+
"(slide.shapes[-1].text_frame).",
|
|
56
|
+
)
|
|
57
|
+
if not shape.has_text_frame:
|
|
58
|
+
return None
|
|
59
|
+
return shape.text_frame._txBody
|
|
60
|
+
|
|
61
|
+
def get_or_add_txBody(self) -> Any:
|
|
62
|
+
"""The ``p:txBody`` (:attr:`txBody`). python-pptx adds one to a ``p:sp``
|
|
63
|
+
without one; here every shape that can hold text has its text frame's,
|
|
64
|
+
and no command gives one to a shape that cannot."""
|
|
65
|
+
txBody = self.txBody
|
|
66
|
+
if txBody is None:
|
|
67
|
+
raise UnsupportedAttributeError(
|
|
68
|
+
"CT_Shape.get_or_add_txBody",
|
|
69
|
+
"this shape has no text frame (shape.has_text_frame is False), and no studio "
|
|
70
|
+
"command gives it one: put the text in a text box or an autoshape.",
|
|
71
|
+
)
|
|
72
|
+
return txBody
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = ["CT_Shape", "CT_ShapeNonVisual"]
|
|
@@ -6,7 +6,8 @@ what python-pptx's delete, z-order and duplicate recipes do with one: read its
|
|
|
6
6
|
``tag``, find it in its tree (``getparent()``, ``getnext()``, ``getprevious()``),
|
|
7
7
|
place another shape beside it (``addnext()``, ``addprevious()``) and copy it
|
|
8
8
|
(``copy.deepcopy()``). Its non-visual properties (``sp.nvSpPr.cNvPr``) give the
|
|
9
|
-
shape's id and name, read-only.
|
|
9
|
+
shape's id and name, read-only. A ``p:sp``'s ``p:txBody`` is its text frame's
|
|
10
|
+
(:class:`~pptx.oxml.shapes.autoshape.CT_Shape`).
|
|
10
11
|
|
|
11
12
|
Edits go through :class:`_ShapeTreeEdit`, one per presentation, which turns
|
|
12
13
|
them into studio commands once they are complete (see
|
|
@@ -20,6 +21,7 @@ import contextlib
|
|
|
20
21
|
from collections.abc import Iterator, Mapping
|
|
21
22
|
from typing import TYPE_CHECKING, Any, ClassVar
|
|
22
23
|
|
|
24
|
+
from ..._unsupported import take_getter_error
|
|
23
25
|
from ...errors import UnsupportedAttributeError
|
|
24
26
|
from ..ns import qn
|
|
25
27
|
|
|
@@ -32,8 +34,9 @@ _MIRRORED: str = (
|
|
|
32
34
|
"athena-python-pptx mirrors the shape-tree XML python-pptx's delete, z-order and "
|
|
33
35
|
"duplicate recipes use: an element's tag, getparent(), getnext() / getprevious(), "
|
|
34
36
|
"addnext() / addprevious() and copy.deepcopy(); the read-only p:cNvPr id and name "
|
|
35
|
-
"(sp.nvSpPr.cNvPr.get('name'));
|
|
36
|
-
"
|
|
37
|
+
"(sp.nvSpPr.cNvPr.get('name')); a p:sp's p:txBody (sp.txBody, its text frame's "
|
|
38
|
+
"_txBody); and the shape tree's len, iteration, indexing, index, remove, del, "
|
|
39
|
+
"insert, append, extend and insert_element_before."
|
|
37
40
|
)
|
|
38
41
|
_SHAPE_API: str = (
|
|
39
42
|
"For the rest use the shape API: shape.text_frame for text, shape.fill / shape.line "
|
|
@@ -55,9 +58,9 @@ _PROPERTY_HINTS: dict[str, str] = {
|
|
|
55
58
|
"Its a:xfrm is not mirrored: use shape.left / top / width / height, rotation, "
|
|
56
59
|
"flip_h and flip_v.",
|
|
57
60
|
),
|
|
58
|
-
|
|
59
|
-
(
|
|
60
|
-
"
|
|
61
|
+
"get_or_add_txBody": (
|
|
62
|
+
"Only a p:sp (an autoshape, text box or placeholder) holds a p:txBody; there "
|
|
63
|
+
"sp.txBody is its text frame's (shape.text_frame._txBody)."
|
|
61
64
|
),
|
|
62
65
|
**dict.fromkeys(
|
|
63
66
|
("blipFill", "blip_rId", "srcRect_l", "srcRect_t", "srcRect_r", "srcRect_b"),
|
|
@@ -369,6 +372,13 @@ class BaseShapeElement:
|
|
|
369
372
|
"""True for a placeholder (a ``p:sp`` / ``p:pic`` / ``p:graphicFrame`` with ``p:ph``)."""
|
|
370
373
|
return bool(self._source.is_placeholder)
|
|
371
374
|
|
|
375
|
+
@property
|
|
376
|
+
def txBody(self) -> Any:
|
|
377
|
+
"""The ``p:txBody`` child: None, as python-pptx answers for every shape
|
|
378
|
+
element but a ``p:sp``, the only one that holds one (there it is the
|
|
379
|
+
text frame's: :class:`~pptx.oxml.shapes.autoshape.CT_Shape`)."""
|
|
380
|
+
return None
|
|
381
|
+
|
|
372
382
|
# -- its place in a tree -------------------------------------------------
|
|
373
383
|
|
|
374
384
|
def _edit(self) -> _ShapeTreeEdit:
|
|
@@ -514,8 +524,15 @@ class BaseShapeElement:
|
|
|
514
524
|
def __getattr__(self, name: str) -> Any:
|
|
515
525
|
if name.startswith("__"):
|
|
516
526
|
raise AttributeError(name)
|
|
527
|
+
# The explanation a property getter raised (keep_getter_errors), which
|
|
528
|
+
# Python would otherwise replace with this lookup's.
|
|
529
|
+
kept = take_getter_error(self, name)
|
|
530
|
+
if kept is not None:
|
|
531
|
+
raise kept
|
|
517
532
|
raise unsupported(f"{type(self).__name__}.{name}", name)
|
|
518
533
|
|
|
534
|
+
__getattr__.__athena_member_guard__ = True # type: ignore[attr-defined]
|
|
535
|
+
|
|
519
536
|
def __repr__(self) -> str:
|
|
520
537
|
return f"<{type(self).__name__} '<{nsptag(self.tag)}>' at 0x{id(self):x}>"
|
|
521
538
|
|