python-docx-oss 0.1.1__py3-none-any.whl → 0.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.
docx/__init__.py CHANGED
@@ -13,7 +13,7 @@ from docx.api import Document
13
13
  if TYPE_CHECKING:
14
14
  from docx.opc.part import Part
15
15
 
16
- __version__ = "0.1.1"
16
+ __version__ = "0.3.0"
17
17
 
18
18
 
19
19
  __all__ = ["Document"]
docx/api.py CHANGED
@@ -5,6 +5,7 @@ Provides a syntactically more convenient API for interacting with the OpcPackage
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import io
8
9
  import os
9
10
  from typing import IO, TYPE_CHECKING, cast
10
11
 
@@ -16,18 +17,20 @@ if TYPE_CHECKING:
16
17
  from docx.parts.document import DocumentPart
17
18
 
18
19
 
19
- def Document(docx: str | IO[bytes] | None = None) -> DocumentObject:
20
+ def Document(docx: str | bytes | IO[bytes] | None = None) -> DocumentObject:
20
21
  """Return a |Document| object loaded from `docx`, where `docx` can be either a path
21
- to a ``.docx`` file (a string) or a file-like object.
22
+ to a ``.docx`` file, raw DOCX bytes, or a binary file-like object.
22
23
 
23
24
  If `docx` is missing or ``None``, the built-in default document "template" is
24
25
  loaded.
25
26
  """
26
27
  docx = _default_docx_path() if docx is None else docx
28
+ source_name = "<bytes>" if isinstance(docx, bytes) else docx
29
+ docx = io.BytesIO(docx) if isinstance(docx, bytes) else docx
27
30
  document_part = cast("DocumentPart", Package.open(docx).main_document_part)
28
31
  if document_part.content_type != CT.WML_DOCUMENT_MAIN:
29
32
  tmpl = "file '%s' is not a Word file, content type is '%s'"
30
- raise ValueError(tmpl % (docx, document_part.content_type))
33
+ raise ValueError(tmpl % (source_name, document_part.content_type))
31
34
  return document_part.document
32
35
 
33
36
 
docx/document.py CHANGED
@@ -5,11 +5,14 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import io
8
9
  from typing import IO, TYPE_CHECKING, Iterator, List, Sequence
9
10
 
11
+ from docx.api import Document as open_document
10
12
  from docx.blkcntnr import BlockItemContainer
11
13
  from docx.enum.section import WD_SECTION
12
14
  from docx.enum.text import WD_BREAK
15
+ from docx.sanitize import SanitizationReport, sanitize_document
13
16
  from docx.section import Section, Sections
14
17
  from docx.shared import ElementProxy, Emu, Inches, Length
15
18
  from docx.text.run import Run
@@ -182,6 +185,10 @@ class Document(ElementProxy):
182
185
  """A |CoreProperties| object providing Dublin Core properties of document."""
183
186
  return self._part.core_properties
184
187
 
188
+ def copy(self) -> Document:
189
+ """Return an independent in-memory snapshot of this document."""
190
+ return open_document(io.BytesIO(self.to_bytes()))
191
+
185
192
  @property
186
193
  def custom_properties(self) -> CustomProperties:
187
194
  """A |CustomProperties| object providing custom properties of document."""
@@ -223,6 +230,27 @@ class Document(ElementProxy):
223
230
  """
224
231
  self._part.save(path_or_stream)
225
232
 
233
+ def sanitize(
234
+ self,
235
+ *,
236
+ personal_metadata: bool = False,
237
+ comments: bool = False,
238
+ web_extensions: bool = False,
239
+ formatting_revisions: bool = False,
240
+ ) -> SanitizationReport:
241
+ """Apply the explicitly selected package-level cleanup operations.
242
+
243
+ Every option defaults to |False| because sanitization is destructive. The
244
+ returned report describes all package and XML changes made to this document.
245
+ """
246
+ return sanitize_document(
247
+ self,
248
+ personal_metadata=personal_metadata,
249
+ comments=comments,
250
+ web_extensions=web_extensions,
251
+ formatting_revisions=formatting_revisions,
252
+ )
253
+
226
254
  @property
227
255
  def sections(self) -> Sections:
228
256
  """|Sections| object providing access to each section in this document."""
@@ -249,6 +277,12 @@ class Document(ElementProxy):
249
277
  """
250
278
  return self._body.tables
251
279
 
280
+ def to_bytes(self) -> bytes:
281
+ """Serialize this document as a complete in-memory DOCX package."""
282
+ with io.BytesIO() as stream:
283
+ self.save(stream)
284
+ return stream.getvalue()
285
+
252
286
  @property
