python-docx-oss 0.1.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 (132) hide show
  1. docx/__init__.py +71 -0
  2. docx/api.py +37 -0
  3. docx/blkcntnr.py +101 -0
  4. docx/comments.py +163 -0
  5. docx/dml/__init__.py +0 -0
  6. docx/dml/color.py +112 -0
  7. docx/document.py +285 -0
  8. docx/drawing/__init__.py +59 -0
  9. docx/enum/__init__.py +0 -0
  10. docx/enum/base.py +150 -0
  11. docx/enum/dml.py +103 -0
  12. docx/enum/section.py +86 -0
  13. docx/enum/shape.py +19 -0
  14. docx/enum/style.py +452 -0
  15. docx/enum/table.py +136 -0
  16. docx/enum/text.py +367 -0
  17. docx/exceptions.py +18 -0
  18. docx/image/__init__.py +32 -0
  19. docx/image/bmp.py +43 -0
  20. docx/image/constants.py +172 -0
  21. docx/image/emf.py +46 -0
  22. docx/image/exceptions.py +13 -0
  23. docx/image/gif.py +38 -0
  24. docx/image/helpers.py +86 -0
  25. docx/image/image.py +237 -0
  26. docx/image/jpeg.py +469 -0
  27. docx/image/png.py +253 -0
  28. docx/image/svg.py +118 -0
  29. docx/image/tiff.py +289 -0
  30. docx/opc/__init__.py +0 -0
  31. docx/opc/constants.py +307 -0
  32. docx/opc/coreprops.py +142 -0
  33. docx/opc/customprops.py +59 -0
  34. docx/opc/exceptions.py +12 -0
  35. docx/opc/oxml.py +247 -0
  36. docx/opc/package.py +254 -0
  37. docx/opc/packuri.py +109 -0
  38. docx/opc/part.py +267 -0
  39. docx/opc/parts/__init__.py +0 -0
  40. docx/opc/parts/coreprops.py +48 -0
  41. docx/opc/parts/customprops.py +61 -0
  42. docx/opc/phys_pkg.py +119 -0
  43. docx/opc/pkgreader.py +254 -0
  44. docx/opc/pkgwriter.py +115 -0
  45. docx/opc/rel.py +169 -0
  46. docx/opc/shared.py +31 -0
  47. docx/opc/spec.py +24 -0
  48. docx/oxml/__init__.py +252 -0
  49. docx/oxml/comments.py +124 -0
  50. docx/oxml/coreprops.py +298 -0
  51. docx/oxml/customprops.py +141 -0
  52. docx/oxml/dml/CT_ExtensionList.py +48 -0
  53. docx/oxml/document.py +88 -0
  54. docx/oxml/drawing.py +11 -0
  55. docx/oxml/exceptions.py +10 -0
  56. docx/oxml/ns.py +111 -0
  57. docx/oxml/numbering.py +109 -0
  58. docx/oxml/parser.py +62 -0
  59. docx/oxml/section.py +537 -0
  60. docx/oxml/settings.py +138 -0
  61. docx/oxml/shape.py +426 -0
  62. docx/oxml/shared.py +52 -0
  63. docx/oxml/simpletypes.py +434 -0
  64. docx/oxml/styles.py +320 -0
  65. docx/oxml/table.py +977 -0
  66. docx/oxml/text/__init__.py +0 -0
  67. docx/oxml/text/font.py +358 -0
  68. docx/oxml/text/hyperlink.py +45 -0
  69. docx/oxml/text/pagebreak.py +278 -0
  70. docx/oxml/text/paragraph.py +106 -0
  71. docx/oxml/text/parfmt.py +392 -0
  72. docx/oxml/text/run.py +316 -0
  73. docx/oxml/xmlchemy.py +711 -0
  74. docx/package.py +119 -0
  75. docx/parts/__init__.py +0 -0
  76. docx/parts/comments.py +51 -0
  77. docx/parts/customxml.py +67 -0
  78. docx/parts/document.py +193 -0
  79. docx/parts/hdrftr.py +53 -0
  80. docx/parts/image.py +80 -0
  81. docx/parts/numbering.py +51 -0
  82. docx/parts/settings.py +50 -0
  83. docx/parts/story.py +113 -0
  84. docx/parts/styles.py +42 -0
  85. docx/py.typed +0 -0
  86. docx/section.py +479 -0
  87. docx/settings.py +35 -0
  88. docx/shape.py +103 -0
  89. docx/shared.py +382 -0
  90. docx/styles/__init__.py +40 -0
  91. docx/styles/latent.py +198 -0
  92. docx/styles/style.py +254 -0
  93. docx/styles/styles.py +136 -0
  94. docx/table.py +537 -0
  95. docx/templates/default-comments.xml +12 -0
  96. docx/templates/default-docx-template/[Content_Types].xml +17 -0
  97. docx/templates/default-docx-template/_rels/.rels +7 -0
  98. docx/templates/default-docx-template/customXml/_rels/item1.xml.rels +4 -0
  99. docx/templates/default-docx-template/customXml/item1.xml +2 -0
  100. docx/templates/default-docx-template/customXml/itemProps1.xml +6 -0
  101. docx/templates/default-docx-template/docProps/app.xml +36 -0
  102. docx/templates/default-docx-template/docProps/core.xml +13 -0
  103. docx/templates/default-docx-template/docProps/thumbnail.jpeg +0 -0
  104. docx/templates/default-docx-template/word/_rels/document.xml.rels +11 -0
  105. docx/templates/default-docx-template/word/document.xml +11 -0
  106. docx/templates/default-docx-template/word/fontTable.xml +61 -0
  107. docx/templates/default-docx-template/word/numbering.xml +201 -0
  108. docx/templates/default-docx-template/word/settings.xml +53 -0
  109. docx/templates/default-docx-template/word/styles.xml +11844 -0
  110. docx/templates/default-docx-template/word/stylesWithEffects.xml +11800 -0
  111. docx/templates/default-docx-template/word/theme/theme1.xml +318 -0
  112. docx/templates/default-docx-template/word/webSettings.xml +5 -0
  113. docx/templates/default-footer.xml +27 -0
  114. docx/templates/default-header.xml +27 -0
  115. docx/templates/default-numbering.xml +20 -0
  116. docx/templates/default-settings.xml +26 -0
  117. docx/templates/default-styles.xml +190 -0
  118. docx/templates/default.docx +0 -0
  119. docx/text/__init__.py +0 -0
  120. docx/text/font.py +466 -0
  121. docx/text/hyperlink.py +121 -0
  122. docx/text/pagebreak.py +104 -0
  123. docx/text/paragraph.py +173 -0
  124. docx/text/parfmt.py +286 -0
  125. docx/text/run.py +272 -0
  126. docx/text/tabstops.py +123 -0
  127. docx/types.py +34 -0
  128. python_docx_oss-0.1.0.dist-info/METADATA +57 -0
  129. python_docx_oss-0.1.0.dist-info/RECORD +132 -0
  130. python_docx_oss-0.1.0.dist-info/WHEEL +5 -0
  131. python_docx_oss-0.1.0.dist-info/licenses/LICENSE +21 -0
  132. python_docx_oss-0.1.0.dist-info/top_level.txt +1 -0
