athena-python-pptx 0.17.0__tar.gz → 0.17.1__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 (108) hide show
  1. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/CHANGELOG.md +29 -0
  2. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/PKG-INFO +1 -1
  3. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/docs/API_PARITY_EXCEPTIONS.md +25 -11
  4. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/__init__.py +1 -1
  5. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/_unsupported.py +15 -6
  6. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/commands.py +3 -2
  7. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/_tbl.py +22 -14
  8. athena_python_pptx-0.17.1/pptx/oxml/shapes/autoshape.py +75 -0
  9. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/shapes/shared.py +23 -6
  10. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shapes/__init__.py +42 -30
  11. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pyproject.toml +1 -1
  12. athena_python_pptx-0.17.0/pptx/oxml/shapes/autoshape.py +0 -32
  13. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/.gitignore +0 -0
  14. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/AGENTS.md +0 -0
  15. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/API_PARITY_REPORT.md +0 -0
  16. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/CLAUDE.md +0 -0
  17. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/DEV-GUIDE.md +0 -0
  18. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/PARITY_QUESTIONS.md +0 -0
  19. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/README.md +0 -0
  20. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/docs/athena-api.json +0 -0
  21. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/docs/athena-api.md +0 -0
  22. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/_athena_extension.py +0 -0
  23. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/_citations.py +0 -0
  24. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/_ptc.py +0 -0
  25. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/_references.py +0 -0
  26. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/action.py +0 -0
  27. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/batching.py +0 -0
  28. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/__init__.py +0 -0
  29. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/axis.py +0 -0
  30. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/category.py +0 -0
  31. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/chart.py +0 -0
  32. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/data.py +0 -0
  33. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/datalabel.py +0 -0
  34. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/legend.py +0 -0
  35. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/marker.py +0 -0
  36. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/plot.py +0 -0
  37. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/point.py +0 -0
  38. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/series.py +0 -0
  39. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/chart/xlsx.py +0 -0
  40. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/client.py +0 -0
  41. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/decorators.py +0 -0
  42. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/dml/__init__.py +0 -0
  43. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/dml/chtfmt.py +0 -0
  44. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/dml/color.py +0 -0
  45. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/dml/effect.py +0 -0
  46. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/dml/fill.py +0 -0
  47. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/dml/line.py +0 -0
  48. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/docgen.py +0 -0
  49. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/enum/__init__.py +0 -0
  50. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/enum/action.py +0 -0
  51. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/enum/chart.py +0 -0
  52. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/enum/dml.py +0 -0
  53. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/enum/lang.py +0 -0
  54. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/enum/shapes.py +0 -0
  55. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/enum/text.py +0 -0
  56. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/errors.py +0 -0
  57. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/exc.py +0 -0
  58. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/generated/__init__.py +0 -0
  59. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/generated/autoshape_types.py +0 -0
  60. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/kit.py +0 -0
  61. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/media.py +0 -0
  62. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/__init__.py +0 -0
  63. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/_mirror.py +0 -0
  64. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/_txbody.py +0 -0
  65. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/ns.py +0 -0
  66. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/presentation.py +0 -0
  67. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/shapes/__init__.py +0 -0
  68. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/shapes/connector.py +0 -0
  69. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/shapes/graphfrm.py +0 -0
  70. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/shapes/groupshape.py +0 -0
  71. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/shapes/picture.py +0 -0
  72. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/simpletypes.py +0 -0
  73. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/table.py +0 -0
  74. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/text.py +0 -0
  75. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/oxml/xmlchemy.py +0 -0
  76. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/package.py +0 -0
  77. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/parts/__init__.py +0 -0
  78. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/parts/_base.py +0 -0
  79. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/parts/chart.py +0 -0
  80. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/parts/coreprops.py +0 -0
  81. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/parts/embeddedpackage.py +0 -0
  82. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/parts/image.py +0 -0
  83. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/parts/media.py +0 -0
  84. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/parts/presentation.py +0 -0
  85. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/parts/slide.py +0 -0
  86. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/presentation.py +0 -0
  87. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shapes/autoshape.py +0 -0
  88. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shapes/base.py +0 -0
  89. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shapes/connector.py +0 -0
  90. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shapes/freeform.py +0 -0
  91. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shapes/graphfrm.py +0 -0
  92. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shapes/group.py +0 -0
  93. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shapes/picture.py +0 -0
  94. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shapes/placeholder.py +0 -0
  95. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shapes/shapetree.py +0 -0
  96. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/shared.py +0 -0
  97. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/slide.py +0 -0
  98. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/slides.py +0 -0
  99. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/spec.py +0 -0
  100. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/table.py +0 -0
  101. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/text/__init__.py +0 -0
  102. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/text/fonts.py +0 -0
  103. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/text/layout.py +0 -0
  104. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/text/text.py +0 -0
  105. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/types.py +0 -0
  106. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/typing.py +0 -0
  107. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/units.py +0 -0
  108. {athena_python_pptx-0.17.0 → athena_python_pptx-0.17.1}/pptx/util.py +0 -0