253
287
  def _block_width(self) -> Length:
254
288
  """A |Length| object specifying the space between margins in last section."""
docx/image/constants.py CHANGED
@@ -101,6 +101,7 @@ class MIME_TYPE:
101
101
  """Image content types."""
102
102
 
103
103
  BMP = "image/bmp"
104
+ EMF = "image/x-emf"
104
105
  GIF = "image/gif"
105
106
  JPEG = "image/jpeg"
106
107
  PNG = "image/png"
docx/image/emf.py CHANGED
@@ -13,7 +13,7 @@ class Emf(BaseImageHeader):
13
13
  @property
14
14
  def content_type(self):
15
15
  """
16
- MIME content type for this image, unconditionally `image/emf` for
16
+ MIME content type for this image, unconditionally `image/x-emf` for
17
17
  EMF images.
18
18
  """
19
19
  return MIME_TYPE.EMF
@@ -38,7 +38,6 @@ class Emf(BaseImageHeader):
38
38
  y1 = stream_rdr.read_long(20)
39
39
 
40
40
  frame = tuple(map(stream_rdr.read_long, (24, 28, 32, 36)))
41
- size = x1 - x0, y1 - y0
42
41
 
43
42
  xdpi = 2540.0 * (x1 - x0) / (frame[2] - frame[0])
44
43
  ydpi = 2540.0 * (y1 - y0) / (frame[3] - frame[1])
docx/opc/coreprops.py CHANGED
@@ -9,6 +9,7 @@ import datetime as dt
9
9
  from typing import TYPE_CHECKING
10
10
 
11
11
  from docx.oxml.coreprops import CT_CoreProperties
12
+ from docx.oxml.ns import qn
12
13
 
13
14
  if TYPE_CHECKING:
14
15
  from docx.oxml.coreprops import CT_CoreProperties
@@ -21,6 +22,40 @@ class CoreProperties:
21
22
  def __init__(self, element: CT_CoreProperties):
22
23
  self._element = element
23
24
 
25
+ def deduplicate(self, *property_names: str) -> set[str]:
26
+ """Remove duplicate XML elements for the named core properties.
27
+
28
+ The first occurrence is retained so callers can subsequently normalize its
29
+ value through the regular property setter.
30
+ """
31
+ element_tags = {
32
+ "author": qn("dc:creator"),
33
+ "category": qn("cp:category"),
34
+ "comments": qn("dc:description"),
35
+ "content_status": qn("cp:contentStatus"),
36
+ "created": qn("dcterms:created"),
37
+ "identifier": qn("dc:identifier"),
38
+ "keywords": qn("cp:keywords"),
39
+ "language": qn("dc:language"),
40
+ "last_modified_by": qn("cp:lastModifiedBy"),
41
+ "last_printed": qn("cp:lastPrinted"),
42
+ "modified": qn("dcterms:modified"),
43
+ "revision": qn("cp:revision"),
44
+ "subject": qn("dc:subject"),
45
+ "title": qn("dc:title"),
46
+ "version": qn("cp:version"),
47
+ }
48
+ changed: set[str] = set()
49
+ for property_name in property_names:
50
+ try:
51
+ tag = element_tags[property_name]
52
+ except KeyError as exc:
53
+ raise ValueError(f"Unsupported core property: {property_name}") from exc
54
+ for duplicate in self._element.findall(tag)[1:]:
55
+ self._element.remove(duplicate)
56
+ changed.add(property_name)
57
+ return changed
58
+
24
59
  @property
25
60
  def author(self):
26
61
  return self._element.author_text
docx/opc/part.py CHANGED
@@ -144,6 +144,10 @@ class Part:
144
144
  rel = self.rels.get_or_add(reltype, cast(Part, target))
145
145
  return rel.rId
146
146
 
147
+ def replace_blob(self, blob: bytes) -> None:
148
+ """Replace the serialized payload of this non-XML part."""
149
+ self._blob = blob
150
+
147
151
  @property
148
152
  def related_parts(self):
149
153
  """Dictionary mapping related parts by rId, so child objects can resolve
@@ -235,6 +239,10 @@ class XmlPart(Part):
235
239
  """The root XML element of this XML part."""
236
240
  return self._element
237
241
 
242
+ def replace_blob(self, blob: bytes) -> None:
243
+ """Reject raw replacement because this part serializes from ``element``."""
244
+ raise TypeError("cannot replace the blob of an XmlPart; replace its element instead")
245
+
238
246
  @element.setter
239
247
  def element(self, e: BaseOxmlElement):
240
248
  self._element = e
docx/opc/rel.py CHANGED
@@ -28,6 +28,11 @@ class Relationships(Dict[str, "_Relationship"]):
28
28
  self._target_parts_by_rId[rId] = target
29
29
  return rel
30
30
 
31
+ def __delitem__(self, rId: str) -> None:
32
+ """Remove the relationship and its cached target-part reference."""
33
+ super().__delitem__(rId)
34
+ self._target_parts_by_rId.pop(rId, None)
35
+
31
36
  def get_or_add(self, reltype: str, target_part: Part) -> _Relationship:
32
37
  """Return relationship of `reltype` to `target_part`, newly added if not already
