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/opc/package.py ADDED
@@ -0,0 +1,762 @@
1
+ """Fundamental Open Packaging Convention (OPC) objects.
2
+
3
+ The :mod:`pptx2.packaging` module coheres around the concerns of reading and writing
4
+ presentations to and from a .pptx file.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import collections
10
+ from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Mapping, Set, cast
11
+
12
+ from pptx2.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
13
+ from pptx2.opc.constants import RELATIONSHIP_TYPE as RT
14
+ from pptx2.opc.oxml import CT_Relationships, serialize_part_xml
15
+ from pptx2.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI
16
+ from pptx2.opc.serialized import PackageReader, PackageWriter
17
+ from pptx2.opc.shared import CaseInsensitiveDict
18
+ from pptx2.oxml import parse_xml
19
+ from pptx2.util import lazyproperty
20
+
21
+ if TYPE_CHECKING:
22
+ from typing_extensions import Self
23
+
24
+ from pptx2.opc.oxml import CT_Relationship, CT_Types
25
+ from pptx2.oxml.xmlchemy import BaseOxmlElement
26
+ from pptx2.package import Package
27
+ from pptx2.parts.presentation import PresentationPart
28
+
29
+
30
+ class _RelatableMixin:
31
+ """Provide relationship methods required by both the package and each part."""
32
+
33
+ def part_related_by(self, reltype: str) -> Part:
34
+ """Return (single) part having relationship to this package of `reltype`.
35
+
36
+ Raises |KeyError| if no such relationship is found and |ValueError| if more than one such
37
+ relationship is found.
38
+ """
39
+ return self._rels.part_with_reltype(reltype)
40
+
41
+ def relate_to(self, target: Part | str, reltype: str, is_external: bool = False) -> str:
42
+ """Return rId key of relationship of `reltype` to `target`.
43
+
44
+ If such a relationship already exists, its rId is returned. Otherwise the relationship is
45
+ added and its new rId returned.
46
+ """
47
+ if isinstance(target, str):
48
+ assert is_external
49
+ return self._rels.get_or_add_ext_rel(reltype, target)
50
+
51
+ return self._rels.get_or_add(reltype, target)
52
+
53
+ def related_part(self, rId: str) -> Part:
54
+ """Return related |Part| subtype identified by `rId`."""
55
+ return self._rels[rId].target_part
56
+
57
+ def target_ref(self, rId: str) -> str:
58
+ """Return URL contained in target ref of relationship identified by `rId`."""
59
+ return self._rels[rId].target_ref
60
+
61
+ @lazyproperty
62
+ def _rels(self) -> _Relationships:
63
+ """|_Relationships| object containing relationships from this part to others."""
64
+ raise NotImplementedError( # pragma: no cover
65
+ "`%s` must implement `.rels`" % type(self).__name__
66
+ )
67
+
68
+
69
+ class OpcPackage(_RelatableMixin):
70
+ """Main API class for |python-opc|.
71
+
72
+ A new instance is constructed by calling the :meth:`open` classmethod with a path to a package
73
+ file or file-like object containing a package (.pptx file).
74
+ """
75
+
76
+ def __init__(self, pkg_file: str | IO[bytes]):
77
+ self._pkg_file = pkg_file
78
+
79
+ @classmethod
80
+ def open(cls, pkg_file: str | IO[bytes]) -> Self:
81
+ """Return an |OpcPackage| instance loaded with the contents of `pkg_file`."""
82
+ return cls(pkg_file)._load()
83
+
84
+ def drop_rel(self, rId: str) -> None:
85
+ """Remove relationship identified by `rId`."""
86
+ self._rels.pop(rId)
87
+
88
+ def iter_parts(self) -> Iterator[Part]:
89
+ """Generate exactly one reference to each part in the package."""
90
+ visited: Set[Part] = set()
91
+ for rel in self.iter_rels():
92
+ if rel.is_external:
93
+ continue
94
+ part = rel.target_part
95
+ if part in visited:
96
+ continue
97
+ yield part
98
+ visited.add(part)
99
+
100
+ def iter_rels(self) -> Iterator[_Relationship]:
101
+ """Generate exactly one reference to each relationship in package.
102
+
103
+ Performs a depth-first traversal of the rels graph.
104
+ """
105
+ visited: Set[Part] = set()
106
+
107
+ def walk_rels(rels: _Relationships) -> Iterator[_Relationship]:
108
+ for rel in rels.values():
109
+ yield rel
110
+ # --- external items can have no relationships ---
111
+ if rel.is_external:
112
+ continue
113
+ # -- all relationships other than those for the package belong to a part. Once
114
+ # -- that part has been processed, processing it again would lead to the same
115
+ # -- relationships appearing more than once.
116
+ part = rel.target_part
117
+ if part in visited:
118
+ continue
119
+ visited.add(part)
120
+ # --- recurse into relationships of each unvisited target-part ---
121
+ yield from walk_rels(part.rels)
122
+
123
+ yield from walk_rels(self._rels)
124
+
125
+ @property
126
+ def main_document_part(self) -> PresentationPart:
127
+ """Return |Part| subtype serving as the main document part for this package.
128
+
129
+ In this case it will be a |Presentation| part.
130
+ """
131
+ return cast("PresentationPart", self.part_related_by(RT.OFFICE_DOCUMENT))
132
+
133
+ def next_partname(self, tmpl: str) -> PackURI:
134
+ """Return |PackURI| next available partname matching `tmpl`.
135
+
136
+ `tmpl` is a printf (%)-style template string containing a single replacement item, a '%d'
137
+ to be used to insert the integer portion of the partname. Example:
138
+ '/ppt/slides/slide%d.xml'
139
+ """
140
+ # --- expected next partname is tmpl % n where n is one greater than the number
141
+ # --- of existing partnames that match tmpl. Speed up finding the next one
142
+ # --- (maybe) by searching from the end downward rather than from 1 upward.
143
+ prefix = tmpl[: (tmpl % 42).find("42")]
144
+ partnames = {p.partname for p in self.iter_parts() if p.partname.startswith(prefix)}
145
+ for n in range(len(partnames) + 1, 0, -1):
146
+ candidate_partname = tmpl % n
147
+ if candidate_partname not in partnames:
148
+ return PackURI(candidate_partname)
149
+ raise Exception("ProgrammingError: ran out of candidate_partnames") # pragma: no cover
150
+
151
+ def save(self, pkg_file: str | IO[bytes]) -> None:
152
+ """Save this package to `pkg_file`.
153
+
154
+ `file` can be either a path to a file (a string) or a file-like object.
155
+ """
156
+ PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts()))
157
+
158
+ def _load(self) -> Self:
159
+ """Return the package after loading all parts and relationships."""
160
+ pkg_xml_rels, parts = _PackageLoader.load(self._pkg_file, cast("Package", self))
161
+ self._rels.load_from_xml(PACKAGE_URI, pkg_xml_rels, parts)
162
+ return self
163
+
164
+ @lazyproperty
165
+ def _rels(self) -> _Relationships:
166
+ """|Relationships| object containing relationships of this package."""
167
+ return _Relationships(PACKAGE_URI.baseURI)
168
+
169
+
170
+ class _PackageLoader:
171
+ """Function-object that loads a package from disk (or other store)."""
172
+
173
+ def __init__(self, pkg_file: str | IO[bytes], package: Package):
174
+ self._pkg_file = pkg_file
175
+ self._package = package
176
+
177
+ @classmethod
178
+ def load(
179
+ cls, pkg_file: str | IO[bytes], package: Package
180
+ ) -> tuple[CT_Relationships, dict[PackURI, Part]]:
181
+ """Return (pkg_xml_rels, parts) pair resulting from loading `pkg_file`.
182
+
183
+ The returned `parts` value is a {partname: part} mapping with each part in the package
184
+ included and constructed complete with its relationships to other parts in the package.
185
+
186
+ The returned `pkg_xml_rels` value is a `CT_Relationships` object containing the parsed
187
+ package relationships. It is the caller's responsibility (the package object) to load
188
+ those relationships into its |_Relationships| object.
189
+ """
190
+ return cls(pkg_file, package)._load()
191
+
192
+ def _load(self) -> tuple[CT_Relationships, dict[PackURI, Part]]:
193
+ """Return (pkg_xml_rels, parts) pair resulting from loading pkg_file."""
194
+ parts, xml_rels = self._parts, self._xml_rels
195
+
196
+ for partname, part in parts.items():
197
+ part.load_rels_from_xml(xml_rels[partname], parts)
198
+
199
+ return xml_rels[PACKAGE_URI], parts
200
+
201
+ @lazyproperty
202
+ def _content_types(self) -> _ContentTypeMap:
203
+ """|_ContentTypeMap| object providing content-types for items of this package.
204
+
205
+ Provides a content-type (MIME-type) for any given partname.
206
+ """
207
+ return _ContentTypeMap.from_xml(self._package_reader[CONTENT_TYPES_URI])
208
+
209
+ @lazyproperty
210
+ def _package_reader(self) -> PackageReader:
211
+ """|PackageReader| object providing access to package-items in pkg_file."""
212
+ return PackageReader(self._pkg_file)
213
+
214
+ @lazyproperty
215
+ def _parts(self) -> dict[PackURI, Part]:
216
+ """dict {partname: Part} populated with parts loading from package.
217
+
218
+ Among other duties, this collection is passed to each relationships collection so each
219
+ relationship can resolve a reference to its target part when required. This reference can
220
+ only be reliably carried out once the all parts have been loaded.
221
+ """
222
+ content_types = self._content_types
223
+ package = self._package
224
+ package_reader = self._package_reader
225
+
226
+ return {
227
+ partname: PartFactory(
228
+ partname,
229
+ content_types[partname],
230
+ package,
231
+ blob=package_reader[partname],
232
+ )
233
+ for partname in (p for p in self._xml_rels if p != "/")
234
+ # -- invalid partnames can arise in some packages; ignore those rather than raise an
235
+ # -- exception.
236
+ if partname in package_reader
237
+ }
238
+
239
+ @lazyproperty
240
+ def _xml_rels(self) -> dict[PackURI, CT_Relationships]:
241
+ """dict {partname: xml_rels} for package and all package parts.
242
+
243
+ This is used as the basis for other loading operations such as loading parts and
244
+ populating their relationships.
245
+ """
246
+ xml_rels: dict[PackURI, CT_Relationships] = {}
247
+ visited_partnames: Set[PackURI] = set()
248
+
249
+ def load_rels(source_partname: PackURI, rels: CT_Relationships):
250
+ """Populate `xml_rels` dict by traversing relationships depth-first."""
251
+ xml_rels[source_partname] = rels
252
+ visited_partnames.add(source_partname)
253
+ base_uri = source_partname.baseURI
254
+
255
+ # --- recursion stops when there are no unvisited partnames in rels ---
256
+ for rel in rels.relationship_lst:
257
+ if rel.targetMode == RTM.EXTERNAL:
258
+ continue
259
+ target_partname = PackURI.from_rel_ref(base_uri, rel.target_ref)
260
+ if target_partname in visited_partnames:
261
+ continue
262
+ load_rels(target_partname, self._xml_rels_for(target_partname))
263
+
264
+ load_rels(PACKAGE_URI, self._xml_rels_for(PACKAGE_URI))
265
+ return xml_rels
266
+
267
+ def _xml_rels_for(self, partname: PackURI) -> CT_Relationships:
268
+ """Return CT_Relationships object formed by parsing rels XML for `partname`.
269
+
270
+ A CT_Relationships object is returned in all cases. A part that has no relationships
271
+ receives an "empty" CT_Relationships object, i.e. containing no `CT_Relationship` objects.
272
+ """
273
+ rels_xml = self._package_reader.rels_xml_for(partname)
274
+ return (
275
+ CT_Relationships.new()
276
+ if rels_xml is None
277
+ else cast(CT_Relationships, parse_xml(rels_xml))
278
+ )
279
+
280
+
281
+ class Part(_RelatableMixin):
282
+ """Base class for package parts.
283
+
284
+ Provides common properties and methods, but intended to be subclassed in client code to
285
+ implement specific part behaviors. Also serves as the default class for parts that are not yet
286
+ given specific behaviors.
287
+ """
288
+
289
+ def __init__(
290
+ self, partname: PackURI, content_type: str, package: Package, blob: bytes | None = None
291
+ ):
292
+ # --- XmlPart subtypes, don't store a blob (the original XML) ---
293
+ self._partname = partname
294
+ self._content_type = content_type
295
+ self._package = package
296
+ self._blob = blob
297
+
298
+ @classmethod
299
+ def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Self:
300
+ """Return `cls` instance loaded from arguments.
301
+
302
+ This one is a straight pass-through, but subtypes may do some pre-processing, see XmlPart
303
+ for an example.
304
+ """
305
+ return cls(partname, content_type, package, blob)
306
+
307
+ @property
308
+ def blob(self) -> bytes:
309
+ """Contents of this package part as a sequence of bytes.
310
+
311
+ Intended to be overridden by subclasses. Default behavior is to return the blob initial
312
+ loaded during `Package.open()` operation.
313
+ """
314
+ return self._blob or b""
315
+
316
+ @blob.setter
317
+ def blob(self, blob: bytes):
318
+ """Note that not all subclasses use the part blob as their blob source.
319
+
320
+ In particular, the |XmlPart| subclass uses its `self._element` to serialize a blob on
321
+ demand. This works fine for binary parts though.
322
+ """
323
+ self._blob = blob
324
+
325
+ @lazyproperty
326
+ def content_type(self) -> str:
327
+ """Content-type (MIME-type) of this part."""
328
+ return self._content_type
329
+
330
+ def load_rels_from_xml(self, xml_rels: CT_Relationships, parts: dict[PackURI, Part]) -> None:
331
+ """load _Relationships for this part from `xml_rels`.
332
+
333
+ Part references are resolved using the `parts` dict that maps each partname to the loaded
334
+ part with that partname. These relationships are loaded from a serialized package and so
335
+ already have assigned rIds. This method is only used during package loading.
336
+ """
337
+ self._rels.load_from_xml(self._partname.baseURI, xml_rels, parts)
338
+
339
+ @lazyproperty
340
+ def package(self) -> Package:
341
+ """Package this part belongs to."""
342
+ return self._package
343
+
344
+ @property
345
+ def partname(self) -> PackURI:
346
+ """|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml"."""
347
+ return self._partname
348
+
349
+ @partname.setter
350
+ def partname(self, partname: PackURI):
351
+ if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance]
352
+ raise TypeError( # pragma: no cover
353
+ "partname must be instance of PackURI, got '%s'" % type(partname).__name__
354
+ )
355
+ self._partname = partname
356
+
357
+ @lazyproperty
358
+ def rels(self) -> _Relationships:
359
+ """Collection of relationships from this part to other parts."""
360
+ # --- this must be public to allow the part graph to be traversed ---
361
+ return self._rels
362
+
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:
368
+ return f.read()
369
+
370
+ # --- otherwise, assume `file` is a file-like object
371
+ # --- reposition file cursor if it has one
372
+ if callable(getattr(file, "seek")):
373
+ file.seek(0)
374
+ return file.read()
375
+
376
+ @lazyproperty
377
+ def _rels(self) -> _Relationships:
378
+ """Relationships from this part to others."""
379
+ return _Relationships(self._partname.baseURI)
380
+
381
+
382
+ class XmlPart(Part):
383
+ """Base class for package parts containing an XML payload, which is most of them.
384
+
385
+ Provides additional methods to the |Part| base class that take care of parsing and
386
+ reserializing the XML payload and managing relationships to other parts.
387
+ """
388
+
389
+ def __init__(
390
+ self, partname: PackURI, content_type: str, package: Package, element: BaseOxmlElement
391
+ ):
392
+ super(XmlPart, self).__init__(partname, content_type, package)
393
+ self._element = element
394
+
395
+ @classmethod
396
+ def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes):
397
+ """Return instance of `cls` loaded with parsed XML from `blob`."""
398
+ return cls(
399
+ partname, content_type, package, element=cast("BaseOxmlElement", parse_xml(blob))
400
+ )
401
+
402
+ @property
403
+ def blob(self) -> bytes: # pyright: ignore[reportIncompatibleMethodOverride]
404
+ """bytes XML serialization of this part."""
405
+ return serialize_part_xml(self._element)
406
+
407
+ # -- XmlPart cannot set its blob, which is why pyright complains --
408
+
409
+ def drop_rel(self, rId: str) -> None:
410
+ """Remove relationship identified by `rId` if its reference count is under 2.
411
+
412
+ Relationships with a reference count of 0 are implicit relationships. Note that only XML
413
+ parts can drop relationships.
414
+ """
415
+ if self._rel_ref_count(rId) < 2:
416
+ self._rels.pop(rId)
417
+
418
+ @property
419
+ def part(self):
420
+ """This part.
421
+
422
+ This is part of the parent protocol, "children" of the document will not know the part
423
+ that contains them so must ask their parent object. That chain of delegation ends here for
424
+ child objects.
425
+ """
426
+ return self
427
+
428
+ def _rel_ref_count(self, rId: str) -> int:
429
+ """Return int count of references in this part's XML to `rId`."""
430
+ return len([r for r in cast("list[str]", self._element.xpath("//@r:id")) if r == rId])
431
+
432
+
433
+ class PartFactory:
434
+ """Constructs a registered subtype of |Part|.
435
+
436
+ Client code can register a subclass of |Part| to be used for a package blob based on its
437
+ content type.
438
+ """
439
+
440
+ part_type_for: dict[str, type[Part]] = {}
441
+
442
+ def __new__(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Part:
443
+ PartClass = cls._part_cls_for(content_type)
444
+ return PartClass.load(partname, content_type, package, blob)
445
+
446
+ @classmethod
447
+ def _part_cls_for(cls, content_type: str) -> type[Part]:
448
+ """Return the custom part class registered for `content_type`.
449
+
450
+ Returns |Part| if no custom class is registered for `content_type`.
451
+ """
452
+ if content_type in cls.part_type_for:
453
+ return cls.part_type_for[content_type]
454
+ return Part
455
+
456
+
457
+ class _ContentTypeMap:
458
+ """Value type providing dict semantics for looking up content type by partname."""
459
+
460
+ def __init__(self, overrides: dict[str, str], defaults: dict[str, str]):
461
+ self._overrides = overrides
462
+ self._defaults = defaults
463
+
464
+ def __getitem__(self, partname: PackURI) -> str:
465
+ """Return content-type (MIME-type) for part identified by *partname*."""
466
+ if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance]
467
+ raise TypeError(
468
+ "_ContentTypeMap key must be <type 'PackURI'>, got %s" % type(partname).__name__
469
+ )
470
+
471
+ if partname in self._overrides:
472
+ return self._overrides[partname]
473
+
474
+ if partname.ext in self._defaults:
475
+ return self._defaults[partname.ext]
476
+
477
+ raise KeyError("no content-type for partname '%s' in [Content_Types].xml" % partname)
478
+
479
+ @classmethod
480
+ def from_xml(cls, content_types_xml: bytes) -> _ContentTypeMap:
481
+ """Return |_ContentTypeMap| instance populated from `content_types_xml`."""
482
+ types_elm = cast("CT_Types", parse_xml(content_types_xml))
483
+ # -- note all partnames in [Content_Types].xml are absolute --
484
+ overrides = CaseInsensitiveDict(
485
+ (o.partName.lower(), o.contentType) for o in types_elm.override_lst
486
+ )
487
+ defaults = CaseInsensitiveDict(
488
+ (d.extension.lower(), d.contentType) for d in types_elm.default_lst
489
+ )
490
+ return cls(overrides, defaults)
491
+
492
+
493
+ class _Relationships(Mapping[str, "_Relationship"]):
494
+ """Collection of |_Relationship| instances having `dict` semantics.
495
+
496
+ Relationships are keyed by their rId, but may also be found in other ways, such as by their
497
+ relationship type. |Relationship| objects are keyed by their rId.
498
+
499
+ Iterating this collection has normal mapping semantics, generating the keys (rIds) of the
500
+ mapping. `rels.keys()`, `rels.values()`, and `rels.items() can be used as they would be for a
501
+ `dict`.
502
+ """
503
+
504
+ def __init__(self, base_uri: str):
505
+ self._base_uri = base_uri
506
+
507
+ def __contains__(self, rId: object) -> bool:
508
+ """Implement 'in' operation, like `"rId7" in relationships`."""
509
+ return rId in self._rels
510
+
511
+ def __getitem__(self, rId: str) -> _Relationship:
512
+ """Implement relationship lookup by rId using indexed access, like rels[rId]."""
513
+ try:
514
+ return self._rels[rId]
515
+ except KeyError:
516
+ raise KeyError("no relationship with key '%s'" % rId)
517
+
518
+ def __iter__(self) -> Iterator[str]:
519
+ """Implement iteration of rIds (iterating a mapping produces its keys)."""
520
+ return iter(self._rels)
521
+
522
+ def __len__(self) -> int:
523
+ """Return count of relationships in collection."""
524
+ return len(self._rels)
525
+
526
+ def get_or_add(self, reltype: str, target_part: Part) -> str:
527
+ """Return str rId of `reltype` to `target_part`.
528
+
529
+ The rId of an existing matching relationship is used if present. Otherwise, a new
530
+ relationship is added and that rId is returned.
531
+ """
532
+ existing_rId = self._get_matching(reltype, target_part)
533
+ return (
534
+ self._add_relationship(reltype, target_part) if existing_rId is None else existing_rId
535
+ )
536
+
537
+ def get_or_add_ext_rel(self, reltype: str, target_ref: str) -> str:
538
+ """Return str rId of external relationship of `reltype` to `target_ref`.
539
+
540
+ The rId of an existing matching relationship is used if present. Otherwise, a new
541
+ relationship is added and that rId is returned.
542
+ """
543
+ existing_rId = self._get_matching(reltype, target_ref, is_external=True)
544
+ return (
545
+ self._add_relationship(reltype, target_ref, is_external=True)
546
+ if existing_rId is None
547
+ else existing_rId
548
+ )
549
+
550
+ def load_from_xml(
551
+ self, base_uri: str, xml_rels: CT_Relationships, parts: dict[PackURI, Part]
552
+ ) -> None:
553
+ """Replace any relationships in this collection with those from `xml_rels`."""
554
+
555
+ def iter_valid_rels():
556
+ """Filter out broken relationships such as those pointing to NULL."""
557
+ for rel_elm in xml_rels.relationship_lst:
558
+ # --- Occasionally a PowerPoint plugin or other client will "remove"
559
+ # --- a relationship simply by "voiding" its Target value, like making
560
+ # --- it "/ppt/slides/NULL". Skip any relationships linking to a
561
+ # --- partname that is not present in the package.
562
+ if rel_elm.targetMode == RTM.INTERNAL:
563
+ partname = PackURI.from_rel_ref(base_uri, rel_elm.target_ref)
564
+ if partname not in parts:
565
+ continue
566
+ yield _Relationship.from_xml(base_uri, rel_elm, parts)
567
+
568
+ self._rels.clear()
569
+ self._rels.update((rel.rId, rel) for rel in iter_valid_rels())
570
+
571
+ def part_with_reltype(self, reltype: str) -> Part:
572
+ """Return target part of relationship with matching `reltype`.
573
+
574
+ Raises |KeyError| if not found and |ValueError| if more than one matching relationship is
575
+ found.
576
+ """
577
+ rels_of_reltype = self._rels_by_reltype[reltype]
578
+
579
+ if len(rels_of_reltype) == 0:
580
+ raise KeyError("no relationship of type '%s' in collection" % reltype)
581
+
582
+ if len(rels_of_reltype) > 1:
583
+ raise ValueError("multiple relationships of type '%s' in collection" % reltype)
584
+
585
+ return rels_of_reltype[0].target_part
586
+
587
+ def pop(self, rId: str) -> _Relationship:
588
+ """Return |_Relationship| identified by `rId` after removing it from collection.
589
+
590
+ The caller is responsible for ensuring it is no longer required.
591
+ """
592
+ return self._rels.pop(rId)
593
+
594
+ @property
595
+ def xml(self):
596
+ """bytes XML serialization of this relationship collection.
597
+
598
+ This value is suitable for storage as a .rels file in an OPC package. Includes a `<?xml..`
599
+ declaration header with encoding as UTF-8.
600
+ """
601
+ rels_elm = CT_Relationships.new()
602
+
603
+ # -- Sequence <Relationship> elements deterministically (in numerical order) to
604
+ # -- simplify testing and manual inspection.
605
+ def iter_rels_in_numerical_order():
606
+ sorted_num_rId_pairs = sorted(
607
+ (
608
+ int(rId[3:]) if rId.startswith("rId") and rId[3:].isdigit() else 0,
609
+ rId,
610
+ )
611
+ for rId in self.keys()
612
+ )
613
+ return (self[rId] for _, rId in sorted_num_rId_pairs)
614
+
615
+ for rel in iter_rels_in_numerical_order():
616
+ rels_elm.add_rel(rel.rId, rel.reltype, rel.target_ref, rel.is_external)
617
+
618
+ return rels_elm.xml_file_bytes
619
+
620
+ def _add_relationship(self, reltype: str, target: Part | str, is_external: bool = False) -> str:
621
+ """Return str rId of |_Relationship| newly added to spec."""
622
+ rId = self._next_rId
623
+ self._rels[rId] = _Relationship(
624
+ self._base_uri,
625
+ rId,
626
+ reltype,
627
+ target_mode=RTM.EXTERNAL if is_external else RTM.INTERNAL,
628
+ target=target,
629
+ )
630
+ return rId
631
+
632
+ def _get_matching(
633
+ self, reltype: str, target: Part | str, is_external: bool = False
634
+ ) -> str | None:
635
+ """Return optional str rId of rel of `reltype`, `target`, and `is_external`.
636
+
637
+ Returns `None` on no matching relationship
638
+ """
639
+ for rel in self._rels_by_reltype[reltype]:
640
+ if rel.is_external != is_external:
641
+ continue
642
+ rel_target = rel.target_ref if rel.is_external else rel.target_part
643
+ if rel_target == target:
644
+ return rel.rId
645
+
646
+ return None
647
+
648
+ @property
649
+ def _next_rId(self) -> str:
650
+ """Next str rId available in collection.
651
+
652
+ The next rId is the first unused key starting from "rId1" and making use of any gaps in
653
+ numbering, e.g. 'rId2' for rIds ['rId1', 'rId3'].
654
+ """
655
+ # --- The common case is where all sequential numbers starting at "rId1" are
656
+ # --- used and the next available rId is "rId%d" % (len(rels)+1). So we start
657
+ # --- there and count down to produce the best performance.
658
+ for n in range(len(self) + 1, 0, -1):
659
+ rId_candidate = "rId%d" % n # like 'rId19'
660
+ if rId_candidate not in self._rels:
661
+ return rId_candidate
662
+ raise Exception(
663
+ "ProgrammingError: Impossible to have more distinct rIds than relationships"
664
+ )
665
+
666
+ @lazyproperty
667
+ def _rels(self) -> dict[str, _Relationship]:
668
+ """dict {rId: _Relationship} containing relationships of this collection."""
669
+ return {}
670
+
671
+ @property
672
+ def _rels_by_reltype(self) -> dict[str, list[_Relationship]]:
673
+ """defaultdict {reltype: [rels]} for all relationships in collection."""
674
+ D: DefaultDict[str, list[_Relationship]] = collections.defaultdict(list)
675
+ for rel in self.values():
676
+ D[rel.reltype].append(rel)
677
+ return D
678
+
679
+
680
+ class _Relationship:
681
+ """Value object describing link from a part or package to another part."""
682
+
683
+ def __init__(self, base_uri: str, rId: str, reltype: str, target_mode: str, target: Part | str):
684
+ self._base_uri = base_uri
685
+ self._rId = rId
686
+ self._reltype = reltype
687
+ self._target_mode = target_mode
688
+ self._target = target
689
+
690
+ @classmethod
691
+ def from_xml(
692
+ cls, base_uri: str, rel: CT_Relationship, parts: dict[PackURI, Part]
693
+ ) -> _Relationship:
694
+ """Return |_Relationship| object based on CT_Relationship element `rel`."""
695
+ target = (
696
+ rel.target_ref
697
+ if rel.targetMode == RTM.EXTERNAL
698
+ else parts[PackURI.from_rel_ref(base_uri, rel.target_ref)]
699
+ )
700
+ return cls(base_uri, rel.rId, rel.reltype, rel.targetMode, target)
701
+
702
+ @lazyproperty
703
+ def is_external(self) -> bool:
704
+ """True if target_mode is `RTM.EXTERNAL`.
705
+
706
+ An external relationship is a link to a resource outside the package, such as a
707
+ web-resource (URL).
708
+ """
709
+ return self._target_mode == RTM.EXTERNAL
710
+
711
+ @lazyproperty
712
+ def reltype(self) -> str:
713
+ """Member of RELATIONSHIP_TYPE describing relationship of target to source."""
714
+ return self._reltype
715
+
716
+ @lazyproperty
717
+ def rId(self) -> str:
718
+ """str relationship-id, like 'rId9'.
719
+
720
+ Corresponds to the `Id` attribute on the `CT_Relationship` element and uniquely identifies
721
+ this relationship within its peers for the source-part or package.
722
+ """
723
+ return self._rId
724
+
725
+ @lazyproperty
726
+ def target_part(self) -> Part:
727
+ """|Part| or subtype referred to by this relationship."""
728
+ if self.is_external:
729
+ raise ValueError(
730
+ "`.target_part` property on _Relationship is undefined when "
731
+ "target-mode is external"
732
+ )
733
+ assert isinstance(self._target, Part)
734
+ return self._target
735
+
736
+ @lazyproperty
737
+ def target_partname(self) -> PackURI:
738
+ """|PackURI| instance containing partname targeted by this relationship.
739
+
740
+ Raises `ValueError` on reference if target_mode is external. Use :attr:`target_mode` to
741
+ check before referencing.
742
+ """
743
+ if self.is_external:
744
+ raise ValueError(
745
+ "`.target_partname` property on _Relationship is undefined when "
746
+ "target-mode is external"
747
+ )
748
+ assert isinstance(self._target, Part)
749
+ return self._target.partname
750
+
751
+ @lazyproperty
752
+ def target_ref(self) -> str:
753
+ """str reference to relationship target.
754
+
755
+ For internal relationships this is the relative partname, suitable for serialization
756
+ purposes. For an external relationship it is typically a URL.
757
+ """
758
+ if self.is_external:
759
+ assert isinstance(self._target, str)
760
+ return self._target
761
+
762
+ return self.target_partname.relative_ref(self._base_uri)