@@ -2,6 +2,35 @@
2
2
 
3
3
  All notable changes to `athena-python-pptx` are documented in this file.
4
4
 
5
+ ## 0.17.1
6
+
7
+ - `Table.table_style` returns the table's style GUID, the text of
8
+ `table._tbl`'s `a:tableStyleId`, which python-pptx code reads: an uploaded
9
+ table's from the deck, or the one `add_table()` / `add_linked_table()` gave a
10
+ table the SDK added; None for a table without one. It returned None for every
11
+ table (it read a snapshot field the studio never sends). It stays read-only:
12
+ the studio has no command that changes an existing table's style.
13
+ - `shape._element.txBody` (and `shape._sp.txBody`) of an autoshape, text box
14
+ or placeholder is its text frame's `_txBody`, the same lxml element
15
+ (`sp.txBody is shape.text_frame._txBody`, as in python-pptx), and
16
+ `sp.get_or_add_txBody()` returns it; it raised `UnsupportedAttributeError`.
17
+ Edits through it are held and sent as the text's commands, as edits through
18
+ `text_frame._txBody` are, so python-pptx's "for every shape, rewrite each
19
+ `a:t` of `shape._element.txBody`" recipe works. As in python-pptx it is None
20
+ for a picture's, graphic frame's, connector's or group's element, and for a
21
+ shape without a text frame.
22
+ - A table cell's cleared edge (`cell.border_left.no_border()`, or
23
+ `SetCellBorders` with `None`) reads in `cell._tc` as an `a:lnX` holding
24
+ `a:noFill` on every table, as the studio now stores and exports it (an
25
+ uploaded table's read as no `a:lnX`, which in PowerPoint is the table
26
+ style's line). A width or dash written after `no_border()` keeps the edge
27
+ without a line, as it does an `a:noFill` an XML recipe wrote, and an RGB
28
+ color draws it again (it sent a width-only edge). Removing a cell's `a:lnX`
29
+ through `cell._tc` raises
30
+ `UnsupportedAttributeError`: in python-pptx that hands the edge back to the
31
+ table style, which no studio command does (it cleared the edge before). For
32
+ no line, keep the `a:lnX` and give it an `a:noFill`, or call `no_border()`.
33
+
5
34
  ## 0.17.0
6
35
 
7
36
  - 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.17.0
3
+ Version: 0.17.1
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`, and its read-only `p:cNvPr` (`sp.nvSpPr.cNvPr`,
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`, `txBody`, `xfrm`), XML built
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`), `text_frame._bodyPr`, `paragraph._p`
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; removing an edge clears it (`cell.border_left.no_border()`), and
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, and removing a cell's fill (handing it back to the table style);
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()` /
@@ -2008,7 +2016,7 @@ cell.border_top.color.rgb = RGBColor(0x1F, 0x49, 0x7D)
2008
2016
  cell.border_top.width = Pt(1.5)
2009
2017
  cell.border_top.dash_style = "solid"
2010
2018
 