33
38
  present in collection."""
docx/parts/customxml.py CHANGED
@@ -59,7 +59,7 @@ class CustomXmlPart(XmlPart):
59
59
  """
60
60
  return self.element.getchildren()
61
61
 
62
- def add_item(self, tag: str, text: str = "", **attrib):
62
+ def add_item(self, tag: str, text: str = "", **attrib: str) -> None:
63
63
  """
64
64
  Add a child element to root.
65
65
  """
docx/parts/document.py CHANGED
@@ -54,6 +54,24 @@ class DocumentPart(StoryPart):
54
54
  rId = self.relate_to(header_part, RT.HEADER)
55
55
  return header_part, rId
56
56
 
57
+ def delete_custom_xml_part(self, part: CustomXmlPart) -> None:
58
+ """Delete a Custom XML part related to this document.
59
+
60
+ Related Custom XML property parts are detached with the part and omitted from
61
+ the package when it is next saved.
62
+ """
63
+ matching_rIds = [
64
+ rId
65
+ for rId, rel in self.rels.items()
66
+ if rel.reltype == RT.CUSTOM_XML and not rel.is_external and rel.target_part is part
67
+ ]
68
+ if not matching_rIds:
69
+ raise ValueError(f"part '{part.partname}' is not related to this document")
70
+
71
+ part.drop_rels(lambda rel: rel.reltype == RT.CUSTOM_XML_PROPS)
72
+ for rId in matching_rIds:
73
+ del self.rels[rId]
74
+
57
75
  @property
58
76
  def comments(self) -> Comments:
59
77
  """|Comments| object providing access to the comments added to this document."""
