1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
|
# pyright: reportImportCycles=false
"""Open Packaging Convention (OPC) objects related to package parts."""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, Type, cast
from docx.opc.oxml import serialize_part_xml
from docx.opc.packuri import PackURI
from docx.opc.rel import Relationships
from docx.opc.shared import cls_method_fn
from docx.oxml.parser import parse_xml
from docx.shared import lazyproperty
if TYPE_CHECKING:
from docx.oxml.xmlchemy import BaseOxmlElement
from docx.package import Package
class Part:
"""Base class for package parts.
Provides common properties and methods, but intended to be subclassed in client code
to implement specific part behaviors.
"""
def __init__(
self,
partname: PackURI,
content_type: str,
blob: bytes | None = None,
package: Package | None = None,
):
super(Part, self).__init__()
self._partname = partname
self._content_type = content_type
self._blob = blob
self._package = package
def after_unmarshal(self):
"""Entry point for post-unmarshaling processing, for example to parse the part
XML.
May be overridden by subclasses without forwarding call to super.
"""
# don't place any code here, just catch call if not overridden by
# subclass
pass
def before_marshal(self):
"""Entry point for pre-serialization processing, for example to finalize part
naming if necessary.
May be overridden by subclasses without forwarding call to super.
"""
# don't place any code here, just catch call if not overridden by
# subclass
pass
@property
def blob(self) -> bytes:
"""Contents of this package part as a sequence of bytes.
May be text or binary. Intended to be overridden by subclasses. Default behavior
is to return load blob.
"""
return self._blob or b""
@property
def content_type(self):
"""Content type of this part."""
return self._content_type
def drop_rel(self, rId: str):
"""Remove the relationship identified by `rId` if its reference count is less
than 2.
Relationships with a reference count of 0 are implicit relationships.
"""
if self._rel_ref_count(rId) < 2:
del self.rels[rId]
@classmethod
def load(cls, partname: PackURI, content_type: str, blob: bytes, package: Package):
return cls(partname, content_type, blob, package)
def load_rel(self, reltype: str, target: Part | str, rId: str, is_external: bool = False):
"""Return newly added |_Relationship| instance of `reltype`.
The new relationship relates the `target` part to this part with key `rId`.
Target mode is set to ``RTM.EXTERNAL`` if `is_external` is |True|. Intended for
use during load from a serialized package, where the rId is well-known. Other
methods exist for adding a new relationship to a part when manipulating a part.
"""
return self.rels.add_relationship(reltype, target, rId, is_external)
@property
def package(self):
"""|OpcPackage| instance this part belongs to."""
return self._package
@property
def partname(self):
"""|PackURI| instance holding partname of this part, e.g.
'/ppt/slides/slide1.xml'."""
return self._partname
@partname.setter
def partname(self, partname: str):
if not isinstance(partname, PackURI):
tmpl = "partname must be instance of PackURI, got '%s'"
raise TypeError(tmpl % type(partname).__name__)
self._partname = partname
def part_related_by(self, reltype: str) -> Part:
"""Return part to which this part has a relationship of `reltype`.
Raises |KeyError| if no such relationship is found and |ValueError| if more than
one such relationship is found. Provides ability to resolve implicitly related
part, such as Slide -> SlideLayout.
"""
return self.rels.part_with_reltype(reltype)
def relate_to(self, target: Part | str, reltype: str, is_external: bool = False) -> str:
"""Return rId key of relationship of `reltype` to `target`.
The returned `rId` is from an existing relationship if there is one, otherwise a
new relationship is created.
"""
if is_external:
return self.rels.get_or_add_ext_rel(reltype, cast(str, target))
else:
rel = self.rels.get_or_add(reltype, cast(Part, target))
return rel.rId
@property
def related_parts(self):
"""Dictionary mapping related parts by rId, so child objects can resolve
explicit relationships present in the part XML, e.g. sldIdLst to a specific
|Slide| instance."""
return self.rels.related_parts
@lazyproperty
def rels(self):
"""|Relationships| instance holding the relationships for this part."""
# -- prevent breakage in `python-docx-template` by retaining legacy `._rels` attribute --
self._rels = Relationships(self._partname.baseURI)
return self._rels
def target_ref(self, rId: str) -> str:
"""Return URL contained in target ref of relationship identified by `rId`."""
rel = self.rels[rId]
return rel.target_ref
def _rel_ref_count(self, rId: str) -> int:
"""Return the count of references in this part to the relationship identified by `rId`.
Only an XML part can contain references, so this is 0 for `Part`.
"""
return 0
class PartFactory:
"""Provides a way for client code to specify a subclass of |Part| to be constructed
by |Unmarshaller| based on its content type and/or a custom callable.
Setting ``PartFactory.part_class_selector`` to a callable object will cause that
object to be called with the parameters ``content_type, reltype``, once for each
part in the package. If the callable returns an object, it is used as the class for
that part. If it returns |None|, part class selection falls back to the content type
map defined in ``PartFactory.part_type_for``. If no class is returned from either of
these, the class contained in ``PartFactory.default_part_type`` is used to construct
the part, which is by default ``opc.package.Part``.
"""
part_class_selector: Callable[[str, str], Type[Part] | None] | None
part_type_for: dict[str, Type[Part]] = {}
default_part_type = Part
def __new__(
cls,
partname: PackURI,
content_type: str,
reltype: str,
blob: bytes,
package: Package,
):
PartClass: Type[Part] | None = None
if cls.part_class_selector is not None:
part_class_selector = cls_method_fn(cls, "part_class_selector")
PartClass = part_class_selector(content_type, reltype)
if PartClass is None:
PartClass = cls._part_cls_for(content_type)
return PartClass.load(partname, content_type, blob, package)
@classmethod
def _part_cls_for(cls, content_type: str):
"""Return the custom part class registered for `content_type`, or the default
part class if no custom class is registered for `content_type`."""
if content_type in cls.part_type_for:
return cls.part_type_for[content_type]
return cls.default_part_type
class XmlPart(Part):
"""Base class for package parts containing an XML payload, which is most of them.
Provides additional methods to the |Part| base class that take care of parsing and
reserializing the XML payload and managing relationships to other parts.
"""
def __init__(
self, partname: PackURI, content_type: str, element: BaseOxmlElement, package: Package
):
super(XmlPart, self).__init__(partname, content_type, package=package)
self._element = element
@property
def blob(self):
return serialize_part_xml(self._element)
@property
def element(self):
"""The root XML element of this XML part."""
return self._element
@classmethod
def load(cls, partname: PackURI, content_type: str, blob: bytes, package: Package):
element = parse_xml(blob)
return cls(partname, content_type, element, package)
@property
def part(self):
"""Part of the parent protocol, "children" of the document will not know the
part that contains them so must ask their parent object.
That chain of delegation ends here for child objects.
"""
return self
def _rel_ref_count(self, rId: str) -> int:
"""Return the count of references in this part's XML to the relationship
identified by `rId`."""
rIds = cast("list[str]", self._element.xpath("//@r:id"))
return len([_rId for _rId in rIds if _rId == rId])
|