2011
- cell.border_bottom.no_border() # remove the bottom edge
2019
+ cell.border_bottom.no_border() # no line on the bottom edge
2012
2020
  ```
2013
2021
 
2014
2022
  Each edge stores `colorHex`, `widthEmu`, `dashStyle` and the
@@ -2024,11 +2032,17 @@ indirectly.
2024
2032
 
2025
2033
  ## `Table.table_style` is read-only
2026
2034
 
2027
- python-pptx exposes `Table.table_style` as a writable property that
2028
- swaps the OOXML `<a:tableStyleId>` GUID. Our SDK exposes it as read-only
2029
- today. Pass `table_style` to `slide.shapes.add_table(table_style=...)`
2030
- at creation time to choose the style; modifying it on an existing table
2031
- will be unlocked when the server gains a `SetTableStyle` command.
2035
+ python-pptx has no `Table.table_style`: python-pptx code reads a table's
2036
+ style GUID from its XML, the text of `table._tbl.tblPr`'s
2037
+ `<a:tableStyleId>`. The SDK's read-only `Table.table_style` returns that
2038
+ same GUID: an uploaded table's style from the deck, or the one the studio
2039
+ gave a table the SDK added (`add_table()` uses python-pptx's default,
2040
+ `{5C22544A-7EE6-4342-B048-85BDC9FD1C3A}`; `add_linked_table()` takes
2041
+ `table_style_id`). It is None for a table without a style, and
2042
+ `table._tbl` shows the same value. The studio has no command that changes
2043
+ an existing table's style, so assigning the property raises
2044
+ `AttributeError`, and an edit to `a:tableStyleId` through `table._tbl`
2045
+ raises `UnsupportedAttributeError`.
2032
2046
 
2033
2047
  ---
2034
2048
 
@@ -327,7 +327,7 @@ def validate_all_touched() -> dict[int, list[str]]:
327
327
  return results
328
328
 
329
329
 
330
- __version__ = "0.17.0"
330
+ __version__ = "0.17.1"
331
331
 
332
332
  __all__ = [
333
333
  # Main entry point
@@ -423,18 +423,27 @@ def keep_getter_error(obj: Any, name: str, error: UnsupportedAttributeError) ->
423
423
  pass
424
424
 
425
425
 
426
+ def take_getter_error(obj: Any, name: str) -> UnsupportedAttributeError | None:
427
+ """The error the getter of property ``name`` left on ``obj``
428
+ (:func:`keep_getter_error`), taken off it, for a guard's ``__getattr__`` to
429
+ raise; None when it left none. A class whose own ``__getattr__`` does this
430
+ flags it ``__athena_member_guard__``."""
431
+ try:
432
+ stashed = vars(obj).pop(_GETTER_ERROR, None)
433
+ except TypeError:
434
+ return None
435
+ return stashed[1] if stashed is not None and stashed[0] == name else None
436
+
437
+
426
438
  def _make_guard(
427
439
  cls: type, members: dict[str, str], guessed: dict[str, str]
428
440
  ) -> Callable[[Any, str], Any]:
429
441
  own = cls.__dict__.get("__getattr__")
430
442
 
431
443
  def __getattr__(self: Any, name: str) -> Any:
432
- try:
433
- stashed = vars(self).pop(_GETTER_ERROR, None)
434
- except TypeError:
435
- stashed = None
436
- if stashed is not None and stashed[0] == name:
437
- raise stashed[1]
444
+ stashed = take_getter_error(self, name)
445
+ if stashed is not None:
446
+ raise stashed
438
447
  reason = members.get(name)
439
448
  if reason is not None:
440
449
  raise UnsupportedAttributeError(f"{type(self).__name__}.{name}", REASONS[reason])
@@ -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 removes that edge's border.
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`` to clear
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; removing an edge's ``a:lnX`` clears that border;
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 anything else no command carries
22
- (merges, rows or columns added through XML, the cells' text).
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 none of its own."""
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
- if table._table_style_id:
299
- _sub(tblPr, "a:tableStyleId").text = table._table_style_id
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] | None, bool]:
946
- """An edge's ``a:lnX`` as the border spec ``SetCellBorders`` sends (None: no
947
- border of its own), and whether it set a line cap."""
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; remove the a:lnX to clear the border "
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')); and the shape tree's len, iteration, indexing, "
36
- "index, remove, del, insert, append, extend and insert_element_before."
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
- **dict.fromkeys(
59
- ("txBody", "get_or_add_txBody"),
60
- "Its p:txBody is not mirrored: use shape.text_frame (paragraphs, runs, font).",
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
 
@@ -13703,19 +13703,17 @@ _CELL_EDGE_FIELDS: tuple[tuple[str, str], ...] = (
13703
13703
  )
13704
13704
 
13705
13705
 
13706
- def _snapshot_cell_borders(
13707
- data: dict[str, Any], tcpr: dict[str, Any], *, uploaded: bool
13708
- ) -> Optional[dict[str, Any]]:
13706
+ def _snapshot_cell_borders(data: dict[str, Any], tcpr: dict[str, Any]) -> Optional[dict[str, Any]]:
13709
13707
  """The cell's own borders as the snapshot records them, keyed by the
13710
13708
  ``a:tcPr`` child each is (``lnL`` ... ``lnB``, the diagonals), for
13711
13709
  ``cell._tc``.
13712
13710
 
13713
13711
  The edges are the cell's ``border*`` fields: ingest sets them from the
13714
- cell's own ``a:lnX`` and a SetCellBorders write sets its edge (a cleared
13715
- one to ``noFill``, which a table the SDK built exports). An uploaded
13716
- table exports a cleared edge as no ``a:lnX`` at all: the write's
13717
- ``tcPr.borders`` holds it as null. The diagonals ride on
13718
- ``tcPr.borders`` only.
13712
+ cell's own ``a:lnX`` and a SetCellBorders write sets its edge, a cleared
13713
+ one to ``noFill``, which the export writes as ``<a:lnX><a:noFill/></a:lnX>``
13714
+ (``tcPr.borders`` holds it as ``noFill`` too, or as null in a deck edited
13715
+ before the studio stored it so). The diagonals ride on ``tcPr.borders``
13716
+ only.
13719
13717
  """
13720
13718
  borders: dict[str, Any] = {
13721
13719
  tag: data[field]
@@ -13727,8 +13725,6 @@ def _snapshot_cell_borders(
13727
13725
  for tag, spec in written.items():
13728
13726
  if tag in ("lnTlToBr", "lnBlToTr") and isinstance(spec, dict) and spec:
13729
13727
  borders[tag] = spec
13730
- elif uploaded and spec is None and tag in borders:
13731
- del borders[tag]
13732
13728
  return borders or None
13733
13729
 
13734
13730
 
@@ -13809,7 +13805,7 @@ def _hydrate_table_cell(cell: "TableCell", data: dict[str, Any], *, uploaded: bo
13809
13805
  elif isinstance(fill, dict) and fill.get("type") == "complexFill":
13810
13806
  cell._snapshot_fill_type = _CELL_COMPLEX_FILL_KINDS.get(fill.get("kind"), "gradient")
13811
13807
  # What cell._tc shows of the rest of the cell's own a:tcPr.
13812
- cell._snapshot_borders = _snapshot_cell_borders(data, tcpr, uploaded=uploaded)
13808
+ cell._snapshot_borders = _snapshot_cell_borders(data, tcpr)
13813
13809
  cell._snapshot_tcpr_attrs = _snapshot_tcpr_attrs(tcpr)
13814
13810
  # A link written with SetTableCell.hyperlinkUrl is kept on the cell (the
13815
13811
  # export puts it on the runs); an uploaded one is on its run.
@@ -14496,7 +14492,7 @@ class _CellBorder:
14496
14492
  server applier stores in ``cell.tcPr.borders`` and the export-worker
14497
14493
  writes to ``<a:lnL>`` / ``<a:lnR>`` / ``<a:lnT>`` / ``<a:lnB>``).
14498
14494
 
14499
- Calling :meth:`no_border` removes the edge entirely.
14495
+ Calling :meth:`no_border` gives the edge no line.
14500
14496
  """
14501
14497
 
14502
14498
  _DASH_STYLES = {
@@ -14549,12 +14545,15 @@ class _CellBorder:
14549
14545
  self._emit()
14550
14546
 
14551
14547
  def no_border(self) -> None:
14552
- """Remove this edge's border entirely."""
14548
+ """Give this edge no line: the studio draws none and exports the edge as
14549
+ ``<a:lnX><a:noFill/></a:lnX>``, so the table style's line does not show
14550
+ there either. A later width or dash write keeps it without a line, as
14551
+ it does an ``a:noFill`` an XML recipe wrote; an RGB color draws it."""
14553
14552
  self._color_hex = None
14554
14553
  self._width_emu = None
14555
14554
  self._dash_style = None
14556
14555
  self._scheme_color = None
14557
- self._no_fill = False
14556
+ self._no_fill = True
14558
14557
  self._cell._emit_border_change(self._edge, None)
14559
14558
 
14560
14559
  def _emit(self) -> None:
@@ -15159,10 +15158,11 @@ class TableCell:
15159
15158
  if existing is not None:
15160
15159
  return existing
15161
15160
  adapter = _CellBorder(cell=self, edge=edge)
15162
- # A border this session sent through cell._tc reads back here.
15163
- spec = (self._border_specs or {}).get(edge)
15164
- if spec:
15165
- _adopt_border_spec(adapter, spec)
15161
+ # A border this session sent (through cell._tc or a cleared edge's
15162
+ # no line) reads back here.
15163
+ specs = self._border_specs or {}
15164
+ if edge in specs:
15165
+ _adopt_border_spec(adapter, specs[edge] or {"noFill": True})
15166
15166
  setattr(self, cache_attr, adapter)
15167
15167
  return adapter
15168
15168
 
@@ -15191,14 +15191,15 @@ class TableCell:
15191
15191
  ``"top"`` / ``"bottom"`` -> border spec; None clears the edge).
15192
15192
 
15193
15193
  Each edge's spec is kept for ``cell._tc`` and for its ``border_*``
15194
- adapter, so a border sent through either reads back through both.
15194
+ adapter, so a border sent through either reads back through both. A
15195
+ cleared edge is one with no line (the studio stores it so).
15195
15196
  """
15196
15197
  specs = dict(self._border_specs or {})
15197
15198
  for edge, spec in edges.items():
15198
15199
  specs[edge] = dict(spec) if spec else None
15199
15200
  adapter = getattr(self, f"_border_{edge}_cached", None)
15200
15201
  if adapter is not None:
15201
- _adopt_border_spec(adapter, spec or {})
15202
+ _adopt_border_spec(adapter, spec or {"noFill": True})
15202
15203
  self._border_specs = specs
15203
15204
  if self._table._buffer is None:
15204
15205
  return
@@ -17439,16 +17440,27 @@ class Table(Shape):
17439
17440
 
17440
17441
  @property
17441
17442
  def table_style(self) -> Optional[str]:
17442
- """Table style GUID (read-only mirror of the ingested PPTX value).
17443
-
17444
- python-pptx exposes a writable ``Table.table_style`` that swaps the
17445
- OOXML ``<a:tableStyleId>`` GUID. Our SDK exposes the value as
17446
- read-only today — see API_PARITY_EXCEPTIONS.md for the pending
17447
- ``SetTableStyle`` server command. Newly-created tables can pass
17448
- ``table_style`` to ``shapes.add_table(...)`` to choose the style
17449
- at creation time.
17450
- """
17451
- return self._properties.get("tableStyleId")
17443
+ """The table style's GUID, the text of the table's ``a:tableStyleId``;
17444
+ None for a table without one.
17445
+
17446
+ python-pptx has no such property: its code reads the GUID from the
17447
+ table's XML (``table._tbl``'s ``a:tblPr``), which shows the same value.
17448
+ It is the style the deck's table has (an uploaded table's, from the
17449
+ deck), or the one ``add_table()`` / ``add_linked_table()`` gave a new
17450
+ table. Read-only: the studio sets a table's style when the table is
17451
+ added and has no command that changes it.
17452
+ """
17453
+ return self._style_id()
17454
+
17455
+ def _style_id(self) -> Optional[str]:
17456
+ """The ``a:tableStyleId`` GUID, or None: the one the deck's table data or
17457
+ the add command gave this table ("" is none), else, for a table whose
17458
+ data names none, the snapshot properties' ``tableStyleId``."""
17459
+ style_id = self._table_style_id
17460
+ if style_id is None:
17461
+ stored = self._properties.get("tableStyleId")
17462
+ style_id = stored if isinstance(stored, str) else None
17463
+ return style_id or None
17452
17464
 
17453
17465
  def _emit_look_change(self) -> None:
17454
17466
  """Emit a SetTableLook command with the current look flag values."""
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "athena-python-pptx"
7
- version = "0.17.0"
7
+ version = "0.17.1"
8
8
  description = "Drop-in replacement for python-pptx that connects to PPTX Studio for real-time collaboration"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,32 +0,0 @@
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.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from ..ns import qn
11
- from .shared import BaseShapeElement, _NonVisualProps
12
-
13
-
14
- class CT_ShapeNonVisual(_NonVisualProps):
15
- """``p:nvSpPr``: the shape's non-visual properties (its ``cNvPr``)."""
16
-
17
- _tag = qn("p:nvSpPr")
18
-
19
-
20
- class CT_Shape(BaseShapeElement):
21
- """``p:sp``: an autoshape, text box, freeform or placeholder."""
22
-
23
- _tag = qn("p:sp")
24
- _nv_class = CT_ShapeNonVisual
25
-
26
- @property
27
- def nvSpPr(self) -> CT_ShapeNonVisual:
28
- """The ``p:nvSpPr`` child."""
29
- return self._nvXxPr # type: ignore[return-value]
30
-
31
-
32
- __all__ = ["CT_Shape", "CT_ShapeNonVisual"]