ps-python-docx 1.3.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 (126) hide show
  1. docx/__init__.py +68 -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 +275 -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 +23 -0
  19. docx/image/bmp.py +43 -0
  20. docx/image/constants.py +172 -0
  21. docx/image/exceptions.py +13 -0
  22. docx/image/gif.py +38 -0
  23. docx/image/helpers.py +86 -0
  24. docx/image/image.py +234 -0
  25. docx/image/jpeg.py +425 -0
  26. docx/image/png.py +253 -0
  27. docx/image/tiff.py +289 -0
  28. docx/opc/__init__.py +0 -0
  29. docx/opc/constants.py +306 -0
  30. docx/opc/coreprops.py +142 -0
  31. docx/opc/exceptions.py +12 -0
  32. docx/opc/oxml.py +247 -0
  33. docx/opc/package.py +219 -0
  34. docx/opc/packuri.py +109 -0
  35. docx/opc/part.py +247 -0
  36. docx/opc/parts/__init__.py +0 -0
  37. docx/opc/parts/coreprops.py +48 -0
  38. docx/opc/phys_pkg.py +119 -0
  39. docx/opc/pkgreader.py +254 -0
  40. docx/opc/pkgwriter.py +115 -0
  41. docx/opc/rel.py +153 -0
  42. docx/opc/shared.py +31 -0
  43. docx/opc/spec.py +24 -0
  44. docx/oxml/__init__.py +261 -0
  45. docx/oxml/comments.py +124 -0
  46. docx/oxml/coreprops.py +298 -0
  47. docx/oxml/document.py +88 -0
  48. docx/oxml/drawing.py +11 -0
  49. docx/oxml/exceptions.py +10 -0
  50. docx/oxml/ns.py +109 -0
  51. docx/oxml/numbering.py +109 -0
  52. docx/oxml/parser.py +62 -0
  53. docx/oxml/section.py +537 -0
  54. docx/oxml/settings.py +138 -0
  55. docx/oxml/shape.py +299 -0
  56. docx/oxml/shared.py +52 -0
  57. docx/oxml/simpletypes.py +434 -0
  58. docx/oxml/styles.py +341 -0
  59. docx/oxml/table.py +977 -0
  60. docx/oxml/text/__init__.py +0 -0
  61. docx/oxml/text/font.py +333 -0
  62. docx/oxml/text/hyperlink.py +45 -0
  63. docx/oxml/text/pagebreak.py +278 -0
  64. docx/oxml/text/paragraph.py +106 -0
  65. docx/oxml/text/parfmt.py +392 -0
  66. docx/oxml/text/run.py +307 -0
  67. docx/oxml/xmlchemy.py +696 -0
  68. docx/package.py +110 -0
  69. docx/parts/__init__.py +0 -0
  70. docx/parts/comments.py +51 -0
  71. docx/parts/document.py +182 -0
  72. docx/parts/hdrftr.py +53 -0
  73. docx/parts/image.py +80 -0
  74. docx/parts/numbering.py +32 -0
  75. docx/parts/settings.py +50 -0
  76. docx/parts/story.py +95 -0
  77. docx/parts/styles.py +42 -0
  78. docx/parts/theme.py +53 -0
  79. docx/py.typed +0 -0
  80. docx/section.py +479 -0
  81. docx/settings.py +35 -0
  82. docx/shape.py +103 -0
  83. docx/shared.py +382 -0
  84. docx/styles/__init__.py +40 -0
  85. docx/styles/latent.py +198 -0
  86. docx/styles/style.py +264 -0
  87. docx/styles/styles.py +147 -0
  88. docx/table.py +537 -0
  89. docx/templates/default-comments.xml +12 -0
  90. docx/templates/default-docx-template/[Content_Types].xml +17 -0
  91. docx/templates/default-docx-template/_rels/.rels +7 -0
  92. docx/templates/default-docx-template/customXml/_rels/item1.xml.rels +4 -0
  93. docx/templates/default-docx-template/customXml/item1.xml +2 -0
  94. docx/templates/default-docx-template/customXml/itemProps1.xml +6 -0
  95. docx/templates/default-docx-template/docProps/app.xml +36 -0
  96. docx/templates/default-docx-template/docProps/core.xml +13 -0
  97. docx/templates/default-docx-template/docProps/thumbnail.jpeg +0 -0
  98. docx/templates/default-docx-template/word/_rels/document.xml.rels +11 -0
  99. docx/templates/default-docx-template/word/document.xml +11 -0
  100. docx/templates/default-docx-template/word/fontTable.xml +61 -0
  101. docx/templates/default-docx-template/word/numbering.xml +201 -0
  102. docx/templates/default-docx-template/word/settings.xml +53 -0
  103. docx/templates/default-docx-template/word/styles.xml +11844 -0
  104. docx/templates/default-docx-template/word/stylesWithEffects.xml +11800 -0
  105. docx/templates/default-docx-template/word/theme/theme1.xml +318 -0
  106. docx/templates/default-docx-template/word/webSettings.xml +5 -0
  107. docx/templates/default-footer.xml +27 -0
  108. docx/templates/default-header.xml +27 -0
  109. docx/templates/default-settings.xml +26 -0
  110. docx/templates/default-styles.xml +190 -0
  111. docx/templates/default.docx +0 -0
  112. docx/text/__init__.py +0 -0
  113. docx/text/font.py +472 -0
  114. docx/text/hyperlink.py +121 -0
  115. docx/text/pagebreak.py +104 -0
  116. docx/text/paragraph.py +173 -0
  117. docx/text/parfmt.py +286 -0
  118. docx/text/run.py +257 -0
  119. docx/text/tabstops.py +123 -0
  120. docx/theme.py +67 -0
  121. docx/types.py +34 -0
  122. ps_python_docx-1.3.0.dist-info/METADATA +77 -0
  123. ps_python_docx-1.3.0.dist-info/RECORD +126 -0
  124. ps_python_docx-1.3.0.dist-info/WHEEL +5 -0
  125. ps_python_docx-1.3.0.dist-info/licenses/LICENSE +20 -0
  126. ps_python_docx-1.3.0.dist-info/top_level.txt +1 -0