docx/opc/oxml.py ADDED
@@ -0,0 +1,247 @@
1
+ # pyright: reportPrivateUsage=false
2
+
3
+ """Temporary stand-in for main oxml module.
4
+
5
+ This module came across with the PackageReader transplant. Probably much will get
6
+ replaced with objects from the pptx.oxml.core and then this module will either get
7
+ deleted or only hold the package related custom element classes.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import cast
13
+
14
+ from lxml import etree
15
+
16
+ from docx.opc.constants import NAMESPACE as NS
17
+ from docx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM
18
+
19
+ # configure XML parser
20
+ element_class_lookup = etree.ElementNamespaceClassLookup()
21
+ oxml_parser = etree.XMLParser(remove_blank_text=True, resolve_entities=False)
22
+ oxml_parser.set_element_class_lookup(element_class_lookup)
23
+
24
+ nsmap = {
25
+ "ct": NS.OPC_CONTENT_TYPES,
26
+ "pr": NS.OPC_RELATIONSHIPS,
27
+ "r": NS.OFC_RELATIONSHIPS,
28
+ }
29
+
30
+
31
+ # ===========================================================================
32
+ # functions
33
+ # ===========================================================================
34
+
35
+
36
+ def parse_xml(text: str) -> etree._Element:
37
+ """`etree.fromstring()` replacement that uses oxml parser."""
38
+ return etree.fromstring(text, oxml_parser)
39
+
40
+
41
+ def qn(tag: str) -> str:
42
+ """Stands for "qualified name", a utility function to turn a namespace prefixed tag
43
+ name into a Clark-notation qualified tag name for lxml.
44
+
45
+ For
46
+ example, ``qn('p:cSld')`` returns ``'{http://schemas.../main}cSld'``.
47
+ """
48
+ prefix, tagroot = tag.split(":")
49
+ uri = nsmap[prefix]
50
+ return "{%s}%s" % (uri, tagroot)
51
+
52
+
53
+ def serialize_part_xml(part_elm: etree._Element) -> bytes:
54
+ """Serialize `part_elm` etree element to XML suitable for storage as an XML part.
55
+
56
+ That is to say, no insignificant whitespace added for readability, and an
57
+ appropriate XML declaration added with UTF-8 encoding specified.
58
+ """
59
+ return etree.tostring(part_elm, encoding="UTF-8", standalone=True)
60
+
61
+
62
+ def serialize_for_reading(element: etree._Element) -> str:
63
+ """Serialize `element` to human-readable XML suitable for tests.
64
+
65
+ No XML declaration.
66
+ """
67
+ return etree.tostring(element, encoding="unicode", pretty_print=True)
68
+
69
+
70
+ # ===========================================================================
71
+ # Custom element classes
72
+ # ===========================================================================
73
+
74
+
75
+ class BaseOxmlElement(etree.ElementBase):
76
+ """Base class for all custom element classes, to add standardized behavior to all
77
+ classes in one place."""
78
+
79
+ @property
80
+ def xml(self) -> str:
81
+ """Return XML string for this element, suitable for testing purposes.
82
+
83
+ Pretty printed for readability and without an XML declaration at the top.
84
+ """
85
+ return serialize_for_reading(self)
86
+
87
+
88
+ class CT_Default(BaseOxmlElement):
89
+ """`<Default>` element that appears in `[Content_Types].xml` part.
90
+
91
+ Used to specify a default content type to be applied to any part with the specified extension.
92
+ """
93
+
94
+ @property
95
+ def content_type(self):
96
+ """String held in the ``ContentType`` attribute of this ``<Default>``
97
+ element."""
98
+ return self.get("ContentType")
99
+
100
+ @property
101
+ def extension(self):
102
+ """String held in the ``Extension`` attribute of this ``<Default>`` element."""
103
+ return self.get("Extension")
104
+
105
+ @staticmethod
106
+ def new(ext: str, content_type: str):
107
+ """Return a new ``<Default>`` element with attributes set to parameter values."""
108
+ xml = '<Default xmlns="%s"/>' % nsmap["ct"]
109
+ default = parse_xml(xml)
110
+ default.set("Extension", ext)
111
+ default.set("ContentType", content_type)
112
+ return default
113
+
114
+
115
+ class CT_Override(BaseOxmlElement):
116
+ """``<Override>`` element, specifying the content type to be applied for a part with
117
+ the specified partname."""
118
+
119
+ @property
120
+ def content_type(self):
121
+ """String held in the ``ContentType`` attribute of this ``<Override>``
122
+ element."""
123
+ return self.get("ContentType")
124
+
125
+ @staticmethod
126
+ def new(partname, content_type):
127
+ """Return a new ``<Override>`` element with attributes set to parameter values."""
128
+ xml = '<Override xmlns="%s"/>' % nsmap["ct"]
129
+ override = parse_xml(xml)
130
+ override.set("PartName", partname)
131
+ override.set("ContentType", content_type)
132
+ return override
133
+
134
+ @property
135
+ def partname(self):
136
+ """String held in the ``PartName`` attribute of this ``<Override>`` element."""
137
+ return self.get("PartName")
138
+
139
+
140
+ class CT_Relationship(BaseOxmlElement):
141
+ """`<Relationship>` element, representing a single relationship from source to target part."""
142
+
143
+ @staticmethod
144
+ def new(rId: str, reltype: str, target: str, target_mode: str = RTM.INTERNAL):
145
+ """Return a new ``<Relationship>`` element."""
146
+ xml = '<Relationship xmlns="%s"/>' % nsmap["pr"]
147
+ relationship = parse_xml(xml)
148
+ relationship.set("Id", rId)
149
+ relationship.set("Type", reltype)
150
+ relationship.set("Target", target)
151
+ if target_mode == RTM.EXTERNAL:
152
+ relationship.set("TargetMode", RTM.EXTERNAL)
153
+ return relationship
154
+
155
+ @property
156
+ def rId(self):
157
+ """String held in the ``Id`` attribute of this ``<Relationship>`` element."""
158
+ return self.get("Id")
159
+
160
+ @property
161
+ def reltype(self):
162
+ """String held in the ``Type`` attribute of this ``<Relationship>`` element."""
163
+ return self.get("Type")
164
+
165
+ @property
166
+ def target_ref(self):
167
+ """String held in the ``Target`` attribute of this ``<Relationship>``
168
+ element."""
169
+ return self.get("Target")
170
+
171
+ @property
172
+ def target_mode(self):
173
+ """String held in the ``TargetMode`` attribute of this ``<Relationship>``
174
+ element, either ``Internal`` or ``External``.
175
+
176
+ Defaults to ``Internal``.
177
+ """
178
+ return self.get("TargetMode", RTM.INTERNAL)
179
+
180
+
181
+ class CT_Relationships(BaseOxmlElement):
182
+ """``<Relationships>`` element, the root element in a .rels file."""
183
+
184
+ def add_rel(self, rId: str, reltype: str, target: str, is_external: bool = False):
185
+ """Add a child ``<Relationship>`` element with attributes set according to
186
+ parameter values."""
187
+ target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL
188
+ relationship = CT_Relationship.new(rId, reltype, target, target_mode)
189
+ self.append(relationship)
190
+
191
+ @staticmethod
192
+ def new() -> CT_Relationships:
193
+ """Return a new ``<Relationships>`` element."""
194
+ xml = '<Relationships xmlns="%s"/>' % nsmap["pr"]
195
+ return cast(CT_Relationships, parse_xml(xml))
196
+
197
+ @property
198
+ def Relationship_lst(self):
199
+ """Return a list containing all the ``<Relationship>`` child elements."""
200
+ return self.findall(qn("pr:Relationship"))
201
+
202
+ @property
203
+ def xml(self):
204
+ """Return XML string for this element, suitable for saving in a .rels stream,
205
+ not pretty printed and with an XML declaration at the top."""
206
+ return serialize_part_xml(self)
207
+
208
+
209
+ class CT_Types(BaseOxmlElement):
210
+ """``<Types>`` element, the container element for Default and Override elements in
211
+ [Content_Types].xml."""
212
+
213
+ def add_default(self, ext, content_type):
214
+ """Add a child ``<Default>`` element with attributes set to parameter values."""
215
+ default = CT_Default.new(ext, content_type)
216
+ self.append(default)
217
+
218
+ def add_override(self, partname, content_type):
219
+ """Add a child ``<Override>`` element with attributes set to parameter
220
+ values."""
221
+ override = CT_Override.new(partname, content_type)
222
+ self.append(override)
223
+
224
+ @property
225
+ def defaults(self):
226
+ return self.findall(qn("ct:Default"))
227
+
228
+ @staticmethod
229
+ def new():
230
+ """Return a new ``<Types>`` element."""
231
+ xml = '<Types xmlns="%s"/>' % nsmap["ct"]
232
+ types = parse_xml(xml)
233
+ return types
234
+
235
+ @property
236
+ def overrides(self):
237
+ return self.findall(qn("ct:Override"))
238
+
239
+
240
+ ct_namespace = element_class_lookup.get_namespace(nsmap["ct"])
241
+ ct_namespace["Default"] = CT_Default
242
+ ct_namespace["Override"] = CT_Override
243
+ ct_namespace["Types"] = CT_Types
244
+
245
+ pr_namespace = element_class_lookup.get_namespace(nsmap["pr"])
246
+ pr_namespace["Relationship"] = CT_Relationship
247
+ pr_namespace["Relationships"] = CT_Relationships
docx/opc/package.py ADDED
@@ -0,0 +1,254 @@
1
+ """Objects that implement reading and writing OPC packages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import IO, TYPE_CHECKING, Callable, Iterator, cast
6
+
7
+ from docx.opc.constants import RELATIONSHIP_TYPE as RT
8
+ from docx.opc.packuri import PACKAGE_URI, PackURI
9
+ from docx.opc.part import PartFactory
10
+ from docx.opc.parts.coreprops import CorePropertiesPart
11
+ from docx.opc.parts.customprops import CustomPropertiesPart
12
+ from docx.opc.pkgreader import PackageReader
13
+ from docx.opc.pkgwriter import PackageWriter
14
+ from docx.opc.rel import Relationships, _Relationship
15
+ from docx.shared import lazyproperty
16
+
17
+ if TYPE_CHECKING:
18
+ from typing_extensions import Self
19
+
20
+ from docx.opc.coreprops import CoreProperties
21
+ from docx.opc.customprops import CustomProperties
22
+ from docx.opc.part import Part
23
+ from docx.opc.rel import _Relationship # pyright: ignore[reportPrivateUsage]
24
+
25
+
26
+ class OpcPackage:
27
+ """Main API class for |python-opc|.
28
+
29
+ A new instance is constructed by calling the :meth:`open` class method with a path
30
+ to a package file or file-like object containing one.
31
+ """
32
+
33
+ def after_unmarshal(self):
34
+ """Entry point for any post-unmarshaling processing.
35
+
36
+ May be overridden by subclasses without forwarding call to super.
37
+ """
38
+ # don't place any code here, just catch call if not overridden by
39
+ # subclass
40
+ pass
41
+
42
+ @property
43
+ def core_properties(self) -> CoreProperties:
44
+ """|CoreProperties| object providing read/write access to the Dublin Core
45
+ properties for this document."""
46
+ return self._core_properties_part.core_properties
47
+
48
+ @property
49
+ def custom_properties(self) -> CustomProperties:
50
+ """|CustomProperties| object providing read/write access to the custom
51
+ properties for this document."""
52
+ return self._custom_properties_part.custom_properties
53
+
54
+ def iter_rels(self) -> Iterator[_Relationship]:
55
+ """Generate exactly one reference to each relationship in the package by
56
+ performing a depth-first traversal of the rels graph."""
57
+
58
+ def walk_rels(
59
+ source: OpcPackage | Part, visited: list[Part] | None = None
60
+ ) -> Iterator[_Relationship]:
61
+ visited = [] if visited is None else visited
62
+ for rel in source.rels.values():
63
+ yield rel
64
+ if rel.is_external:
65
+ continue
66
+ part = rel.target_part
67
+ if part in visited:
68
+ continue
69
+ visited.append(part)
70
+ new_source = part
71
+ for rel in walk_rels(new_source, visited):
72
+ yield rel
73
+
74
+ for rel in walk_rels(self):
75
+ yield rel
76
+
77
+ def iter_parts(self) -> Iterator[Part]:
78
+ """Generate exactly one reference to each of the parts in the package by
79
+ performing a depth-first traversal of the rels graph."""
80
+
81
+ def walk_parts(source, visited=[]):
82
+ for rel in source.rels.values():
83
+ if rel.is_external:
84
+ continue
85
+ part = rel.target_part
86
+ if part in visited:
87
+ continue
88
+ visited.append(part)
89
+ yield part
90
+ new_source = part
91
+ for part in walk_parts(new_source, visited):
92
+ yield part
93
+
94
+ for part in walk_parts(self):
95
+ yield part
96
+
97
+ def load_rel(self, reltype: str, target: Part | str, rId: str, is_external: bool = False):
98
+ """Return newly added |_Relationship| instance of `reltype` between this part
99
+ and `target` with key `rId`.
100
+
101
+ Target mode is set to ``RTM.EXTERNAL`` if `is_external` is |True|. Intended for
102
+ use during load from a serialized package, where the rId is well known. Other
103
+ methods exist for adding a new relationship to the package during processing.
104
+ """
105
+ return self.rels.add_relationship(reltype, target, rId, is_external)
106
+
107
+ @property
108
+ def main_document_part(self):
109
+ """Return a reference to the main document part for this package.
110
+
111
+ Examples include a document part for a WordprocessingML package, a presentation
112
+ part for a PresentationML package, or a workbook part for a SpreadsheetML
113
+ package.
114
+ """
115
+ return self.part_related_by(RT.OFFICE_DOCUMENT)
116
+
117
+ def next_partname(self, template: str) -> PackURI:
118
+ """Return a |PackURI| instance representing partname matching `template`.
119
+
120
+ The returned part-name has the next available numeric suffix to distinguish it
121
+ from other parts of its type. `template` is a printf (%)-style template string
122
+ containing a single replacement item, a '%d' to be used to insert the integer
123
+ portion of the partname. Example: "/word/header%d.xml"
124
+ """
125
+ partnames = {part.partname for part in self.iter_parts()}
126
+ for n in range(1, len(partnames) + 2):
127
+ candidate_partname = template % n
128
+ if candidate_partname not in partnames:
129
+ return PackURI(candidate_partname)
130
+
131
+ @classmethod
132
+ def open(cls, pkg_file: str | IO[bytes]) -> Self:
133
+ """Return an |OpcPackage| instance loaded with the contents of `pkg_file`."""
134
+ pkg_reader = PackageReader.from_file(pkg_file)
135
+ package = cls()
136
+ Unmarshaller.unmarshal(pkg_reader, package, PartFactory)
137
+ return package
138
+
139
+ def part_related_by(self, reltype: str) -> Part:
140
+ """Return part to which this package has a relationship of `reltype`.
141
+
142
+ Raises |KeyError| if no such relationship is found and |ValueError| if more than
143
+ one such relationship is found.
144
+ """
145
+ return self.rels.part_with_reltype(reltype)
146
+
147
+ @property
148
+ def parts(self) -> list[Part]:
149
+ """Return a list containing a reference to each of the parts in this package."""
150
+ return list(self.iter_parts())
151
+
152
+ def relate_to(self, part: Part, reltype: str):
153
+ """Return rId key of new or existing relationship to `part`.
154
+
155
+ If a relationship of `reltype` to `part` already exists, its rId is returned. Otherwise a
156
+ new relationship is created and that rId is returned.
157
+ """
158
+ rel = self.rels.get_or_add(reltype, part)
159
+ return rel.rId
160
+
161
+ @property
162
+ def rels(self) -> Relationships:
163
+ """Return a reference to the |Relationships| instance holding the collection of
164
+ relationships for this package."""
165
+ if not hasattr(self, "_rels"):
166
+ self._rels = Relationships(PACKAGE_URI.baseURI)
167
+ return self._rels
168
+
169
+ @rels.setter
170
+ def rels(self, new_rels: Relationships):
171
+ """Set the relationships collection for this package."""
172
+ self._rels = new_rels
173
+
174
+ def drop_rels(self, cmp: Callable[[_Relationship], bool]) -> list[str]:
175
+ """Drop _Relationship by cmp function, related parts will be ignored when save."""
176
+ rids = [rid for rid, rel in self.rels.items() if cmp(rel)]
177
+ for rid in rids:
178
+ del self.rels[rid]
179
+ return rids
180
+
181
+ def save(self, pkg_file: str | IO[bytes]):
182
+ """Save this package to `pkg_file`.
183
+
184
+ `pkg_file` can be either a file-path or a file-like object.
185
+ """
186
+ for part in self.parts:
187
+ part.before_marshal()
188
+ PackageWriter.write(pkg_file, self.rels, self.parts)
189
+
190
+ @property
191
+ def _core_properties_part(self) -> CorePropertiesPart:
192
+ """|CorePropertiesPart| object related to this package.
193
+
194
+ Creates a default core properties part if one is not present (not common).
195
+ """
196
+ try:
197
+ return cast(CorePropertiesPart, self.part_related_by(RT.CORE_PROPERTIES))
198
+ except KeyError:
199
+ core_properties_part = CorePropertiesPart.default(self)
200
+ self.relate_to(core_properties_part, RT.CORE_PROPERTIES)
201
+ return core_properties_part
202
+
203
+ @property
204
+ def _custom_properties_part(self) -> CustomPropertiesPart:
205
+ """|CustomPropertiesPart| object related to this package.
206
+
207
+ Creates a default custom properties part if one is not present.
208
+ """
209
+ try:
210
+ return cast(CustomPropertiesPart, self.part_related_by(RT.CUSTOM_PROPERTIES))
211
+ except KeyError:
212
+ custom_properties_part = CustomPropertiesPart.default(self)
213
+ self.relate_to(custom_properties_part, RT.CUSTOM_PROPERTIES)
214
+ return custom_properties_part
215
+
216
+
217
+ class Unmarshaller:
218
+ """Hosts static methods for unmarshalling a package from a |PackageReader|."""
219
+
220
+ @staticmethod
221
+ def unmarshal(pkg_reader, package, part_factory):
222
+ """Construct graph of parts and realized relationships based on the contents of
223
+ `pkg_reader`, delegating construction of each part to `part_factory`.
224
+
225
+ Package relationships are added to `pkg`.
226
+ """
227
+ parts = Unmarshaller._unmarshal_parts(pkg_reader, package, part_factory)
228
+ Unmarshaller._unmarshal_relationships(pkg_reader, package, parts)
229
+ for part in parts.values():
230
+ part.after_unmarshal()
231
+ package.after_unmarshal()
232
+
233
+ @staticmethod
234
+ def _unmarshal_parts(pkg_reader, package, part_factory):
235
+ """Return a dictionary of |Part| instances unmarshalled from `pkg_reader`, keyed
236
+ by partname.
237
+
238
+ Side-effect is that each part in `pkg_reader` is constructed using
239
+ `part_factory`.
240
+ """
241
+ parts = {}
242
+ for partname, content_type, reltype, blob in pkg_reader.iter_sparts():
243
+ parts[partname] = part_factory(partname, content_type, reltype, blob, package)
244
+ return parts
245
+
246
+ @staticmethod
247
+ def _unmarshal_relationships(pkg_reader, package, parts):
248
+ """Add a relationship to the source object corresponding to each of the
249
+ relationships in `pkg_reader` with its target_part set to the actual target part
250
+ in `parts`."""
251
+ for source_uri, srel in pkg_reader.iter_srels():
252
+ source = package if source_uri == "/" else parts[source_uri]
253
+ target = srel.target_ref if srel.is_external else parts[srel.target_partname]
254
+ source.load_rel(srel.reltype, target, srel.rId, srel.is_external)
docx/opc/packuri.py ADDED
@@ -0,0 +1,109 @@
1
+ """Provides the PackURI value type.
2
+
3
+ Also some useful known pack URI strings such as PACKAGE_URI.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import posixpath
9
+ import re
10
+
11
+
12
+ class PackURI(str):
13
+ """Provides access to pack URI components such as the baseURI and the filename slice.
14
+
15
+ Behaves as |str| otherwise.
16
+ """
17
+
18
+ _filename_re = re.compile("([a-zA-Z]+)([1-9][0-9]*)?")
19
+
20
+ def __new__(cls, pack_uri_str: str):
21
+ if pack_uri_str[0] != "/":
22
+ tmpl = "PackURI must begin with slash, got '%s'"
23
+ raise ValueError(tmpl % pack_uri_str)
24
+ return str.__new__(cls, pack_uri_str)
25
+
26
+ @staticmethod
27
+ def from_rel_ref(baseURI: str, relative_ref: str) -> PackURI:
28
+ """The absolute PackURI formed by translating `relative_ref` onto `baseURI`."""
29
+ joined_uri = posixpath.join(baseURI, relative_ref)
30
+ abs_uri = posixpath.abspath(joined_uri)
31
+ return PackURI(abs_uri)
32
+
33
+ @property
34
+ def baseURI(self) -> str:
35
+ """The base URI of this pack URI, the directory portion, roughly speaking.
36
+
37
+ E.g. ``'/ppt/slides'`` for ``'/ppt/slides/slide1.xml'``. For the package pseudo-
38
+ partname '/', baseURI is '/'.
39
+ """
40
+ return posixpath.split(self)[0]
41
+
42
+ @property
43
+ def ext(self) -> str:
44
+ """The extension portion of this pack URI, e.g. ``'xml'`` for ``'/word/document.xml'``.
45
+
46
+ Note the period is not included.
47
+ """
48
+ # raw_ext is either empty string or starts with period, e.g. '.xml'
49
+ raw_ext = posixpath.splitext(self)[1]
50
+ return raw_ext[1:] if raw_ext.startswith(".") else raw_ext
51
+
52
+ @property
53
+ def filename(self):
54
+ """The "filename" portion of this pack URI, e.g. ``'slide1.xml'`` for
55
+ ``'/ppt/slides/slide1.xml'``.
56
+
57
+ For the package pseudo-partname '/', filename is ''.
58
+ """
59
+ return posixpath.split(self)[1]
60
+
61
+ @property
62
+ def idx(self):
63
+ """Return partname index as integer for tuple partname or None for singleton
64
+ partname, e.g. ``21`` for ``'/ppt/slides/slide21.xml'`` and |None| for
65
+ ``'/ppt/presentation.xml'``."""
66
+ filename = self.filename
67
+ if not filename:
68
+ return None
69
+ name_part = posixpath.splitext(filename)[0] # filename w/ext removed
70
+ match = self._filename_re.match(name_part)
71
+ if match is None:
72
+ return None
73
+ if match.group(2):
74
+ return int(match.group(2))
75
+ return None
76
+
77
+ @property
78
+ def membername(self):
79
+ """The pack URI with the leading slash stripped off, the form used as the Zip
80
+ file membername for the package item.
81
+
82
+ Returns '' for the package pseudo-partname '/'.
83
+ """
84
+ return self[1:]
85
+
86
+ def relative_ref(self, baseURI: str):
87
+ """Return string containing relative reference to package item from `baseURI`.
88
+
89
+ E.g. PackURI('/ppt/slideLayouts/slideLayout1.xml') would return
90
+ '../slideLayouts/slideLayout1.xml' for baseURI '/ppt/slides'.
91
+ """
92
+ # workaround for posixpath bug in 2.6, doesn't generate correct
93
+ # relative path when `start` (second) parameter is root ('/')
94
+ return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI)
95
+
96
+ @property
97
+ def rels_uri(self):
98
+ """The pack URI of the .rels part corresponding to the current pack URI.
99
+
100
+ Only produces sensible output if the pack URI is a partname or the package
101
+ pseudo-partname '/'.
102
+ """
103
+ rels_filename = "%s.rels" % self.filename
104
+ rels_uri_str = posixpath.join(self.baseURI, "_rels", rels_filename)
105
+ return PackURI(rels_uri_str)
106
+
107
+
108
+ PACKAGE_URI = PackURI("/")
109
+ CONTENT_TYPES_URI = PackURI("/[Content_Types].xml")