@@ -0,0 +1,10 @@
1
+ """Package-level DOCX sanitization.
2
+
3
+ Callers use :meth:`docx.document.Document.sanitize`; this package keeps OPC graph
4
+ mutation and XML cleanup details behind that single method.
5
+ """
6
+
7
+ from docx.sanitize.cleaner import sanitize_document
8
+ from docx.sanitize.result import RemovedRelationship, SanitizationReport
9
+
10
+ __all__ = ["RemovedRelationship", "SanitizationReport", "sanitize_document"]
@@ -0,0 +1,221 @@
1
+ """Implementation of the package-level sanitization boundary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Iterator
6
+
7
+ from lxml import etree
8
+
9
+ from docx.opc.oxml import serialize_part_xml
10
+ from docx.opc.part import Part, XmlPart
11
+ from docx.oxml.ns import qn
12
+ from docx.oxml.parser import parse_xml
13
+ from docx.sanitize.result import RemovedRelationship, SanitizationReport
14
+
15
+ if TYPE_CHECKING:
16
+ from lxml.etree import _Element as etree_Element # pyright: ignore[reportPrivateUsage]
17
+
18
+ from docx.document import Document
19
+ from docx.opc.package import OpcPackage
20
+ from docx.oxml.xmlchemy import BaseOxmlElement
21
+
22
+
23
+ _COMMENT_MARKER_TAGS = (
24
+ qn("w:commentRangeStart"),
25
+ qn("w:commentRangeEnd"),
26
+ qn("w:commentReference"),
27
+ )
28
+ _COMMENT_RELATIONSHIP_NAMES = frozenset(
29
+ {
30
+ "commentauthors",
31
+ "comments",
32
+ "commentsextended",
33
+ "commentsextensible",
34
+ "commentsids",
35
+ "people",
36
+ }
37
+ )
38
+ _FORMATTING_REVISION_TAGS = tuple(
39
+ qn(tag)
40
+ for tag in (
41
+ "w:rPrChange",
42
+ "w:pPrChange",
43
+ "w:tblPrChange",
44
+ "w:trPrChange",
45
+ "w:tcPrChange",
46
+ "w:sectPrChange",
47
+ )
48
+ )
49
+ _WEB_EXTENSION_RELATIONSHIP_NAMES = frozenset({"webextension", "webextensiontaskpanes"})
50
+
51
+
52
+ def sanitize_document(
53
+ document: Document,
54
+ *,
55
+ personal_metadata: bool,
56
+ comments: bool,
57
+ web_extensions: bool,
58
+ formatting_revisions: bool,
59
+ ) -> SanitizationReport:
60
+ """Apply selected sanitizers while keeping all graph mechanics internal."""
61
+ package = document.part.package
62
+ assert package is not None
63
+
64
+ if not any((personal_metadata, comments, web_extensions, formatting_revisions)):
65
+ return SanitizationReport()
66
+
67
+ parts_before = _reachable_part_names(package)
68
+ scrubbed_properties: list[str] = []
69
+ removed_relationships: list[RemovedRelationship] = []
70
+ warnings: list[str] = []
71
+ removed_comment_markers = 0
72
+ removed_formatting_revisions = 0
73
+
74
+ if personal_metadata:
75
+ scrubbed_properties.extend(_scrub_personal_metadata(package))
76
+
77
+ if comments or formatting_revisions:
78
+ for part, root in _iter_word_xml(package, warnings):
79
+ part_changed = False
80
+ if comments:
81
+ count = _remove_elements(root, _COMMENT_MARKER_TAGS)
82
+ removed_comment_markers += count
83
+ part_changed = part_changed or count > 0
84
+ if formatting_revisions:
85
+ count = _remove_elements(root, _FORMATTING_REVISION_TAGS)
86
+ removed_formatting_revisions += count
87
+ part_changed = part_changed or count > 0
88
+ if part_changed and not isinstance(part, XmlPart):
89
+ part.replace_blob(serialize_part_xml(root))
90
+
91
+ if comments:
92
+ removed_relationships.extend(_drop_relationships(package, _COMMENT_RELATIONSHIP_NAMES))
93
+ if web_extensions:
94
+ removed_relationships.extend(
95
+ _drop_relationships(package, _WEB_EXTENSION_RELATIONSHIP_NAMES)
96
+ )
97
+
98
+ parts_after = _reachable_part_names(package)
99
+ return SanitizationReport(
100
+ scrubbed_properties=tuple(scrubbed_properties),
101
+ removed_comment_markers=removed_comment_markers,
102
+ removed_formatting_revisions=removed_formatting_revisions,
103
+ removed_parts=tuple(sorted(parts_before - parts_after)),
104
+ removed_relationships=tuple(removed_relationships),
105
+ warnings=tuple(warnings),
106
+ )
107
+
108
+
109
+ def _drop_relationships(
110
+ package: OpcPackage, relationship_names: frozenset[str]
111
+ ) -> list[RemovedRelationship]:
112
+ removed: list[RemovedRelationship] = []
113
+ for source_name, source in _relationship_sources(package):
114
+ for rId, rel in list(source.rels.items()):
115
+ if _relationship_name(rel.reltype) not in relationship_names:
116
+ continue
117
+ target = rel.target_ref if rel.is_external else str(rel.target_part.partname)
118
+ removed.append(RemovedRelationship(source_name, rId, rel.reltype, target))
119
+ del source.rels[rId]
120
+ return removed
121
+
122
+
123
+ def _iter_word_xml(
124
+ package: OpcPackage, warnings: list[str]
125
+ ) -> Iterator[tuple[Part, BaseOxmlElement]]:
126
+ for part in _reachable_parts(package):
127
+ if not str(part.partname).startswith("/word/"):
128
+ continue
129
+ if isinstance(part, XmlPart):
130
+ yield part, part.element
131
+ continue
132
+ if not part.content_type.endswith("+xml") and part.content_type != "application/xml":
133
+ continue
134
+ try:
135
+ yield part, parse_xml(part.blob)
136
+ except etree.XMLSyntaxError as exc:
137
+ warnings.append(f"Could not sanitize XML part {part.partname}: {exc}")
138
+
139
+
140
+ def _reachable_parts(package: OpcPackage) -> list[Part]:
141
+ parts: list[Part] = []
142
+ visited: set[int] = set()
143
+
144
+ def walk(source: OpcPackage | Part) -> None:
145
+ for rel in source.rels.values():
146
+ if rel.is_external:
147
+ continue
148
+ part = rel.target_part
149
+ identity = id(part)
150
+ if identity in visited:
151
+ continue
152
+ visited.add(identity)
153
+ parts.append(part)
154
+ walk(part)
155
+
156
+ walk(package)
157
+ return parts
158
+
159
+
160
+ def _reachable_part_names(package: OpcPackage) -> set[str]:
161
+ return {str(part.partname) for part in _reachable_parts(package)}
162
+
163
+
164
+ def _relationship_name(relationship_type: str) -> str:
165
+ return relationship_type.rstrip("/").rsplit("/", 1)[-1].lower()
166
+
167
+
168
+ def _relationship_sources(package: OpcPackage) -> list[tuple[str, OpcPackage | Part]]:
169
+ return [("/", package), *[(str(part.partname), part) for part in _reachable_parts(package)]]
170
+
171
+
172
+ def _remove_elements(root: BaseOxmlElement, tags: tuple[str, ...]) -> int:
173
+ tag_set = set(tags)
174
+ elements = [
175
+ element
176
+ for tag in tags
177
+ for element in root.iter(tag)
178
+ if not any(ancestor.tag in tag_set for ancestor in element.iterancestors())
179
+ ]
180
+ for element in elements:
181
+ parent = element.getparent()
182
+ if parent is not None:
183
+ comment_run = _empty_comment_reference_run(element)
184
+ parent.remove(element)
185
+ if comment_run is not None:
186
+ comment_run_parent = comment_run.getparent()
187
+ if comment_run_parent is not None:
188
+ comment_run_parent.remove(comment_run)
189
+ return len(elements)
190
+
191
+
192
+ def _scrub_personal_metadata(package: OpcPackage) -> list[str]:
193
+ core_properties = package.core_properties
194
+ changed = core_properties.deduplicate("author", "last_modified_by", "revision")
195
+ if core_properties.author:
196
+ changed.add("author")
197
+ if core_properties.last_modified_by:
198
+ changed.add("last_modified_by")
199
+ if core_properties.revision != 1:
200
+ changed.add("revision")
201
+ core_properties.author = ""
202
+ core_properties.last_modified_by = ""
203
+ core_properties.revision = 1
204
+ return [name for name in ("author", "last_modified_by", "revision") if name in changed]
205
+
206
+
207
+ def _empty_comment_reference_run(element: etree_Element) -> etree_Element | None:
208
+ if element.tag != qn("w:commentReference"):
209
+ return None
210
+ run = element.getparent()
211
+ if run is None or run.tag != qn("w:r"):
212
+ return None
213
+ remaining = [child for child in run if child is not element]
214
+ if not remaining:
215
+ return run
216
+ if len(remaining) != 1 or remaining[0].tag != qn("w:rPr"):
217
+ return None
218
+ run_properties = remaining[0]
219
+ if len(run_properties) != 1 or run_properties[0].tag != qn("w:rStyle"):
220
+ return None
221
+ return run if run_properties[0].get(qn("w:val")) == "CommentReference" else None
@@ -0,0 +1,38 @@
1
+ """Value objects returned by document sanitization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class RemovedRelationship:
10
+ """A package relationship removed during sanitization."""
11
+
12
+ source: str
13
+ relationship_id: str
14
+ relationship_type: str
15
+ target: str
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class SanitizationReport:
20
+ """Immutable summary of changes made by ``Document.sanitize()``."""
21
+
22
+ scrubbed_properties: tuple[str, ...] = ()
23
+ removed_comment_markers: int = 0
24
+ removed_formatting_revisions: int = 0
25
+ removed_parts: tuple[str, ...] = ()
26
+ removed_relationships: tuple[RemovedRelationship, ...] = ()
27
+ warnings: tuple[str, ...] = ()
28
+
29
+ @property
30
+ def changed(self) -> bool:
31
+ """Whether the document package was modified."""
32
+ return bool(
33
+ self.scrubbed_properties
34
+ or self.removed_comment_markers
35
+ or self.removed_formatting_revisions
36
+ or self.removed_parts
37
+ or self.removed_relationships
38
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-docx-oss
3
- Version: 0.1.1
3
+ Version: 0.3.0
4
4
  Summary: Create, read, and update Microsoft Word .docx files.
5
5
  Author: Ethan St Lee
6
6
  License-Expression: MIT
@@ -23,7 +23,7 @@ Classifier: Topic :: Software Development :: Libraries
23
23
  Requires-Python: >=3.12
24
24
  Description-Content-Type: text/markdown
25
25
  License-File: LICENSE
26
- Requires-Dist: lxml>=3.1.0
26
+ Requires-Dist: lxml>=6.1.0
27
27
  Requires-Dist: typing_extensions>=4.9.0
28
28
  Dynamic: license-file
29
29
 
@@ -53,18 +53,20 @@ Use it when an application needs more than basic paragraphs and tables: custom d
53
53
  metadata, application-owned XML, vector images, floating pictures, East Asian fonts,
54
54
  or low-level package and relationship access.
55
55
 
56
- ## Features
57
-
58
- | Capability | Support |
59
- | --- | --- |
60
- | Documents, paragraphs, runs, tables, styles, sections, headers, and footers | ✓ |
61
- | Custom document properties | ✓ |
62
- | Custom XML parts | ✓ |
63
- | SVG and EMF images | ✓ |
64
- | Inline and floating pictures | ✓ |
65
- | Independent ASCII, High ANSI, and East Asian font controls | ✓ |
66
- | Low-level OPC parts and relationships | ✓ |
67
- | File-path and in-memory stream processing | ✓ |
56
+ ## Capability Matrix
57
+
58
+ | Capability | Status | Primary API |
59
+ | --- | --- | --- |
60
+ | Documents, paragraphs, tables, styles, sections, headers, and footers | Stable | `Document()` and document object APIs |
61
+ | Custom document properties | Stable | `document.custom_properties` |
62
+ | Custom XML lifecycle | Stable | `add_custom_xml_part()`, `delete_custom_xml_part()` |
63
+ | SVG and EMF images | Stable | `add_picture()` |
64
+ | Inline and floating pictures | Stable | `add_picture()`, `add_float_picture()` |
65
+ | Latin and East Asian font controls | Stable | `run.font.name`, `run.font.hAnsi`, `run.font.eastAsia` |
66
+ | Low-level OPC parts and relationships | Stable | `document.part.package`, `part.rels` |
67
+ | In-memory read, export, and independent copies | Stable | `Document()`, `to_bytes()`, `copy()` |
68
+ | Document sanitization | Development | `document.sanitize()` |
69
+ | Package inspection and diff | Planned | — |
68
70
 
69
71
  ## Installation
70
72
 
@@ -170,6 +172,8 @@ available through `Document.add_float_picture()` and `Run.add_float_picture()`.
170
172
  - [API reference](docs/index.rst#api-documentation)
171
173
  - [Custom properties](docs/user/custom-properties.rst)
172
174
  - [Custom XML](docs/user/custom-xml.rst)
175
+ - [python-docx compatibility](docs/user/compatibility.rst)
176
+ - [Upstream maintenance](docs/dev/upstream-maintenance.rst)
173
177
  - [Release history](HISTORY.md)
174
178
 
175
179
  ## Quality and Testing
@@ -1,8 +1,8 @@
1
- docx/__init__.py,sha256=lzMxui-aFJFpJ33Mg8ra9T6A4KSd24ZH5sEHgXwgmcU,2102
2
- docx/api.py,sha256=13U56iPSM2tMRDEb03OmSZQkDAPmwGjfxosYkWS2tcc,1350
1
+ docx/__init__.py,sha256=sDaLx2PbYx5dXsjgnP0rhCKyeZZrysYhseDkNNC3Q2Y,2102
2
+ docx/api.py,sha256=jIojXW4IRDEXQqaqvmOZbzBvNeyRXv9ntHByxd-Kb0M,1518
3
3
  docx/blkcntnr.py,sha256=_xe7Uz3RBCz4TPKXpIzRxtGg-1YPb3gAG9YvsSUyoLU,3525
4
4
  docx/comments.py,sha256=MmaP7U9mZOUFsRhaZNIbXatqOOA8-pOC6UlqWAuTHsc,6385
5
- docx/document.py,sha256=MbIMThSh8tEVExx72aKI4SMGQBcsScQdGLzNv2Asa9o,11941
5
+ docx/document.py,sha256=IYTU_lmh_f1SILGGc0XqrDMl814hZ937d8iubfsYFos,13163
6
6
  docx/exceptions.py,sha256=WWiMy3ZtsfKRS31Wv09egLZnpPkX0H66B8JkOPCC3FM,454
7
7
  docx/package.py,sha256=Eob2q7R6jXQ3iPD0cAGHzu2RVYgU2te4zfG3oVN_-lM,4288
8
8
  docx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -25,8 +25,8 @@ docx/enum/table.py,sha256=7PuQ--PGnSaNiE0Bu4VJ9t6LOgM-hNNmKEcl-3S2eUk,3943
25
25
  docx/enum/text.py,sha256=o6bdpJot2iY40UnnBQ1uGfdE8oHFm3dtXUrmt9eMlvE,10276
26
26
  docx/image/__init__.py,sha256=OvnRg5_DTmmrJObUBWLVcPFMRPb-u63e0EKfF_eIQlk,924
27
27
  docx/image/bmp.py,sha256=w_Jhu6hByAckJNyKhI3zHOKsHyibHgxDIjiPI9ev704,1347
28
- docx/image/constants.py,sha256=Y-ME9Z2KDLsjFQDun8XxhXrL6HNXhkUZqGHuCHXEMf4,3466
29
- docx/image/emf.py,sha256=BnPe6n9f4H9GZvB3Rb8zY-aaHr61WFLcQkryAqx4V4c,1210
28
+ docx/image/constants.py,sha256=c6Mtsy38b0nilCcRp9vvXkV7hyLDb0sDHfJoPqH-lPk,3490
29
+ docx/image/emf.py,sha256=C22sBpDu6vxLqAQmycPCb6xBc8ezVpr1C8x-C8SL2GE,1180
30
30
  docx/image/exceptions.py,sha256=_6rkjXXcbFxb_H-J0kK4Q1J9RaLx3TVQ5hoOVENvQaI,383
31
31
  docx/image/gif.py,sha256=0R7L0r7kjf844pdpN6UKEISsEQzOPAyA3cghXXdJW2s,1118
32
32
  docx/image/helpers.py,sha256=jy0fQBqIfstp9ExfKw3dGNKozn74RnmSA_YwmolsDqM,3089
@@ -37,17 +37,17 @@ docx/image/svg.py,sha256=qDtoQerJ7lJscevXM65BlWqmtUKVB05jTnQajjwLpFw,3576
37
37
  docx/image/tiff.py,sha256=LZx-P2uMSzgdzHfIWRqRqtnbhuIqWBY_cLkx9_HTrPs,10379
38
38
  docx/opc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
39
  docx/opc/constants.py,sha256=u7JE0q95EFiAp3jXOntjv1cV0GdGpKtLfx85D2mlch0,17124
40
- docx/opc/coreprops.py,sha256=IfQeqC5o4k9Obj_kQ4ZKfFl6IL7eIm7dcIC1AZkVvac,3468
40
+ docx/opc/coreprops.py,sha256=DUE7xnk0syV2EtwHtJimX5V5efDccsqoB2KW_mmuHEA,4937
41
41
  docx/opc/customprops.py,sha256=oCHxWPNWSekBQrtO07WPSb0WKLQgTgfS3y8m8ZUXN-A,1986
42
42
  docx/opc/exceptions.py,sha256=pqa1GuFxMlGZDBgGBqntZ_8aDINLwo2gtPuNRwADzyM,264
43
43
  docx/opc/oxml.py,sha256=IE3H4F-DWr8QSZi0pxc767apU3Nk-BhFTs7HiTD5ck0,8325
44
44
  docx/opc/package.py,sha256=zWteqHOyPbR4mpcoOZy-sPVWvQR2e2fQVoYdjaHXXfI,10217
45
45
  docx/opc/packuri.py,sha256=1-uRAwjIftl475nozvssEoS-0EBz0w-oJvT-fNYcx9o,3729
46
- docx/opc/part.py,sha256=15TjdwOs137LDdr-EVNeLb4iQdRyIfDZg6Vjx6_8Zdc,9606
46
+ docx/opc/part.py,sha256=WZsVGi25sZhRobXEFRBQCVX6EXan2pE1GPe_zH5nhxk,9977
47
47
  docx/opc/phys_pkg.py,sha256=-eNZDBbLShHo3W1iglf1CwN_nkK8qnmTWp6ABg77cjA,4005
48
48
  docx/opc/pkgreader.py,sha256=iUPZ7Ba4kNXjPB-q1mQEO44eVmbnP08po4WFH-HD2IE,9588
49
49
  docx/opc/pkgwriter.py,sha256=shDeuBtW2BbFOTHrXx8R97PWFaH_fE37JYBfbw6QWRs,4524
50
- docx/opc/rel.py,sha256=9BAZ59Cr5-BKs_5R8aifPDnPpXw-75GdfYYY6_a8Ays,6219
50
+ docx/opc/rel.py,sha256=ziqjJ_zQZ1Ow1sNM46se8F6YZRVST-Z8Z4NYvnnOt8o,6423
51
51
  docx/opc/shared.py,sha256=Zn2avg2g0J2fkG5YgFfCbl138gXex8stR-QwVJcuSIY,996
52
52
  docx/opc/spec.py,sha256=GP8YEb7bSmZQ0i338M3lQCVZm6yUnkUhm3PFKVgW8bQ,638
53
53
  docx/opc/parts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -81,14 +81,17 @@ docx/oxml/text/parfmt.py,sha256=5TYCf-QfcwlECXUkrLSYFfewFA1JieKGTtm3X01fu64,1222
81
81
  docx/oxml/text/run.py,sha256=W8vwOomvk1oGzAq2BqDgyVftKsSi5LK9_vlADVcMgpg,11238
82
82
  docx/parts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
83
  docx/parts/comments.py,sha256=Dmxh_i8ADRo-wLfO9LV0Lyho5XC_76oU1NJN8itw8lc,1760
84
- docx/parts/customxml.py,sha256=7GNGmzJf6-jBZ7ltciXp_3nfGDJGKI7y5jDCarbjVuI,1907
85
- docx/parts/document.py,sha256=lQSAco_MB0Y5hv7zDQGypR9JcYNi6HcF5Qsfn5c3ahg,7450
84
+ docx/parts/customxml.py,sha256=hbwDmPwR-1RRLOOFhmiPd0B-dNwpfZFrOxIaGU4UlG4,1920
85
+ docx/parts/document.py,sha256=hox7yRnsZRTF1C4ersAQUHNP58YhHxHMJnnztyp-2wM,8171
86
86
  docx/parts/hdrftr.py,sha256=_U0PCjVkK6EWwkyZb9oTunlNb6x55rECD1xFDzwUlPM,1709
87
87
  docx/parts/image.py,sha256=c-DF5YFvp1qVTx7Zon-xOXw0qPc_3LoeX9qMSFPs6mo,2739
88
88
  docx/parts/numbering.py,sha256=rrVIiRoBQIy6j8eWGhWyhcJvNx6RRaItWVDhorwrVyo,1715
89
89
  docx/parts/settings.py,sha256=lqcDzDNLEc8ElxmNMKwy6uNgOesfYcvBDhKz7uUMnb4,1717
90
90
  docx/parts/story.py,sha256=WgsvzLkfZ7DigCE9MVSGPfajzennxdPvOtIdoSxmeuk,4577
91
91
  docx/parts/styles.py,sha256=Ly7aJyPgtwQ2TRIwuUtHz_x1Z0FH9JTtsNohml1-ykM,1391
92
+ docx/sanitize/__init__.py,sha256=woy_ORRETalWdjmOsDAV7F4ON0jmOWBWm5b8KHhp_yw,387
93
+ docx/sanitize/cleaner.py,sha256=BwAKuJ_B36J6TLSnePMzzvgB129T3QitO6lHS5LXsB0,7602
94
+ docx/sanitize/result.py,sha256=JL5IrKnFpG4WQrXcMqNNxFmBHib7J0IXp4D5p2_iI5c,1055
92
95
  docx/styles/__init__.py,sha256=aCJqhH4JhG_cIm3iwVJGtWpWW0AXJx3QMag7MORmrdI,1406
93
96
  docx/styles/latent.py,sha256=EDBcgd-1jKmW-wGz64GvcGSopnOuPQG1inU0pnHRsQA,7211
94
97
  docx/styles/style.py,sha256=BoGXQRSyODmPI38bAEWPgTXEPwhXyVYZZ9nSYxRSHPM,8051
@@ -125,8 +128,8 @@ docx/text/paragraph.py,sha256=N3F62_JRQZYaumuoaJXKCDnRSw2DIiOwzyMPV2Jd7GI,6828
125
128
  docx/text/parfmt.py,sha256=70M75HFo7ydpqmIdZjkV711muyrsBGdr4WZscF3YVJY,10339
126
129
  docx/text/run.py,sha256=tsW8dXOE4VnqEM7cJz_uU2Vq3qgdnrMGzyDq9x5jvE4,10800
127
130
  docx/text/tabstops.py,sha256=bJN98GWW22f4Sqg0dQbyUcnuOn65fHsNZuCGswI03_c,3896
128
- python_docx_oss-0.1.1.dist-info/licenses/LICENSE,sha256=lf2yZv-riPwFeah3IMDeG0zqI6fXcV9T9YOToeG54NM,1164
129
- python_docx_oss-0.1.1.dist-info/METADATA,sha256=vsUjUJmj42pvIO-LKhVnsuAxjpmU415JVPKJzSRfR4Q,7319
130
- python_docx_oss-0.1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
131
- python_docx_oss-0.1.1.dist-info/top_level.txt,sha256=zkbrD3gmyqHpD_XhHe7lCbr2M5vT229nJ4UFqF5VjIs,5
132
- python_docx_oss-0.1.1.dist-info/RECORD,,
131
+ python_docx_oss-0.3.0.dist-info/licenses/LICENSE,sha256=lf2yZv-riPwFeah3IMDeG0zqI6fXcV9T9YOToeG54NM,1164
132
+ python_docx_oss-0.3.0.dist-info/METADATA,sha256=hp-fShMO4V9dZLm86V-xoMsQgyphHVcgtLr8-tS7aa0,7900
133
+ python_docx_oss-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
134
+ python_docx_oss-0.3.0.dist-info/top_level.txt,sha256=zkbrD3gmyqHpD_XhHe7lCbr2M5vT229nJ4UFqF5VjIs,5
135
+ python_docx_oss-0.3.0.dist-info/RECORD,,