docx/oxml/__init__.py ADDED
@@ -0,0 +1,261 @@
1
+ # ruff: noqa: E402, I001
2
+
3
+ """Initializes oxml sub-package.
4
+
5
+ This including registering custom element classes corresponding to Open XML elements.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from docx.oxml.drawing import CT_Drawing
11
+ from docx.oxml.parser import OxmlElement, parse_xml, register_element_cls
12
+ from docx.oxml.shape import (
13
+ CT_Anchor,
14
+ CT_Blip,
15
+ CT_BlipFillProperties,
16
+ CT_GraphicalObject,
17
+ CT_GraphicalObjectData,
18
+ CT_Inline,
19
+ CT_NonVisualDrawingProps,
20
+ CT_Picture,
21
+ CT_PictureNonVisual,
22
+ CT_Point2D,
23
+ CT_PositiveSize2D,
24
+ CT_ShapeProperties,
25
+ CT_Transform2D,
26
+ )
27
+ from docx.oxml.shared import CT_DecimalNumber, CT_OnOff, CT_String
28
+ from docx.oxml.text.hyperlink import CT_Hyperlink
29
+ from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak
30
+ from docx.oxml.text.run import (
31
+ CT_R,
32
+ CT_Br,
33
+ CT_Cr,
34
+ CT_NoBreakHyphen,
35
+ CT_PTab,
36
+ CT_Text,
37
+ )
38
+
39
+ # -- `OxmlElement` and `parse_xml()` are not used in this module but several downstream
40
+ # -- "extension" packages expect to find them here and there's no compelling reason
41
+ # -- not to republish them here so those keep working.
42
+ __all__ = ["OxmlElement", "parse_xml"]
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # DrawingML-related elements
46
+
47
+ register_element_cls("a:blip", CT_Blip)
48
+ register_element_cls("a:ext", CT_PositiveSize2D)
49
+ register_element_cls("a:graphic", CT_GraphicalObject)
50
+ register_element_cls("a:graphicData", CT_GraphicalObjectData)
51
+ register_element_cls("a:off", CT_Point2D)
52
+ register_element_cls("a:xfrm", CT_Transform2D)
53
+ register_element_cls("pic:blipFill", CT_BlipFillProperties)
54
+ register_element_cls("pic:cNvPr", CT_NonVisualDrawingProps)
55
+ register_element_cls("pic:nvPicPr", CT_PictureNonVisual)
56
+ register_element_cls("pic:pic", CT_Picture)
57
+ register_element_cls("pic:spPr", CT_ShapeProperties)
58
+ register_element_cls("w:drawing", CT_Drawing)
59
+ register_element_cls("wp:anchor", CT_Anchor)
60
+ register_element_cls("wp:docPr", CT_NonVisualDrawingProps)
61
+ register_element_cls("wp:extent", CT_PositiveSize2D)
62
+ register_element_cls("wp:inline", CT_Inline)
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # hyperlink-related elements
66
+
67
+ register_element_cls("w:hyperlink", CT_Hyperlink)
68
+
69
+ # ---------------------------------------------------------------------------
70
+ # text-related elements
71
+
72
+ register_element_cls("w:br", CT_Br)
73
+ register_element_cls("w:cr", CT_Cr)
74
+ register_element_cls("w:lastRenderedPageBreak", CT_LastRenderedPageBreak)
75
+ register_element_cls("w:noBreakHyphen", CT_NoBreakHyphen)
76
+ register_element_cls("w:ptab", CT_PTab)
77
+ register_element_cls("w:r", CT_R)
78
+ register_element_cls("w:t", CT_Text)
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # header/footer-related mappings
82
+
83
+ register_element_cls("w:evenAndOddHeaders", CT_OnOff)
84
+ register_element_cls("w:titlePg", CT_OnOff)
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # other custom element class mappings
88
+
89
+ from .comments import CT_Comments, CT_Comment
90
+
91
+ register_element_cls("w:comments", CT_Comments)
92
+ register_element_cls("w:comment", CT_Comment)
93
+
94
+ from .coreprops import CT_CoreProperties
95
+
96
+ register_element_cls("cp:coreProperties", CT_CoreProperties)
97
+
98
+ from .document import CT_Body, CT_Document
99
+
100
+ register_element_cls("w:body", CT_Body)
101
+ register_element_cls("w:document", CT_Document)
102
+
103
+ from .numbering import CT_Num, CT_Numbering, CT_NumLvl, CT_NumPr
104
+
105
+ register_element_cls("w:abstractNumId", CT_DecimalNumber)
106
+ register_element_cls("w:ilvl", CT_DecimalNumber)
107
+ register_element_cls("w:lvlOverride", CT_NumLvl)
108
+ register_element_cls("w:num", CT_Num)
109
+ register_element_cls("w:numId", CT_DecimalNumber)
110
+ register_element_cls("w:numPr", CT_NumPr)
111
+ register_element_cls("w:numbering", CT_Numbering)
112
+ register_element_cls("w:startOverride", CT_DecimalNumber)
113
+
114
+ from .section import (
115
+ CT_HdrFtr,
116
+ CT_HdrFtrRef,
117
+ CT_PageMar,
118
+ CT_PageSz,
119
+ CT_SectPr,
120
+ CT_SectType,
121
+ )
122
+
123
+ register_element_cls("w:footerReference", CT_HdrFtrRef)
124
+ register_element_cls("w:ftr", CT_HdrFtr)
125
+ register_element_cls("w:hdr", CT_HdrFtr)
126
+ register_element_cls("w:headerReference", CT_HdrFtrRef)
127
+ register_element_cls("w:pgMar", CT_PageMar)
128
+ register_element_cls("w:pgSz", CT_PageSz)
129
+ register_element_cls("w:sectPr", CT_SectPr)
130
+ register_element_cls("w:type", CT_SectType)
131
+
132
+ from .settings import CT_Settings
133
+
134
+ register_element_cls("w:settings", CT_Settings)
135
+
136
+ from .styles import (
137
+ CT_DocDefaults,
138
+ CT_LatentStyles,
139
+ CT_LsdException,
140
+ CT_RPrDefault,
141
+ CT_Style,
142
+ CT_Styles,
143
+ )
144
+
145
+ register_element_cls("w:docDefaults", CT_DocDefaults)
146
+ register_element_cls("w:rPrDefault", CT_RPrDefault)
147
+ register_element_cls("w:link", CT_String)
148
+ register_element_cls("w:basedOn", CT_String)
149
+ register_element_cls("w:latentStyles", CT_LatentStyles)
150
+ register_element_cls("w:locked", CT_OnOff)
151
+ register_element_cls("w:lsdException", CT_LsdException)
152
+ register_element_cls("w:name", CT_String)
153
+ register_element_cls("w:next", CT_String)
154
+ register_element_cls("w:qFormat", CT_OnOff)
155
+ register_element_cls("w:semiHidden", CT_OnOff)
156
+ register_element_cls("w:style", CT_Style)
157
+ register_element_cls("w:styles", CT_Styles)
158
+ register_element_cls("w:uiPriority", CT_DecimalNumber)
159
+ register_element_cls("w:unhideWhenUsed", CT_OnOff)
160
+
161
+ from .table import (
162
+ CT_Height,
163
+ CT_Row,
164
+ CT_Tbl,
165
+ CT_TblGrid,
166
+ CT_TblGridCol,
167
+ CT_TblLayoutType,
168
+ CT_TblPr,
169
+ CT_TblPrEx,
170
+ CT_TblWidth,
171
+ CT_Tc,
172
+ CT_TcPr,
173
+ CT_TrPr,
174
+ CT_VMerge,
175
+ CT_VerticalJc,
176
+ )
177
+
178
+ register_element_cls("w:bidiVisual", CT_OnOff)
179
+ register_element_cls("w:gridAfter", CT_DecimalNumber)
180
+ register_element_cls("w:gridBefore", CT_DecimalNumber)
181
+ register_element_cls("w:gridCol", CT_TblGridCol)
182
+ register_element_cls("w:gridSpan", CT_DecimalNumber)
183
+ register_element_cls("w:tbl", CT_Tbl)
184
+ register_element_cls("w:tblGrid", CT_TblGrid)
185
+ register_element_cls("w:tblLayout", CT_TblLayoutType)
186
+ register_element_cls("w:tblPr", CT_TblPr)
187
+ register_element_cls("w:tblPrEx", CT_TblPrEx)
188
+ register_element_cls("w:tblStyle", CT_String)
189
+ register_element_cls("w:tc", CT_Tc)
190
+ register_element_cls("w:tcPr", CT_TcPr)
191
+ register_element_cls("w:tcW", CT_TblWidth)
192
+ register_element_cls("w:tr", CT_Row)
193
+ register_element_cls("w:trHeight", CT_Height)
194
+ register_element_cls("w:trPr", CT_TrPr)
195
+ register_element_cls("w:vAlign", CT_VerticalJc)
196
+ register_element_cls("w:vMerge", CT_VMerge)
197
+
198
+ from .text.font import (
199
+ CT_Color,
200
+ CT_Fonts,
201
+ CT_Highlight,
202
+ CT_HpsMeasure,
203
+ CT_RPr,
204
+ CT_Underline,
205
+ CT_VerticalAlignRun,
206
+ )
207
+
208
+ register_element_cls("w:b", CT_OnOff)
209
+ register_element_cls("w:bCs", CT_OnOff)
210
+ register_element_cls("w:caps", CT_OnOff)
211
+ register_element_cls("w:color", CT_Color)
212
+ register_element_cls("w:cs", CT_OnOff)
213
+ register_element_cls("w:dstrike", CT_OnOff)
214
+ register_element_cls("w:emboss", CT_OnOff)
215
+ register_element_cls("w:highlight", CT_Highlight)
216
+ register_element_cls("w:i", CT_OnOff)
217
+ register_element_cls("w:iCs", CT_OnOff)
218
+ register_element_cls("w:imprint", CT_OnOff)
219
+ register_element_cls("w:noProof", CT_OnOff)
220
+ register_element_cls("w:oMath", CT_OnOff)
221
+ register_element_cls("w:outline", CT_OnOff)
222
+ register_element_cls("w:rFonts", CT_Fonts)
223
+ register_element_cls("w:rPr", CT_RPr)
224
+ register_element_cls("w:rStyle", CT_String)
225
+ register_element_cls("w:rtl", CT_OnOff)
226
+ register_element_cls("w:shadow", CT_OnOff)
227
+ register_element_cls("w:smallCaps", CT_OnOff)
228
+ register_element_cls("w:snapToGrid", CT_OnOff)
229
+ register_element_cls("w:specVanish", CT_OnOff)
230
+ register_element_cls("w:strike", CT_OnOff)
231
+ register_element_cls("w:sz", CT_HpsMeasure)
232
+ register_element_cls("w:u", CT_Underline)
233
+ register_element_cls("w:vanish", CT_OnOff)
234
+ register_element_cls("w:vertAlign", CT_VerticalAlignRun)
235
+ register_element_cls("w:webHidden", CT_OnOff)
236
+
237
+ from .text.paragraph import CT_P
238
+
239
+ register_element_cls("w:p", CT_P)
240
+
241
+ from .text.parfmt import (
242
+ CT_Ind,
243
+ CT_Jc,
244
+ CT_PPr,
245
+ CT_Spacing,
246
+ CT_TabStop,
247
+ CT_TabStops,
248
+ )
249
+
250
+ register_element_cls("w:ind", CT_Ind)
251
+ register_element_cls("w:jc", CT_Jc)
252
+ register_element_cls("w:keepLines", CT_OnOff)
253
+ register_element_cls("w:keepNext", CT_OnOff)
254
+ register_element_cls("w:outlineLvl", CT_DecimalNumber)
255
+ register_element_cls("w:pageBreakBefore", CT_OnOff)
256
+ register_element_cls("w:pPr", CT_PPr)
257
+ register_element_cls("w:pStyle", CT_String)
258
+ register_element_cls("w:spacing", CT_Spacing)
259
+ register_element_cls("w:tab", CT_TabStop)
260
+ register_element_cls("w:tabs", CT_TabStops)
261
+ register_element_cls("w:widowControl", CT_OnOff)
docx/oxml/comments.py ADDED
@@ -0,0 +1,124 @@
1
+ """Custom element classes related to document comments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as dt
6
+ from typing import TYPE_CHECKING, Callable, cast
7
+
8
+ from docx.oxml.ns import nsdecls
9
+ from docx.oxml.parser import parse_xml
10
+ from docx.oxml.simpletypes import ST_DateTime, ST_DecimalNumber, ST_String
11
+ from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore
12
+
13
+ if TYPE_CHECKING:
14
+ from docx.oxml.table import CT_Tbl
15
+ from docx.oxml.text.paragraph import CT_P
16
+
17
+
18
+ class CT_Comments(BaseOxmlElement):
19
+ """`w:comments` element, the root element for the comments part.
20
+
21
+ Simply contains a collection of `w:comment` elements, each representing a single comment. Each
22
+ contained comment is identified by a unique `w:id` attribute, used to reference the comment
23
+ from the document text. The offset of the comment in this collection is arbitrary; it is
24
+ essentially a _set_ implemented as a list.
25
+ """
26
+
27
+ # -- type-declarations to fill in the gaps for metaclass-added methods --
28
+ comment_lst: list[CT_Comment]
29
+
30
+ comment = ZeroOrMore("w:comment")
31
+
32
+ def add_comment(self) -> CT_Comment:
33
+ """Return newly added `w:comment` child of this `w:comments`.
34
+
35
+ The returned `w:comment` element is the minimum valid value, having a `w:id` value unique
36
+ within the existing comments and the required `w:author` attribute present but set to the
37
+ empty string. It's content is limited to a single run containing the necessary annotation
38
+ reference but no text. Content is added by adding runs to this first paragraph and by
39
+ adding additional paragraphs as needed.
40
+ """
41
+ next_id = self._next_available_comment_id()
42
+ comment = cast(
43
+ CT_Comment,
44
+ parse_xml(
45
+ f'<w:comment {nsdecls("w")} w:id="{next_id}" w:author="">'
46
+ f" <w:p>"
47
+ f" <w:pPr>"
48
+ f' <w:pStyle w:val="CommentText"/>'
49
+ f" </w:pPr>"
50
+ f" <w:r>"
51
+ f" <w:rPr>"
52
+ f' <w:rStyle w:val="CommentReference"/>'
53
+ f" </w:rPr>"
54
+ f" <w:annotationRef/>"
55
+ f" </w:r>"
56
+ f" </w:p>"
57
+ f"</w:comment>"
58
+ ),
59
+ )
60
+ self.append(comment)
61
+ return comment
62
+
63
+ def get_comment_by_id(self, comment_id: int) -> CT_Comment | None:
64
+ """Return the `w:comment` element identified by `comment_id`, or |None| if not found."""
65
+ comment_elms = self.xpath(f"(./w:comment[@w:id='{comment_id}'])[1]")
66
+ return comment_elms[0] if comment_elms else None
67
+
68
+ def _next_available_comment_id(self) -> int:
69
+ """The next available comment id.
70
+
71
+ According to the schema, this can be any positive integer, as big as you like, and the
72
+ default mechanism is to use `max() + 1`. However, if that yields a value larger than will
73
+ fit in a 32-bit signed integer, we take a more deliberate approach to use the first
74
+ ununsed integer starting from 0.
75
+ """
76
+ used_ids = [int(x) for x in self.xpath("./w:comment/@w:id")]
77
+
78
+ next_id = max(used_ids, default=-1) + 1
79
+
80
+ if next_id <= 2**31 - 1:
81
+ return next_id
82
+
83
+ # -- fall-back to enumerating all used ids to find the first unused one --
84
+ for expected, actual in enumerate(sorted(used_ids)):
85
+ if expected != actual:
86
+ return expected
87
+
88
+ return len(used_ids)
89
+
90
+
91
+ class CT_Comment(BaseOxmlElement):
92
+ """`w:comment` element, representing a single comment.
93
+
94
+ A comment is a so-called "story" and can contain paragraphs and tables much like a table-cell.
95
+ While probably most often used for a single sentence or phrase, a comment can contain rich
96
+ content, including multiple rich-text paragraphs, hyperlinks, images, and tables.
97
+ """
98
+
99
+ # -- attributes on `w:comment` --
100
+ id: int = RequiredAttribute("w:id", ST_DecimalNumber) # pyright: ignore[reportAssignmentType]
101
+ author: str = RequiredAttribute("w:author", ST_String) # pyright: ignore[reportAssignmentType]
102
+ initials: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
103
+ "w:initials", ST_String
104
+ )
105
+ date: dt.datetime | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
106
+ "w:date", ST_DateTime
107
+ )
108
+
109
+ # -- children --
110
+
111
+ p = ZeroOrMore("w:p", successors=())
112
+ tbl = ZeroOrMore("w:tbl", successors=())
113
+
114
+ # -- type-declarations for methods added by metaclass --
115
+
116
+ add_p: Callable[[], CT_P]
117
+ p_lst: list[CT_P]
118
+ tbl_lst: list[CT_Tbl]
119
+ _insert_tbl: Callable[[CT_Tbl], CT_Tbl]
120
+
121
+ @property
122
+ def inner_content_elements(self) -> list[CT_P | CT_Tbl]:
123
+ """Generate all `w:p` and `w:tbl` elements in this comment."""
124
+ return self.xpath("./w:p | ./w:tbl")
docx/oxml/coreprops.py ADDED
@@ -0,0 +1,298 @@
1
+ """Custom element classes for core properties-related XML elements."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as dt
6
+ import re
7
+ from typing import TYPE_CHECKING, Any, Callable, cast
8
+
9
+ from docx.oxml.ns import nsdecls, qn
10
+ from docx.oxml.parser import parse_xml
11
+ from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne
12
+
13
+ if TYPE_CHECKING:
14
+ from lxml.etree import _Element as etree_Element # pyright: ignore[reportPrivateUsage]
15
+
16
+
17
+ class CT_CoreProperties(BaseOxmlElement):
18
+ """`<cp:coreProperties>` element, the root element of the Core Properties part.
19
+
20
+ Stored as `/docProps/core.xml`. Implements many of the Dublin Core document metadata
21
+ elements. String elements resolve to an empty string ("") if the element is not
22
+ present in the XML. String elements are limited in length to 255 unicode characters.
23
+ """
24
+
25
+ get_or_add_revision: Callable[[], etree_Element]
26
+
27
+ category = ZeroOrOne("cp:category", successors=())
28
+ contentStatus = ZeroOrOne("cp:contentStatus", successors=())
29
+ created = ZeroOrOne("dcterms:created", successors=())
30
+ creator = ZeroOrOne("dc:creator", successors=())
31
+ description = ZeroOrOne("dc:description", successors=())
32
+ identifier = ZeroOrOne("dc:identifier", successors=())
33
+ keywords = ZeroOrOne("cp:keywords", successors=())
34
+ language = ZeroOrOne("dc:language", successors=())
35
+ lastModifiedBy = ZeroOrOne("cp:lastModifiedBy", successors=())
36
+ lastPrinted = ZeroOrOne("cp:lastPrinted", successors=())
37
+ modified = ZeroOrOne("dcterms:modified", successors=())
38
+ revision: etree_Element | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
39
+ "cp:revision", successors=()
40
+ )
41
+ subject = ZeroOrOne("dc:subject", successors=())
42
+ title = ZeroOrOne("dc:title", successors=())
43
+ version = ZeroOrOne("cp:version", successors=())
44
+
45
+ _coreProperties_tmpl = "<cp:coreProperties %s/>\n" % nsdecls("cp", "dc", "dcterms")
46
+
47
+ @classmethod
48
+ def new(cls) -> CT_CoreProperties:
49
+ """Return a new `<cp:coreProperties>` element."""
50
+ xml = cls._coreProperties_tmpl
51
+ coreProperties = cast(CT_CoreProperties, parse_xml(xml))
52
+ return coreProperties
53
+
54
+ @property
55
+ def author_text(self) -> str:
56
+ """The text in the `dc:creator` child element."""
57
+ return self._text_of_element("creator")
58
+
59
+ @author_text.setter
60
+ def author_text(self, value: str):
61
+ self._set_element_text("creator", value)
62
+
63
+ @property
64
+ def category_text(self) -> str:
65
+ return self._text_of_element("category")
66
+
67
+ @category_text.setter
68
+ def category_text(self, value: str):
69
+ self._set_element_text("category", value)
70
+
71
+ @property
72
+ def comments_text(self) -> str:
73
+ return self._text_of_element("description")
74
+
75
+ @comments_text.setter
76
+ def comments_text(self, value: str):
77
+ self._set_element_text("description", value)
78
+
79
+ @property
80
+ def contentStatus_text(self) -> str:
81
+ return self._text_of_element("contentStatus")
82
+
83
+ @contentStatus_text.setter
84
+ def contentStatus_text(self, value: str):
85
+ self._set_element_text("contentStatus", value)
86
+
87
+ @property
88
+ def created_datetime(self) -> dt.datetime | None:
89
+ return self._datetime_of_element("created")
90
+
91
+ @created_datetime.setter
92
+ def created_datetime(self, value: dt.datetime):
93
+ self._set_element_datetime("created", value)
94
+
95
+ @property
96
+ def identifier_text(self) -> str:
97
+ return self._text_of_element("identifier")
98
+
99
+ @identifier_text.setter
100
+ def identifier_text(self, value: str):
101
+ self._set_element_text("identifier", value)
102
+
103
+ @property
104
+ def keywords_text(self) -> str:
105
+ return self._text_of_element("keywords")
106
+
107
+ @keywords_text.setter
108
+ def keywords_text(self, value: str):
109
+ self._set_element_text("keywords", value)
110
+
111
+ @property
112
+ def language_text(self) -> str:
113
+ return self._text_of_element("language")
114
+
115
+ @language_text.setter
116
+ def language_text(self, value: str):
117
+ self._set_element_text("language", value)
118
+
119
+ @property
120
+ def lastModifiedBy_text(self) -> str:
121
+ return self._text_of_element("lastModifiedBy")
122
+
123
+ @lastModifiedBy_text.setter
124
+ def lastModifiedBy_text(self, value: str):
125
+ self._set_element_text("lastModifiedBy", value)
126
+
127
+ @property
128
+ def lastPrinted_datetime(self) -> dt.datetime | None:
129
+ return self._datetime_of_element("lastPrinted")
130
+
131
+ @lastPrinted_datetime.setter
132
+ def lastPrinted_datetime(self, value: dt.datetime):
133
+ self._set_element_datetime("lastPrinted", value)
134
+
135
+ @property
136
+ def modified_datetime(self) -> dt.datetime | None:
137
+ return self._datetime_of_element("modified")
138
+
139
+ @modified_datetime.setter
140
+ def modified_datetime(self, value: dt.datetime):
141
+ self._set_element_datetime("modified", value)
142
+
143
+ @property
144
+ def revision_number(self) -> int:
145
+ """Integer value of revision property."""
146
+ revision = self.revision
147
+ if revision is None:
148
+ return 0
149
+ revision_str = str(revision.text)
150
+ try:
151
+ revision = int(revision_str)
152
+ except ValueError:
153
+ # non-integer revision strings also resolve to 0
154
+ revision = 0
155
+ # as do negative integers
156
+ if revision < 0:
157
+ revision = 0
158
+ return revision
159
+
160
+ @revision_number.setter
161
+ def revision_number(self, value: int):
162
+ """Set revision property to string value of integer `value`."""
163
+ if not isinstance(value, int) or value < 1: # pyright: ignore[reportUnnecessaryIsInstance]
164
+ tmpl = "revision property requires positive int, got '%s'"
165
+ raise ValueError(tmpl % value)
166
+ revision = self.get_or_add_revision()
167
+ revision.text = str(value)
168
+
169
+ @property
170
+ def subject_text(self) -> str:
171
+ return self._text_of_element("subject")
172
+
173
+ @subject_text.setter
174
+ def subject_text(self, value: str):
175
+ self._set_element_text("subject", value)
176
+
177
+ @property
178
+ def title_text(self) -> str:
179
+ return self._text_of_element("title")
180
+
181
+ @title_text.setter
182
+ def title_text(self, value: str):
183
+ self._set_element_text("title", value)
184
+
185
+ @property
186
+ def version_text(self) -> str:
187
+ return self._text_of_element("version")
188
+
189
+ @version_text.setter
190
+ def version_text(self, value: str):
191
+ self._set_element_text("version", value)
192
+
193
+ def _datetime_of_element(self, property_name: str) -> dt.datetime | None:
194
+ element = getattr(self, property_name)
195
+ if element is None:
196
+ return None
197
+ datetime_str = element.text
198
+ try:
199
+ return self._parse_W3CDTF_to_datetime(datetime_str)
200
+ except ValueError:
201
+ # invalid datetime strings are ignored
202
+ return None
203
+
204
+ def _get_or_add(self, prop_name: str) -> BaseOxmlElement:
205
+ """Return element returned by "get_or_add_" method for `prop_name`."""
206
+ get_or_add_method_name = "get_or_add_%s" % prop_name
207
+ get_or_add_method = getattr(self, get_or_add_method_name)
208
+ element = get_or_add_method()
209
+ return element
210
+
211
+ @classmethod
212
+ def _offset_dt(cls, dt_: dt.datetime, offset_str: str) -> dt.datetime:
213
+ """A |datetime| instance offset from `dt_` by timezone offset in `offset_str`.
214
+
215
+ `offset_str` is like `"-07:00"`.
216
+ """
217
+ match = cls._offset_pattern.match(offset_str)
218
+ if match is None:
219
+ raise ValueError("'%s' is not a valid offset string" % offset_str)
220
+ sign, hours_str, minutes_str = match.groups()
221
+ sign_factor = -1 if sign == "+" else 1
222
+ hours = int(hours_str) * sign_factor
223
+ minutes = int(minutes_str) * sign_factor
224
+ td = dt.timedelta(hours=hours, minutes=minutes)
225
+ return dt_ + td
226
+
227
+ _offset_pattern = re.compile(r"([+-])(\d\d):(\d\d)")
228
+
229
+ @classmethod
230
+ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str: str) -> dt.datetime:
231
+ # valid W3CDTF date cases:
232
+ # yyyy e.g. "2003"
233
+ # yyyy-mm e.g. "2003-12"
234
+ # yyyy-mm-dd e.g. "2003-12-31"
235
+ # UTC timezone e.g. "2003-12-31T10:14:55Z"
236
+ # numeric timezone e.g. "2003-12-31T10:14:55-08:00"
237
+ templates = (
238
+ "%Y-%m-%dT%H:%M:%S",
239
+ "%Y-%m-%d",
240
+ "%Y-%m",
241
+ "%Y",
242
+ )
243
+ # strptime isn't smart enough to parse literal timezone offsets like
244
+ # "-07:30", so we have to do it ourselves
245
+ parseable_part = w3cdtf_str[:19]
246
+ offset_str = w3cdtf_str[19:]
247
+ dt_ = None
248
+ for tmpl in templates:
249
+ try:
250
+ dt_ = dt.datetime.strptime(parseable_part, tmpl)
251
+ except ValueError:
252
+ continue
253
+ if dt_ is None:
254
+ tmpl = "could not parse W3CDTF datetime string '%s'"
255
+ raise ValueError(tmpl % w3cdtf_str)
256
+ if len(offset_str) == 6:
257
+ dt_ = cls._offset_dt(dt_, offset_str)
258
+ return dt_.replace(tzinfo=dt.timezone.utc)
259
+
260
+ def _set_element_datetime(self, prop_name: str, value: dt.datetime) -> None:
261
+ """Set date/time value of child element having `prop_name` to `value`."""
262
+ if not isinstance(value, dt.datetime): # pyright: ignore[reportUnnecessaryIsInstance]
263
+ tmpl = "property requires <type 'datetime.datetime'> object, got %s"
264
+ raise ValueError(tmpl % type(value))
265
+ element = self._get_or_add(prop_name)
266
+ dt_str = value.strftime("%Y-%m-%dT%H:%M:%SZ")
267
+ element.text = dt_str
268
+ if prop_name in ("created", "modified"):
269
+ # These two require an explicit "xsi:type="dcterms:W3CDTF""
270
+ # attribute. The first and last line are a hack required to add
271
+ # the xsi namespace to the root element rather than each child
272
+ # element in which it is referenced
273
+ self.set(qn("xsi:foo"), "bar")
274
+ element.set(qn("xsi:type"), "dcterms:W3CDTF")
275
+ del self.attrib[qn("xsi:foo")]
276
+
277
+ def _set_element_text(self, prop_name: str, value: Any) -> None:
278
+ """Set string value of `name` property to `value`."""
279
+ if not isinstance(value, str):
280
+ value = str(value)
281
+
282
+ if len(value) > 255:
283
+ tmpl = "exceeded 255 char limit for property, got:\n\n'%s'"
284
+ raise ValueError(tmpl % value)
285
+ element = self._get_or_add(prop_name)
286
+ element.text = value
287
+
288
+ def _text_of_element(self, property_name: str) -> str:
289
+ """The text in the element matching `property_name`.
290
+
291
+ The empty string if the element is not present or contains no text.
292
+ """
293
+ element = getattr(self, property_name)
294
+ if element is None:
295
+ return ""
296
+ if element.text is None:
297
+ return ""
298
+ return element.text