python-pptx2 2.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. pptx2/__init__.py +152 -0
  2. pptx2/_color.py +75 -0
  3. pptx2/_slide_importer.py +597 -0
  4. pptx2/_svg.py +155 -0
  5. pptx2/_template_applier.py +292 -0
  6. pptx2/_textstyle.py +187 -0
  7. pptx2/accessibility.py +365 -0
  8. pptx2/action.py +270 -0
  9. pptx2/animation.py +2237 -0
  10. pptx2/api.py +49 -0
  11. pptx2/audit.py +258 -0
  12. pptx2/chart/__init__.py +0 -0
  13. pptx2/chart/analytics.py +381 -0
  14. pptx2/chart/axis.py +543 -0
  15. pptx2/chart/category.py +200 -0
  16. pptx2/chart/chart.py +670 -0
  17. pptx2/chart/data.py +864 -0
  18. pptx2/chart/datalabel.py +406 -0
  19. pptx2/chart/legend.py +86 -0
  20. pptx2/chart/marker.py +70 -0
  21. pptx2/chart/palettes.py +129 -0
  22. pptx2/chart/plot.py +462 -0
  23. pptx2/chart/point.py +101 -0
  24. pptx2/chart/quick_layouts.py +325 -0
  25. pptx2/chart/series.py +334 -0
  26. pptx2/chart/xlsx.py +272 -0
  27. pptx2/chart/xmlwriter.py +1845 -0
  28. pptx2/compose/__init__.py +28 -0
  29. pptx2/compose/from_spec.py +1094 -0
  30. pptx2/design/__init__.py +8 -0
  31. pptx2/design/components.py +607 -0
  32. pptx2/design/figures.py +389 -0
  33. pptx2/design/layout.py +370 -0
  34. pptx2/design/recipes.py +1967 -0
  35. pptx2/design/style.py +209 -0
  36. pptx2/design/tokens.py +915 -0
  37. pptx2/diagrams.py +754 -0
  38. pptx2/dml/__init__.py +0 -0
  39. pptx2/dml/chtfmt.py +40 -0
  40. pptx2/dml/color.py +496 -0
  41. pptx2/dml/effect.py +909 -0
  42. pptx2/dml/fill.py +691 -0
  43. pptx2/dml/line.py +287 -0
  44. pptx2/dml/picture.py +212 -0
  45. pptx2/dml/three_d.py +381 -0
  46. pptx2/enum/__init__.py +0 -0
  47. pptx2/enum/action.py +71 -0
  48. pptx2/enum/animation.py +31 -0
  49. pptx2/enum/base.py +218 -0
  50. pptx2/enum/chart.py +574 -0
  51. pptx2/enum/dml.py +740 -0
  52. pptx2/enum/lang.py +685 -0
  53. pptx2/enum/presentation.py +133 -0
  54. pptx2/enum/shapes.py +1029 -0
  55. pptx2/enum/text.py +230 -0
  56. pptx2/exc.py +42 -0
  57. pptx2/formats.py +139 -0
  58. pptx2/geometry.py +420 -0
  59. pptx2/inherit.py +109 -0
  60. pptx2/lint.py +2256 -0
  61. pptx2/math.py +177 -0
  62. pptx2/media.py +197 -0
  63. pptx2/opc/__init__.py +0 -0
  64. pptx2/opc/constants.py +332 -0
  65. pptx2/opc/oxml.py +188 -0
  66. pptx2/opc/package.py +762 -0
  67. pptx2/opc/packuri.py +109 -0
  68. pptx2/opc/serialized.py +296 -0
  69. pptx2/opc/shared.py +20 -0
  70. pptx2/opc/spec.py +45 -0
  71. pptx2/oxml/__init__.py +555 -0
  72. pptx2/oxml/action.py +53 -0
  73. pptx2/oxml/chart/__init__.py +0 -0
  74. pptx2/oxml/chart/axis.py +337 -0
  75. pptx2/oxml/chart/chart.py +481 -0
  76. pptx2/oxml/chart/datalabel.py +253 -0
  77. pptx2/oxml/chart/legend.py +72 -0
  78. pptx2/oxml/chart/marker.py +61 -0
  79. pptx2/oxml/chart/plot.py +365 -0
  80. pptx2/oxml/chart/series.py +425 -0
  81. pptx2/oxml/chart/shared.py +220 -0
  82. pptx2/oxml/coreprops.py +288 -0
  83. pptx2/oxml/dml/__init__.py +0 -0
  84. pptx2/oxml/dml/color.py +135 -0
  85. pptx2/oxml/dml/effect.py +213 -0
  86. pptx2/oxml/dml/fill.py +316 -0
  87. pptx2/oxml/dml/line.py +12 -0
  88. pptx2/oxml/dml/three_d.py +110 -0
  89. pptx2/oxml/ns.py +135 -0
  90. pptx2/oxml/presentation.py +313 -0
  91. pptx2/oxml/shapes/__init__.py +19 -0
  92. pptx2/oxml/shapes/autoshape.py +467 -0
  93. pptx2/oxml/shapes/connector.py +107 -0
  94. pptx2/oxml/shapes/graphfrm.py +347 -0
  95. pptx2/oxml/shapes/groupshape.py +329 -0
  96. pptx2/oxml/shapes/picture.py +270 -0
  97. pptx2/oxml/shapes/shared.py +577 -0
  98. pptx2/oxml/simpletypes.py +1027 -0
  99. pptx2/oxml/slide.py +563 -0
  100. pptx2/oxml/table.py +650 -0
  101. pptx2/oxml/text.py +815 -0
  102. pptx2/oxml/theme.py +36 -0
  103. pptx2/oxml/xmlchemy.py +717 -0
  104. pptx2/package.py +222 -0
  105. pptx2/parts/__init__.py +0 -0
  106. pptx2/parts/chart.py +95 -0
  107. pptx2/parts/coreprops.py +167 -0
  108. pptx2/parts/diagram.py +37 -0
  109. pptx2/parts/embeddedpackage.py +93 -0
  110. pptx2/parts/image.py +275 -0
  111. pptx2/parts/media.py +37 -0
  112. pptx2/parts/presentation.py +136 -0
  113. pptx2/parts/slide.py +371 -0
  114. pptx2/presentation.py +408 -0
  115. pptx2/py.typed +0 -0
  116. pptx2/render.py +586 -0
  117. pptx2/section.py +272 -0
  118. pptx2/shapes/__init__.py +26 -0
  119. pptx2/shapes/autoshape.py +442 -0
  120. pptx2/shapes/base.py +1078 -0
  121. pptx2/shapes/connector.py +297 -0
  122. pptx2/shapes/freeform.py +337 -0
  123. pptx2/shapes/graphfrm.py +316 -0
  124. pptx2/shapes/group.py +264 -0
  125. pptx2/shapes/picture.py +422 -0
  126. pptx2/shapes/placeholder.py +468 -0
  127. pptx2/shapes/shapetree.py +2027 -0
  128. pptx2/shared.py +82 -0
  129. pptx2/skill/SKILL.md +450 -0
  130. pptx2/skill/__init__.py +78 -0
  131. pptx2/skill/__main__.py +64 -0
  132. pptx2/skill/references/animations.md +189 -0
  133. pptx2/skill/references/basics.md +421 -0
  134. pptx2/skill/references/charts.md +254 -0
  135. pptx2/skill/references/compose.md +234 -0
  136. pptx2/skill/references/design.md +366 -0
  137. pptx2/skill/references/effects.md +249 -0
  138. pptx2/skill/references/end-to-end-deck.md +231 -0
  139. pptx2/skill/references/geometry-and-arrows.md +334 -0
  140. pptx2/skill/references/lint.md +275 -0
  141. pptx2/skill/references/math.md +86 -0
  142. pptx2/skill/references/picture-effects.md +129 -0
  143. pptx2/skill/references/render.md +151 -0
  144. pptx2/skill/references/smart-art.md +75 -0
  145. pptx2/skill/references/space-aware-authoring.md +249 -0
  146. pptx2/skill/references/tables.md +244 -0
  147. pptx2/skill/references/theme.md +127 -0
  148. pptx2/skill/references/three-d.md +109 -0
  149. pptx2/skill/references/transitions.md +100 -0
  150. pptx2/slide.py +1244 -0
  151. pptx2/smart_art.py +220 -0
  152. pptx2/spec.py +633 -0
  153. pptx2/table.py +1181 -0
  154. pptx2/table_styles.py +184 -0
  155. pptx2/templates/default.pptx +0 -0
  156. pptx2/templates/docx-icon.emf +0 -0
  157. pptx2/templates/generic-icon.emf +0 -0
  158. pptx2/templates/notes.xml +23 -0
  159. pptx2/templates/notesMaster.xml +352 -0
  160. pptx2/templates/pptx-icon.emf +0 -0
  161. pptx2/templates/theme.xml +321 -0
  162. pptx2/templates/xlsx-icon.emf +0 -0
  163. pptx2/text/__init__.py +0 -0
  164. pptx2/text/fonts.py +482 -0
  165. pptx2/text/layout.py +374 -0
  166. pptx2/text/text.py +1272 -0
  167. pptx2/theme.py +721 -0
  168. pptx2/types.py +36 -0
  169. pptx2/util.py +263 -0
  170. python_pptx2-2.13.0.dist-info/METADATA +351 -0
  171. python_pptx2-2.13.0.dist-info/RECORD +175 -0
  172. python_pptx2-2.13.0.dist-info/WHEEL +5 -0
  173. python_pptx2-2.13.0.dist-info/entry_points.txt +3 -0
  174. python_pptx2-2.13.0.dist-info/licenses/LICENSE +22 -0
  175. python_pptx2-2.13.0.dist-info/top_level.txt +1 -0
