python-pptx2 2.14.0__py3-none-any.whl → 2.16.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 (39) hide show
  1. pptx2/__init__.py +4 -1
  2. pptx2/_slide_importer.py +35 -21
  3. pptx2/api.py +11 -5
  4. pptx2/audit.py +14 -2
  5. pptx2/chart/data.py +17 -0
  6. pptx2/chart/plot.py +19 -0
  7. pptx2/chart/xmlwriter.py +11 -2
  8. pptx2/enum/text.py +72 -0
  9. pptx2/opc/package.py +12 -11
  10. pptx2/opc/serialized.py +23 -14
  11. pptx2/oxml/__init__.py +6 -0
  12. pptx2/oxml/chart/plot.py +13 -0
  13. pptx2/oxml/customprops.py +170 -0
  14. pptx2/oxml/ns.py +2 -0
  15. pptx2/oxml/presentation.py +47 -0
  16. pptx2/oxml/simpletypes.py +12 -0
  17. pptx2/oxml/slide.py +66 -0
  18. pptx2/oxml/text.py +33 -0
  19. pptx2/package.py +14 -2
  20. pptx2/parts/customprops.py +129 -0
  21. pptx2/parts/image.py +7 -5
  22. pptx2/parts/presentation.py +21 -6
  23. pptx2/parts/slide.py +68 -1
  24. pptx2/presentation.py +15 -2
  25. pptx2/shapes/shapetree.py +45 -4
  26. pptx2/skill/references/basics.md +30 -0
  27. pptx2/skill/references/charts.md +1 -0
  28. pptx2/skill/references/lint.md +9 -0
  29. pptx2/skill/references/tables.md +10 -0
  30. pptx2/slide.py +104 -0
  31. pptx2/table.py +193 -1
  32. pptx2/text/text.py +134 -3
  33. pptx2/util.py +3 -5
  34. {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/METADATA +1 -1
  35. {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/RECORD +39 -37
  36. {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/WHEEL +0 -0
  37. {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/entry_points.txt +0 -0
  38. {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/licenses/LICENSE +0 -0
  39. {python_pptx2-2.14.0.dist-info → python_pptx2-2.16.0.dist-info}/top_level.txt +0 -0
pptx2/__init__.py CHANGED
@@ -35,6 +35,7 @@ from pptx2.opc.constants import CONTENT_TYPE as CT
35
35
  from pptx2.opc.package import PartFactory
36
36
  from pptx2.parts.chart import ChartPart
37
37
  from pptx2.parts.coreprops import CorePropertiesPart
38
+ from pptx2.parts.customprops import CustomPropertiesPart
38
39
  from pptx2.parts.diagram import (
39
40
  DiagramColorsPart,
40
41
  DiagramDataPart,
@@ -56,7 +57,7 @@ from pptx2.parts.slide import (
56
57
  if TYPE_CHECKING:
57
58
  from pptx2.opc.package import Part
58
59
 
59
- __version__ = "2.14.0"
60
+ __version__ = "2.16.0"
60
61
 
61
62
  sys.modules["pptx2.exceptions"] = exceptions
62
63
  del sys
@@ -98,6 +99,7 @@ content_type_to_part_class_map: dict[str, type[Part]] = {
98
99
  CT.PML_TEMPLATE_MAIN: PresentationPart,
99
100
  CT.PML_SLIDESHOW_MAIN: PresentationPart,
100
101
  CT.OPC_CORE_PROPERTIES: CorePropertiesPart,
102
+ CT.OFC_CUSTOM_PROPERTIES: CustomPropertiesPart,
101
103
  CT.PML_NOTES_MASTER: NotesMasterPart,
102
104
  CT.PML_NOTES_SLIDE: NotesSlidePart,
103
105
  CT.PML_SLIDE: SlidePart,
@@ -136,6 +138,7 @@ PartFactory.part_type_for.update(content_type_to_part_class_map)
136
138
  del (
137
139
  ChartPart,
138
140
  CorePropertiesPart,
141
+ CustomPropertiesPart,
139
142
  DiagramColorsPart,
140
143
  DiagramDataPart,
141
144
  DiagramLayoutPart,
pptx2/_slide_importer.py CHANGED
@@ -93,6 +93,37 @@ def import_slide(
93
93
  return importer.run()
94
94
 
95
95
 
96
+ def next_layout_hierarchy_id(prs_part: PresentationPart) -> int:
97
+ """Return a fresh unique id for a `p:sldMasterId` / `p:sldLayoutId` entry.
98
+
99
+ These share one id space starting at 2147483648 (ST_SlideMasterId /
100
+ ST_SlideLayoutId minimum); duplicates across the presentation are a
101
+ repair trigger, so scan every master's layout-id list plus the
102
+ presentation's master-id list for the current maximum. Used both when
103
+ cloning masters (import_slide) and when adding or cloning slide layouts.
104
+ """
105
+ used = [2147483647]
106
+ prs_element = prs_part._element # pyright: ignore[reportPrivateUsage]
107
+ used += [int(v) for v in prs_element.xpath("p:sldMasterIdLst/p:sldMasterId/@id")]
108
+ for dst_master_part in _iter_master_parts(prs_part):
109
+ used += [
110
+ int(v)
111
+ for v in dst_master_part._element.xpath( # pyright: ignore[reportPrivateUsage]
112
+ "p:sldLayoutIdLst/p:sldLayoutId/@id"
113
+ )
114
+ ]
115
+ return max(used) + 1
116
+
117
+
118
+ def _iter_master_parts(prs_part: PresentationPart):
119
+ """Yield each SlideMasterPart referenced from *prs_part*'s `p:sldMasterIdLst`."""
120
+ prs_element = prs_part._element # pyright: ignore[reportPrivateUsage]
121
+ if prs_element.sldMasterIdLst is None:
122
+ return
123
+ for entry in prs_element.sldMasterIdLst.sldMasterId_lst:
124
+ yield prs_part.related_part(entry.rId)
125
+
126
+
96
127
  # ---------------------------------------------------------------------------
97
128
  # Internal implementation
98
129
  # ---------------------------------------------------------------------------
@@ -169,13 +200,9 @@ class _SlideImporter:
169
200
  return dst_master_part
170
201
  return None
171
202
 
172
- def _iter_dst_masters(self): # type: ignore[return]
203
+ def _iter_dst_masters(self):
173
204
  """Yield each SlideMasterPart already in the destination presentation."""
174
- prs_element = self._dst_prs_part._element # pyright: ignore[reportPrivateUsage]
175
- if prs_element.sldMasterIdLst is None:
176
- return
177
- for entry in prs_element.sldMasterIdLst.sldMasterId_lst:
178
- yield self._dst_prs_part.related_part(entry.rId)
205
+ yield from _iter_master_parts(self._dst_prs_part)
179
206
 
180
207
  def _find_or_clone_layout_in_master(
181
208
  self, src_layout_part: SlideLayoutPart, dst_master_part: SlideMasterPart
@@ -325,22 +352,9 @@ class _SlideImporter:
325
352
  def _next_hierarchy_id(self) -> int:
326
353
  """Return a fresh unique id for a `p:sldMasterId` / `p:sldLayoutId` entry.
327
354
 
328
- These share one id space starting at 2147483648 (ST_SlideMasterId /
329
- ST_SlideLayoutId minimum); duplicates across the presentation are a
330
- repair trigger, so scan every master's layout-id list plus the
331
- presentation's master-id list for the current maximum.
355
+ See :func:`next_layout_hierarchy_id`.
332
356
  """
333
- used = [2147483647]
334
- prs_element = self._dst_prs_part._element # pyright: ignore[reportPrivateUsage]
335
- used += [int(v) for v in prs_element.xpath("p:sldMasterIdLst/p:sldMasterId/@id")]
336
- for dst_master_part in self._iter_dst_masters():
337
- used += [
338
- int(v)
339
- for v in dst_master_part._element.xpath( # pyright: ignore[reportPrivateUsage]
340
- "p:sldLayoutIdLst/p:sldLayoutId/@id"
341
- )
342
- ]
343
- return max(used) + 1
357
+ return next_layout_hierarchy_id(self._dst_prs_part)
344
358
 
345
359
  # ------------------------------------------------------------------
346
360
  # Slide copy
pptx2/api.py CHANGED
@@ -18,12 +18,14 @@ if TYPE_CHECKING:
18
18
  from pptx2.parts.presentation import PresentationPart
19
19
 
20
20
 
21
- def Presentation(pptx: str | IO[bytes] | None = None) -> presentation.Presentation:
21
+ def Presentation(
22
+ pptx: str | os.PathLike[str] | IO[bytes] | None = None,
23
+ ) -> presentation.Presentation:
22
24
  """
23
25
  Return a |Presentation| object loaded from *pptx*, where *pptx* can be
24
- either a path to a ``.pptx`` file (a string) or a file-like object. If
25
- *pptx* is missing or ``None``, the built-in default presentation
26
- "template" is loaded.
26
+ either a path to a ``.pptx`` file (a string or ``pathlib.Path``) or a
27
+ file-like object. If *pptx* is missing or ``None``, the built-in default
28
+ presentation "template" is loaded.
27
29
  """
28
30
  if pptx is None:
29
31
  pptx = _default_pptx_path()
@@ -45,5 +47,9 @@ def _default_pptx_path() -> str:
45
47
 
46
48
  def _is_pptx_package(prs_part: PresentationPart):
47
49
  """Return |True| if *prs_part* is a valid main document part, |False| otherwise."""
48
- valid_content_types = (CT.PML_PRESENTATION_MAIN, CT.PML_PRES_MACRO_MAIN)
50
+ valid_content_types = (
51
+ CT.PML_PRESENTATION_MAIN,
52
+ CT.PML_PRES_MACRO_MAIN,
53
+ CT.PML_TEMPLATE_MAIN,
54
+ )
49
55
  return prs_part.content_type in valid_content_types
pptx2/audit.py CHANGED
@@ -30,7 +30,7 @@ The audit is read-only — it never mutates the deck.
30
30
  from __future__ import annotations
31
31
 
32
32
  from dataclasses import dataclass, field
33
- from typing import TYPE_CHECKING, Any
33
+ from typing import TYPE_CHECKING, Any, Iterable
34
34
 
35
35
  if TYPE_CHECKING:
36
36
  from pptx2.api import Presentation
@@ -174,15 +174,27 @@ def audit(
174
174
  *,
175
175
  size_warn_bytes: int = 2_000_000,
176
176
  check_fonts: bool = True,
177
+ extra_safe_fonts: "Iterable[str] | None" = None,
177
178
  ) -> AuditReport:
178
179
  """Walk the deck and return an :class:`AuditReport` summary.
179
180
 
180
181
  Read-only — never mutates the presentation. Each slide is linted
181
182
  in passing-defaults mode; per-slide overrides should be supplied
182
183
  via the slide-level ``slide.lint(...)`` API directly.
184
+
185
+ *extra_safe_fonts* names additional fonts to treat as safe for the
186
+ font-warning probe, on top of the common Windows/macOS/Office set
187
+ (matching is case-insensitive). Use it where the rendering
188
+ environment genuinely ships the font — e.g. ``DejaVu Sans`` in a
189
+ sandbox whose font policy standardizes on it, or a corporate font
190
+ you embed in every deck.
183
191
  """
184
192
  from pptx2.shapes.picture import Picture
185
193
 
194
+ safe_fonts = _COMMON_FONTS
195
+ if extra_safe_fonts is not None:
196
+ safe_fonts = _COMMON_FONTS | frozenset(f.lower() for f in extra_safe_fonts)
197
+
186
198
  report = AuditReport()
187
199
  slides = list(prs.slides)
188
200
  report.total_slides = len(slides)
@@ -249,7 +261,7 @@ def audit(
249
261
  for para in tf.paragraphs:
250
262
  for run in para.runs:
251
263
  name = run.font.name
252
- if name and name.lower() not in _COMMON_FONTS:
264
+ if name and name.lower() not in safe_fonts:
253
265
  report.font_warnings.append((idx, name))
254
266
 
255
267
  if content_shapes == 0:
pptx2/chart/data.py CHANGED
@@ -12,6 +12,7 @@ from pptx2.chart.xlsx import (
12
12
  XyWorkbookWriter,
13
13
  )
14
14
  from pptx2.chart.xmlwriter import ChartXmlWriter
15
+ from pptx2.oxml.simpletypes import ST_FirstSliceAng
15
16
  from pptx2.util import lazyproperty
16
17
 
17
18
 
@@ -268,6 +269,8 @@ class CategoryChartData(_BaseChartData):
268
269
  The corresponding X value is inferred by its position in the sequence.
269
270
  """
270
271
 
272
+ _first_slice_angle = 0
273
+
271
274
  def add_category(self, label):
272
275
  """
273
276
  Return a newly created |data.Category| object having *label* and
@@ -323,6 +326,20 @@ class CategoryChartData(_BaseChartData):
323
326
  """
324
327
  return self._workbook_writer.categories_ref
325
328
 
329
+ @property
330
+ def first_slice_angle(self):
331
+ """
332
+ Read/write int in range 0..359 specifying the angle in degrees of
333
+ the first slice for a doughnut chart created from this chart data,
334
+ clockwise from 12 o'clock. The default is 0, the PowerPoint default.
335
+ """
336
+ return self._first_slice_angle
337
+
338
+ @first_slice_angle.setter
339
+ def first_slice_angle(self, value):
340
+ ST_FirstSliceAng.validate(value)
341
+ self._first_slice_angle = int(value)
342
+
326
343
  def values_ref(self, series):
327
344
  """
328
345
  The Excel worksheet reference to the values for *series* (not
pptx2/chart/plot.py CHANGED
@@ -220,6 +220,25 @@ class DoughnutPlot(_BasePlot):
220
220
  holeSize = self._element.get_or_add_holeSize()
221
221
  holeSize.val = value
222
222
 
223
+ @property
224
+ def first_slice_angle(self):
225
+ """
226
+ Read/write int in range 0..359 specifying the angle in degrees of
227
+ the first slice of this doughnut, clockwise from 12 o'clock. The
228
+ PowerPoint default for a new doughnut chart is 0, which is what this
229
+ property reports when no explicit ``<c:firstSliceAng>`` element is
230
+ present.
231
+ """
232
+ firstSliceAng = self._element.firstSliceAng
233
+ if firstSliceAng is None:
234
+ return 0
235
+ return firstSliceAng.val
236
+
237
+ @first_slice_angle.setter
238
+ def first_slice_angle(self, value):
239
+ firstSliceAng = self._element.get_or_add_firstSliceAng()
240
+ firstSliceAng.val = value
241
+
223
242
 
224
243
  class LinePlot(_BasePlot):
225
244
  """
pptx2/chart/xmlwriter.py CHANGED
@@ -669,7 +669,7 @@ class _DoughnutChartXmlWriter(_BaseChartXmlWriter):
669
669
  ' <c:showBubbleSize val="0"/>\n'
670
670
  ' <c:showLeaderLines val="1"/>\n'
671
671
  " </c:dLbls>\n"
672
- ' <c:firstSliceAng val="0"/>\n'
672
+ ' <c:firstSliceAng val="{firstSliceAng}"/>\n'
673
673
  ' <c:holeSize val="50"/>\n'
674
674
  " </c:doughnutChart>\n"
675
675
  " </c:plotArea>\n"
@@ -693,7 +693,7 @@ class _DoughnutChartXmlWriter(_BaseChartXmlWriter):
693
693
  " </a:p>\n"
694
694
  " </c:txPr>\n"
695
695
  "</c:chartSpace>\n"
696
- ).format(**{"ser_xml": self._ser_xml})
696
+ ).format(**{"ser_xml": self._ser_xml, "firstSliceAng": self._firstSliceAng})
697
697
 
698
698
  @property
699
699
  def _explosion_xml(self):
@@ -701,6 +701,15 @@ class _DoughnutChartXmlWriter(_BaseChartXmlWriter):
701
701
  return ' <c:explosion val="25"/>\n'
702
702
  return ""
703
703
 
704
+ @property
705
+ def _firstSliceAng(self):
706
+ """
707
+ The integer angle in degrees for the ``<c:firstSliceAng>`` element,
708
+ taken from the chart data object when it specifies one and 0
709
+ (the PowerPoint default) otherwise.
710
+ """
711
+ return getattr(self._chart_data, "first_slice_angle", 0)
712
+
704
713
  @property
705
714
  def _ser_xml(self):
706
715
  xml = ""
pptx2/enum/text.py CHANGED
@@ -228,3 +228,75 @@ class PP_PARAGRAPH_ALIGNMENT(BaseXmlEnum):
228
228
 
229
229
 
230
230
  PP_ALIGN = PP_PARAGRAPH_ALIGNMENT
231
+
232
+
233
+ class MSO_TEXT_FIELD_TYPE(BaseXmlEnum):
234
+ """Specifies the kind of value a text field (`a:fld`) displays.
235
+
236
+ Used with :meth:`.Paragraph.add_field` to add a run-like field element to a paragraph.
237
+ The field's displayed text (the actual slide number, the current date, ...) is computed by
238
+ PowerPoint when the slide is rendered; the `a:t` child element holds only the "cached"
239
+ placeholder text readers that do not compute fields will show.
240
+
241
+ Example::
242
+
243
+ from pptx2.enum.text import MSO_TEXT_FIELD_TYPE
244
+
245
+ paragraph.add_field(MSO_TEXT_FIELD_TYPE.SLIDE_NUMBER)
246
+
247
+ The XML tokens are the ST_TextFieldType values DrawingML defines ("slidenum", "datetime",
248
+ "datetime1" through "datetime13") plus "datetimeFigureOut", the token PowerPoint writes for
249
+ an automatically updated date.
250
+ """
251
+
252
+ SLIDE_NUMBER = (1, "slidenum", "The number of the slide the field appears on.")
253
+ """The number of the slide the field appears on."""
254
+
255
+ DATETIME = (2, "datetime", "The date and time in the containing document's default format.")
256
+ """The date and time in the containing document's default format."""
257
+
258
+ DATETIME_1 = (3, "datetime1", "Predefined date/time format 1 (locale-dependent rendering).")
259
+ """Predefined date/time format 1 (locale-dependent rendering)."""
260
+
261
+ DATETIME_2 = (4, "datetime2", "Predefined date/time format 2 (locale-dependent rendering).")
262
+ """Predefined date/time format 2 (locale-dependent rendering)."""
263
+
264
+ DATETIME_3 = (5, "datetime3", "Predefined date/time format 3 (locale-dependent rendering).")
265
+ """Predefined date/time format 3 (locale-dependent rendering)."""
266
+
267
+ DATETIME_4 = (6, "datetime4", "Predefined date/time format 4 (locale-dependent rendering).")
268
+ """Predefined date/time format 4 (locale-dependent rendering)."""
269
+
270
+ DATETIME_5 = (7, "datetime5", "Predefined date/time format 5 (locale-dependent rendering).")
271
+ """Predefined date/time format 5 (locale-dependent rendering)."""
272
+
273
+ DATETIME_6 = (8, "datetime6", "Predefined date/time format 6 (locale-dependent rendering).")
274
+ """Predefined date/time format 6 (locale-dependent rendering)."""
275
+
276
+ DATETIME_7 = (9, "datetime7", "Predefined date/time format 7 (locale-dependent rendering).")
277
+ """Predefined date/time format 7 (locale-dependent rendering)."""
278
+
279
+ DATETIME_8 = (10, "datetime8", "Predefined date/time format 8 (locale-dependent rendering).")
280
+ """Predefined date/time format 8 (locale-dependent rendering)."""
281
+
282
+ DATETIME_9 = (11, "datetime9", "Predefined date/time format 9 (locale-dependent rendering).")
283
+ """Predefined date/time format 9 (locale-dependent rendering)."""
284
+
285
+ DATETIME_10 = (12, "datetime10", "Predefined date/time format 10 (locale-dependent rendering).")
286
+ """Predefined date/time format 10 (locale-dependent rendering)."""
287
+
288
+ DATETIME_11 = (13, "datetime11", "Predefined date/time format 11 (locale-dependent rendering).")
289
+ """Predefined date/time format 11 (locale-dependent rendering)."""
290
+
291
+ DATETIME_12 = (14, "datetime12", "Predefined date/time format 12 (locale-dependent rendering).")
292
+ """Predefined date/time format 12 (locale-dependent rendering)."""
293
+
294
+ DATETIME_13 = (15, "datetime13", "Predefined date/time format 13 (locale-dependent rendering).")
295
+ """Predefined date/time format 13 (locale-dependent rendering)."""
296
+
297
+ DATETIME_FIGURE_OUT = (
298
+ 16,
299
+ "datetimeFigureOut",
300
+ "The current date, in the format PowerPoint figures out automatically.",
301
+ )
302
+ """The current date, in the format PowerPoint figures out automatically."""
pptx2/opc/package.py CHANGED
@@ -7,6 +7,7 @@ presentations to and from a .pptx file.
7
7
  from __future__ import annotations
8
8
 
9
9
  import collections
10
+ import os
10
11
  from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Mapping, Set, cast
11
12
 
12
13
  from pptx2.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
@@ -73,11 +74,11 @@ class OpcPackage(_RelatableMixin):
73
74
  file or file-like object containing a package (.pptx file).
74
75
  """
75
76
 
76
- def __init__(self, pkg_file: str | IO[bytes]):
77
+ def __init__(self, pkg_file: str | os.PathLike[str] | IO[bytes]):
77
78
  self._pkg_file = pkg_file
78
79
 
79
80
  @classmethod
80
- def open(cls, pkg_file: str | IO[bytes]) -> Self:
81
+ def open(cls, pkg_file: str | os.PathLike[str] | IO[bytes]) -> Self:
81
82
  """Return an |OpcPackage| instance loaded with the contents of `pkg_file`."""
82
83
  return cls(pkg_file)._load()
83
84
 
@@ -148,10 +149,10 @@ class OpcPackage(_RelatableMixin):
148
149
  return PackURI(candidate_partname)
149
150
  raise Exception("ProgrammingError: ran out of candidate_partnames") # pragma: no cover
150
151
 
151
- def save(self, pkg_file: str | IO[bytes]) -> None:
152
+ def save(self, pkg_file: str | os.PathLike[str] | IO[bytes]) -> None:
152
153
  """Save this package to `pkg_file`.
153
154
 
154
- `file` can be either a path to a file (a string) or a file-like object.
155
+ `file` can be either a path to a file (a string or `pathlib.Path`) or a file-like object.
155
156
  """
156
157
  PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts()))
157
158
 
@@ -170,13 +171,13 @@ class OpcPackage(_RelatableMixin):
170
171
  class _PackageLoader:
171
172
  """Function-object that loads a package from disk (or other store)."""
172
173
 
173
- def __init__(self, pkg_file: str | IO[bytes], package: Package):
174
+ def __init__(self, pkg_file: str | os.PathLike[str] | IO[bytes], package: Package):
174
175
  self._pkg_file = pkg_file
175
176
  self._package = package
176
177
 
177
178
  @classmethod
178
179
  def load(
179
- cls, pkg_file: str | IO[bytes], package: Package
180
+ cls, pkg_file: str | os.PathLike[str] | IO[bytes], package: Package
180
181
  ) -> tuple[CT_Relationships, dict[PackURI, Part]]:
181
182
  """Return (pkg_xml_rels, parts) pair resulting from loading `pkg_file`.
182
183
 
@@ -360,11 +361,11 @@ class Part(_RelatableMixin):
360
361
  # --- this must be public to allow the part graph to be traversed ---
361
362
  return self._rels
362
363
 
363
- def _blob_from_file(self, file: str | IO[bytes]) -> bytes:
364
- """Return bytes of `file`, which is either a str path or a file-like object."""
365
- # --- a str `file` is assumed to be a path ---
366
- if isinstance(file, str):
367
- with open(file, "rb") as f:
364
+ def _blob_from_file(self, file: str | os.PathLike[str] | IO[bytes]) -> bytes:
365
+ """Return bytes of `file`, which is either a path or a file-like object."""
366
+ # --- a path-like `file` is assumed to be a path ---
367
+ if isinstance(file, (str, os.PathLike)):
368
+ with open(os.fspath(file), "rb") as f:
368
369
  return f.read()
369
370
 
370
371
  # --- otherwise, assume `file` is a file-like object
pptx2/opc/serialized.py CHANGED
@@ -26,7 +26,7 @@ class PackageReader(Container[bytes]):
26
26
  perhaps by unzipping a .pptx file.
27
27
  """
28
28
 
29
- def __init__(self, pkg_file: str | IO[bytes]):
29
+ def __init__(self, pkg_file: str | os.PathLike[str] | IO[bytes]):
30
30
  self._pkg_file = pkg_file
31
31
 
32
32
  def __contains__(self, pack_uri: object) -> bool:
@@ -62,14 +62,22 @@ class PackageWriter:
62
62
  Its single API classmethod is :meth:`write`. This class is not intended to be instantiated.
63
63
  """
64
64
 
65
- def __init__(self, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]):
65
+ def __init__(
66
+ self,
67
+ pkg_file: str | os.PathLike[str] | IO[bytes],
68
+ pkg_rels: _Relationships,
69
+ parts: Sequence[Part],
70
+ ):
66
71
  self._pkg_file = pkg_file
67
72
  self._pkg_rels = pkg_rels
68
73
  self._parts = parts
69
74
 
70
75
  @classmethod
71
76
  def write(
72
- cls, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]
77
+ cls,
78
+ pkg_file: str | os.PathLike[str] | IO[bytes],
79
+ pkg_rels: _Relationships,
80
+ parts: Sequence[Part],
73
81
  ) -> None:
74
82
  """Write a physical package (.pptx file) to `pkg_file`.
75
83
 
@@ -127,21 +135,22 @@ class _PhysPkgReader(Container[PackURI]):
127
135
  )
128
136
 
129
137
  @classmethod
130
- def factory(cls, pkg_file: str | IO[bytes]) -> _PhysPkgReader:
138
+ def factory(cls, pkg_file: str | os.PathLike[str] | IO[bytes]) -> _PhysPkgReader:
131
139
  """Return |_PhysPkgReader| subtype instance appropriage for `pkg_file`."""
132
- # --- for pkg_file other than str, assume it's a stream and pass it to Zip
140
+ # --- for pkg_file other than a path, assume it's a stream and pass it to Zip
133
141
  # --- reader to sort out
134
- if not isinstance(pkg_file, str):
142
+ if not isinstance(pkg_file, (str, os.PathLike)):
135
143
  return _ZipPkgReader(pkg_file)
136
144
 
137
145
  # --- otherwise we treat `pkg_file` as a path ---
138
- if os.path.isdir(pkg_file):
139
- return _DirPkgReader(pkg_file)
146
+ path = os.fspath(pkg_file)
147
+ if os.path.isdir(path):
148
+ return _DirPkgReader(path)
140
149
 
141
- if zipfile.is_zipfile(pkg_file):
142
- return _ZipPkgReader(pkg_file)
150
+ if zipfile.is_zipfile(path):
151
+ return _ZipPkgReader(path)
143
152
 
144
- raise PackageNotFoundError("Package not found at '%s'" % pkg_file)
153
+ raise PackageNotFoundError("Package not found at '%s'" % path)
145
154
 
146
155
 
147
156
  class _DirPkgReader(_PhysPkgReader):
@@ -172,7 +181,7 @@ class _DirPkgReader(_PhysPkgReader):
172
181
  class _ZipPkgReader(_PhysPkgReader):
173
182
  """Implements |PhysPkgReader| interface for a zip-file OPC package."""
174
183
 
175
- def __init__(self, pkg_file: str | IO[bytes]):
184
+ def __init__(self, pkg_file: str | os.PathLike[str] | IO[bytes]):
176
185
  self._pkg_file = pkg_file
177
186
 
178
187
  def __contains__(self, pack_uri: object) -> bool:
@@ -199,7 +208,7 @@ class _PhysPkgWriter:
199
208
  """Base class for physical package writer objects."""
200
209
 
201
210
  @classmethod
202
- def factory(cls, pkg_file: str | IO[bytes]) -> _ZipPkgWriter:
211
+ def factory(cls, pkg_file: str | os.PathLike[str] | IO[bytes]) -> _ZipPkgWriter:
203
212
  """Return |_PhysPkgWriter| subtype instance appropriage for `pkg_file`.
204
213
 
205
214
  Currently the only subtype is `_ZipPkgWriter`, but a `_DirPkgWriter` could be implemented
@@ -217,7 +226,7 @@ class _PhysPkgWriter:
217
226
  class _ZipPkgWriter(_PhysPkgWriter):
218
227
  """Implements |PhysPkgWriter| interface for a zip-file (.pptx file) OPC package."""
219
228
 
220
- def __init__(self, pkg_file: str | IO[bytes]):
229
+ def __init__(self, pkg_file: str | os.PathLike[str] | IO[bytes]):
221
230
  self._pkg_file = pkg_file
222
231
 
223
232
  def __enter__(self) -> _ZipPkgWriter:
pptx2/oxml/__init__.py CHANGED
@@ -213,8 +213,10 @@ register_element_cls("c:xMode", CT_LayoutMode)
213
213
 
214
214
 
215
215
  from pptx2.oxml.coreprops import CT_CoreProperties # noqa: E402
216
+ from pptx2.oxml.customprops import CT_CustomProperties # noqa: E402
216
217
 
217
218
  register_element_cls("cp:coreProperties", CT_CoreProperties)
219
+ register_element_cls("custom:Properties", CT_CustomProperties)
218
220
 
219
221
 
220
222
  from pptx2.oxml.dml.color import ( # noqa: E402
@@ -324,6 +326,8 @@ register_element_cls("a:prstDash", CT_PresetLineDashProperties)
324
326
 
325
327
 
326
328
  from pptx2.oxml.presentation import ( # noqa: E402
329
+ CT_NotesMasterIdList,
330
+ CT_NotesMasterIdListEntry,
327
331
  CT_Presentation,
328
332
  CT_SlideId,
329
333
  CT_SlideIdList,
@@ -332,6 +336,8 @@ from pptx2.oxml.presentation import ( # noqa: E402
332
336
  CT_SlideSize,
333
337
  )
334
338
 
339
+ register_element_cls("p:notesMasterId", CT_NotesMasterIdListEntry)
340
+ register_element_cls("p:notesMasterIdLst", CT_NotesMasterIdList)
335
341
  register_element_cls("p:presentation", CT_Presentation)
336
342
  register_element_cls("p:sldId", CT_SlideId)
337
343
  register_element_cls("p:sldIdLst", CT_SlideIdList)
pptx2/oxml/chart/plot.py CHANGED
@@ -6,6 +6,7 @@ from pptx2.oxml.chart.datalabel import CT_DLbls
6
6
  from pptx2.oxml.simpletypes import (
7
7
  ST_BarDir,
8
8
  ST_BubbleScale,
9
+ ST_FirstSliceAng,
9
10
  ST_GapAmount,
10
11
  ST_Grouping,
11
12
  ST_HoleSize,
@@ -241,10 +242,21 @@ class CT_DoughnutChart(BaseChartElement):
241
242
  varyColors = ZeroOrOne("c:varyColors", successors=_tag_seq[1:])
242
243
  ser = ZeroOrMore("c:ser", successors=_tag_seq[2:])
243
244
  dLbls = ZeroOrOne("c:dLbls", successors=_tag_seq[3:])
245
+ firstSliceAng = ZeroOrOne("c:firstSliceAng", successors=_tag_seq[4:])
244
246
  holeSize = ZeroOrOne("c:holeSize", successors=_tag_seq[5:])
245
247
  del _tag_seq
246
248
 
247
249
 
250
+ class CT_FirstSliceAng(BaseOxmlElement):
251
+ """
252
+ ``<c:firstSliceAng>`` child of a ``<c:doughnutChart>`` element,
253
+ specifying the angle in degrees of the first slice, clockwise from 12
254
+ o'clock.
255
+ """
256
+
257
+ val = OptionalAttribute("val", ST_FirstSliceAng, default=0)
258
+
259
+
248
260
  class CT_GapAmount(BaseOxmlElement):
249
261
  """
250
262
  ``<c:gapWidth>`` child of ``<c:barChart>`` element, also used for other
@@ -362,4 +374,5 @@ class CT_ScatterChart(BaseChartElement):
362
374
  # -- avoid a circular import at package-initialization time.
363
375
  from pptx2.oxml import register_element_cls # noqa: E402
364
376
 
377
+ register_element_cls("c:firstSliceAng", CT_FirstSliceAng)
365
378
  register_element_cls("c:holeSize", CT_HoleSize)