pptx2/presentation.py ADDED
@@ -0,0 +1,408 @@
1
+ """Main presentation object."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import IO, TYPE_CHECKING, Literal, cast
7
+
8
+ from pptx2.section import Sections
9
+ from pptx2.shared import PartElementProxy
10
+ from pptx2.slide import SlideMasters, Slides
11
+ from pptx2.util import lazyproperty
12
+
13
+ if TYPE_CHECKING:
14
+ from pptx2.enum.presentation import MSO_TRANSITION_TYPE
15
+ from pptx2.lint import LintIssue
16
+ from pptx2.oxml.presentation import CT_Presentation, CT_SlideId
17
+ from pptx2.parts.presentation import PresentationPart
18
+ from pptx2.parts.slide import SlidePart
19
+ from pptx2.slide import NotesMaster, Slide, SlideLayouts
20
+ from pptx2.util import Length
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # Sentinel used by `set_transition` so callers can distinguish "leave the
25
+ # existing value alone" from "explicitly clear it" (which is `None`).
26
+ _UNSET = object()
27
+
28
+ LintOnSaveMode = Literal["off", "warn", "raise"]
29
+
30
+ #: Accepted values for :attr:`Presentation.lint_on_save`.
31
+ _LINT_ON_SAVE_MODES: tuple[LintOnSaveMode, ...] = ("off", "warn", "raise")
32
+
33
+
34
+ class Presentation(PartElementProxy):
35
+ """PresentationML (PML) presentation.
36
+
37
+ Not intended to be constructed directly. Use :func:`pptx2.Presentation` to open or
38
+ create a presentation.
39
+ """
40
+
41
+ _element: CT_Presentation
42
+ part: PresentationPart # pyright: ignore[reportIncompatibleMethodOverride]
43
+
44
+ # ---- `lint_on_save` is a plain instance attribute, not part of the XML.
45
+ # ---- `PresentationPart.presentation` is a `lazyproperty`, so a package
46
+ # ---- hands back this same proxy object every time and the setting sticks
47
+ # ---- for the life of the package. It is deliberately *not* persisted.
48
+ _lint_on_save: LintOnSaveMode = "off"
49
+
50
+ @property
51
+ def core_properties(self):
52
+ """|CoreProperties| instance for this presentation.
53
+
54
+ Provides read/write access to the Dublin Core document properties for the presentation.
55
+ """
56
+ return self.part.core_properties
57
+
58
+ @property
59
+ def notes_master(self) -> NotesMaster:
60
+ """Instance of |NotesMaster| for this presentation.
61
+
62
+ If the presentation does not have a notes master, one is created from a default template
63
+ and returned. The same single instance is returned on each call.
64
+ """
65
+ return self.part.notes_master
66
+
67
+ @property
68
+ def lint_on_save(self) -> LintOnSaveMode:
69
+ """What :meth:`save` does about error-severity lint issues. Read/write.
70
+
71
+ One of:
72
+
73
+ ``"off"`` *(default)*
74
+ No checks are run at save time; :meth:`save` does no lint work at
75
+ all. This is the default so that existing code keeps working
76
+ unchanged.
77
+
78
+ ``"warn"``
79
+ Every slide is linted before the file is written and each
80
+ error-severity issue is logged (stdlib :mod:`logging`, logger
81
+ ``"pptx2.presentation"``). The file is still written.
82
+
83
+ ``"raise"``
84
+ Every slide is linted *before* the file is written and
85
+ :class:`~pptx2.exc.LintError` is raised if any slide has an
86
+ error-severity issue, so a failing deck never reaches disk.
87
+
88
+ Example::
89
+
90
+ prs.lint_on_save = "raise"
91
+ prs.save("deck.pptx") # raises LintError if a shape is off-slide
92
+
93
+ This is a setting on the in-memory |Presentation| object; it is not
94
+ stored in the ``.pptx`` file, so a deck re-opened from disk starts
95
+ out at ``"off"`` again.
96
+
97
+ Raises:
98
+ ValueError: if assigned anything other than ``"off"``, ``"warn"``,
99
+ or ``"raise"``.
100
+ """
101
+ return self._lint_on_save
102
+
103
+ @lint_on_save.setter
104
+ def lint_on_save(self, value: LintOnSaveMode) -> None:
105
+ if value not in _LINT_ON_SAVE_MODES:
106
+ raise ValueError(
107
+ f"lint_on_save must be one of 'off', 'warn', or 'raise', got {value!r}"
108
+ )
109
+ # ---- the `not in` check above narrows `value` to `LintOnSaveMode` ----
110
+ self._lint_on_save = value
111
+
112
+ def save(self, file: str | IO[bytes]):
113
+ """Writes this presentation to `file`.
114
+
115
+ `file` can be either a file-path or a file-like object open for writing bytes.
116
+
117
+ When :attr:`lint_on_save` is ``"warn"`` or ``"raise"``, every slide is
118
+ linted before anything is written; in ``"raise"`` mode a
119
+ :class:`~pptx2.exc.LintError` propagates and no file is written.
120
+ """
121
+ # ---- the default ("off") does no lint work whatsoever ----
122
+ if self._lint_on_save != "off":
123
+ _lint_before_save(self, self._lint_on_save)
124
+ self.part.save(file)
125
+
126
+ def render_thumbnails(self, **kwargs):
127
+ """Render PNG thumbnails for every slide via headless LibreOffice.
128
+
129
+ Thin wrapper around :func:`pptx2.render.render_slide_thumbnails` that
130
+ forwards `out_dir`, `slide_indexes`, `soffice_bin`, `timeout`, and
131
+ `return_bytes` keyword arguments. Requires ``soffice`` (LibreOffice)
132
+ on PATH; raises ``ThumbnailRendererUnavailable`` otherwise.
133
+ """
134
+ from pptx2.render import render_slide_thumbnails
135
+
136
+ return render_slide_thumbnails(self, **kwargs)
137
+
138
+ @property
139
+ def slide_height(self) -> Length | None:
140
+ """Height of slides in this presentation, in English Metric Units (EMU).
141
+
142
+ Returns |None| if no slide width is defined. Read/write.
143
+ """
144
+ sldSz = self._element.sldSz
145
+ if sldSz is None:
146
+ return None
147
+ return sldSz.cy
148
+
149
+ @slide_height.setter
150
+ def slide_height(self, height: Length):
151
+ sldSz = self._element.get_or_add_sldSz()
152
+ sldSz.cy = height
153
+
154
+ @property
155
+ def slide_layouts(self) -> SlideLayouts:
156
+ """|SlideLayouts| collection belonging to the first |SlideMaster| of this presentation.
157
+
158
+ A presentation can have more than one slide master and each master will have its own set
159
+ of layouts. This property is a convenience for the common case where the presentation has
160
+ only a single slide master.
161
+ """
162
+ return self.slide_masters[0].slide_layouts
163
+
164
+ @property
165
+ def slide_master(self):
166
+ """
167
+ First |SlideMaster| object belonging to this presentation. Typically,
168
+ presentations have only a single slide master. This property provides
169
+ simpler access in that common case.
170
+ """
171
+ return self.slide_masters[0]
172
+
173
+ @lazyproperty
174
+ def slide_masters(self) -> SlideMasters:
175
+ """|SlideMasters| collection of slide-masters belonging to this presentation."""
176
+ return SlideMasters(self._element.get_or_add_sldMasterIdLst(), self)
177
+
178
+ @property
179
+ def slide_width(self):
180
+ """
181
+ Width of slides in this presentation, in English Metric Units (EMU).
182
+ Returns |None| if no slide width is defined. Read/write.
183
+ """
184
+ sldSz = self._element.sldSz
185
+ if sldSz is None:
186
+ return None
187
+ return sldSz.cx
188
+
189
+ @slide_width.setter
190
+ def slide_width(self, width: Length):
191
+ sldSz = self._element.get_or_add_sldSz()
192
+ sldSz.cx = width
193
+
194
+ @property
195
+ def theme(self):
196
+ """Return a |Theme| object providing read-only access to the color palette and fonts.
197
+
198
+ Navigates to the theme part of the first slide master, which is where
199
+ Office applications store the active theme. Returns ``None`` if no
200
+ slide master (and therefore no theme) is present.
201
+
202
+ Example::
203
+
204
+ from pptx2.enum.dml import MSO_THEME_COLOR
205
+
206
+ rgb = prs.theme.colors[MSO_THEME_COLOR.ACCENT_1]
207
+ major = prs.theme.fonts.major # e.g. "Calibri"
208
+ """
209
+ return self.slide_master.part.theme
210
+
211
+ @lazyproperty
212
+ def slides(self):
213
+ """|Slides| object containing the slides in this presentation."""
214
+ sldIdLst = self._element.get_or_add_sldIdLst()
215
+ self.part.rename_slide_parts([cast("CT_SlideId", sldId).rId for sldId in sldIdLst])
216
+ return Slides(sldIdLst, self)
217
+
218
+ @property
219
+ def sections(self) -> Sections:
220
+ """|Sections| collection of named slide groupings in this presentation.
221
+
222
+ Sections appear in PowerPoint's outline / slide-sorter pane and are
223
+ stored as a PowerPoint-2010 extension on the presentation part. The
224
+ returned collection supports ``len()``, indexed access, iteration,
225
+ ``.add(name, start_slide_index=None)``, and ``.remove(section)``.
226
+
227
+ Reading this property never modifies the deck; the extension
228
+ container is created only when the first section is added.
229
+ """
230
+ return Sections(self._element, self)
231
+
232
+ def import_slide(
233
+ self,
234
+ source_slide: Slide,
235
+ merge_master: Literal["dedupe", "clone"] = "dedupe",
236
+ ) -> Slide:
237
+ """Copy *source_slide* into this presentation and return the new |Slide|.
238
+
239
+ The imported slide is appended after any existing slides.
240
+
241
+ Parameters
242
+ ----------
243
+ source_slide:
244
+ A |Slide| object from any |Presentation| instance, including this one.
245
+ merge_master:
246
+ Controls how the slide master is handled:
247
+
248
+ ``'dedupe'`` *(default)*
249
+ Reuse an existing master in this presentation if its XML is
250
+ identical to the source master's XML (normalised byte-for-byte
251
+ compare). Clone the master otherwise.
252
+
253
+ ``'clone'``
254
+ Always clone the source master, even if an identical one
255
+ already exists.
256
+
257
+ Returns
258
+ -------
259
+ Slide
260
+ The newly imported slide.
261
+
262
+ Example::
263
+
264
+ src = pptx2.Presentation("source.pptx")
265
+ dst = pptx2.Presentation("dest.pptx")
266
+ new_slide = dst.import_slide(src.slides[0])
267
+ dst.save("merged.pptx")
268
+ """
269
+ from pptx2._slide_importer import import_slide as _import_slide
270
+
271
+ return _import_slide(source_slide.part, self.part, merge_master=merge_master)
272
+
273
+ def apply_template(
274
+ self,
275
+ template_path_or_stream: str | IO[bytes],
276
+ ) -> None:
277
+ """Re-point every slide in this presentation at masters from *template_path_or_stream*.
278
+
279
+ After this call every slide inherits theme, fonts, and colours from
280
+ the template. Slide content (shapes, text, animations) is preserved.
281
+
282
+ The template can be a ``.potx`` or any ``.pptx``/``.pptm`` file — the
283
+ master(s) inside are used regardless of the extension.
284
+
285
+ Layout matching (in priority order):
286
+
287
+ 1. Same ``<p:cSld name="…">`` name.
288
+ 2. Same layout ``type`` attribute (e.g. ``"title"``, ``"obj"``).
289
+ 3. Fall back to the template's first layout.
290
+
291
+ Parameters
292
+ ----------
293
+ template_path_or_stream:
294
+ Path to a ``.potx``/``.pptx`` file, or a file-like object.
295
+
296
+ Example::
297
+
298
+ prs = pptx2.Presentation("deck.pptx")
299
+ prs.apply_template("brand.potx")
300
+ prs.save("branded_deck.pptx")
301
+ """
302
+ from pptx2._template_applier import apply_template as _apply_template
303
+ from pptx2.api import Presentation as _Presentation
304
+
305
+ tpl = _Presentation(template_path_or_stream)
306
+ _apply_template(self.part, tpl.part)
307
+
308
+ def set_transition(
309
+ self,
310
+ kind: "MSO_TRANSITION_TYPE | None" = cast("MSO_TRANSITION_TYPE", _UNSET),
311
+ *,
312
+ duration: int | None = cast(int, _UNSET),
313
+ advance_on_click: bool | None = cast(bool, _UNSET),
314
+ advance_after: int | None = cast(int, _UNSET),
315
+ force: bool = False,
316
+ ) -> None:
317
+ """Apply a transition to every slide in this presentation.
318
+
319
+ Convenience for the common "give the whole deck the same transition"
320
+ case; equivalent to looping over :attr:`slides` and assigning to each
321
+ slide's :attr:`~pptx2.slide.Slide.transition` properties.
322
+
323
+ Any argument left unspecified is left untouched on each slide, so
324
+ partial updates (e.g. only changing ``duration``) are safe::
325
+
326
+ from pptx2.enum.presentation import MSO_TRANSITION
327
+
328
+ prs.set_transition(MSO_TRANSITION.MORPH, duration=750)
329
+
330
+ # later, just bump the duration without disturbing the kind
331
+ prs.set_transition(duration=500)
332
+
333
+ Passing ``kind=None`` clears the transition element on every slide
334
+ (restoring inheritance/defaults). Passing ``duration=None``,
335
+ ``advance_on_click=None``, or ``advance_after=None`` clears that
336
+ individual attribute on every slide.
337
+
338
+ Per-slide overrides are preserved by default
339
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
340
+
341
+ When ``kind`` is supplied, slides that already have an explicit
342
+ transition kind are **left untouched** — the deck-wide call no
343
+ longer silently clobbers an earlier ``slide.transition.kind = …``.
344
+ This is the historical footgun that prompted item 2 in
345
+ ``IMPROVEMENT_PLAN.md``. To restore the old "force every slide to
346
+ match" behaviour, pass ``force=True``::
347
+
348
+ slide_2.transition.kind = MSO_TRANSITION_TYPE.MORPH
349
+ prs.set_transition(MSO_TRANSITION_TYPE.FADE) # slide 2 keeps MORPH
350
+ prs.set_transition(MSO_TRANSITION_TYPE.FADE, force=True) # slide 2 → FADE
351
+
352
+ ``duration``, ``advance_on_click``, and ``advance_after`` are
353
+ always applied to every slide regardless of ``force``; only
354
+ ``kind`` participates in the override-preservation behaviour.
355
+ """
356
+ for slide in self.slides:
357
+ transition = slide.transition
358
+ if kind is not _UNSET:
359
+ # Skip slides that already have an explicit kind unless
360
+ # the caller has opted in to force-overwrite. The
361
+ # ``transition.kind`` getter returns ``None`` only when
362
+ # no ``<p:transition>`` element is present. An explicit
363
+ # ``<p:transition/>`` reads back as
364
+ # ``MSO_TRANSITION_TYPE.NONE`` and counts as an existing
365
+ # explicit choice (the slide author asked for "no
366
+ # transition"), so it is preserved unless ``force=True``.
367
+ #
368
+ # ``kind=None`` on the caller side means "clear" — apply
369
+ # unconditionally so callers can still wipe the
370
+ # transition off every slide in one call without an
371
+ # explicit ``force=True``.
372
+ if kind is None or transition.kind is None or force:
373
+ transition.kind = kind
374
+ if duration is not _UNSET:
375
+ transition.duration = duration
376
+ if advance_on_click is not _UNSET:
377
+ transition.advance_on_click = advance_on_click
378
+ if advance_after is not _UNSET:
379
+ transition.advance_after = advance_after
380
+
381
+
382
+ def _lint_before_save(prs: Presentation, mode: LintOnSaveMode) -> None:
383
+ """Lint every slide of `prs` and warn or raise per `mode`.
384
+
385
+ Mirrors the ``lint`` option of :func:`pptx2.compose.from_spec`: only
386
+ ERROR-severity issues are acted on, and ``"raise"`` reports every offending
387
+ issue in a single :class:`~pptx2.exc.LintError`.
388
+ """
389
+ from pptx2.exc import LintError
390
+ from pptx2.lint import LintSeverity
391
+
392
+ errors: list[tuple[int, LintIssue]] = [
393
+ (idx, issue)
394
+ for idx, slide in enumerate(prs.slides)
395
+ for issue in slide.lint().issues
396
+ if issue.severity == LintSeverity.ERROR
397
+ ]
398
+
399
+ if not errors:
400
+ return
401
+
402
+ if mode == "warn":
403
+ for idx, issue in errors:
404
+ logger.warning("pptx lint: slide %d: %s", idx, issue)
405
+ return
406
+
407
+ msgs = "; ".join(f"slide {idx}: {issue}" for idx, issue in errors)
408
+ raise LintError(f"Lint errors in presentation: {msgs}")
pptx2/py.typed ADDED
File without changes