Skip to content

Commit ae0e82d

Browse files
committed
comments: add DocumentPart._comments_part
Also involves adding `CommentsPart.default`. Because the comments part is optional, we need a mechanism to add a default (empty) comments part when one is not present. This is what `CommentsPart.default()` is for.
1 parent 8f184cc commit ae0e82d

File tree

6 files changed

+111
-1
lines changed

6 files changed

+111
-1
lines changed

src/docx/oxml/comments.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Custom element classes related to document comments."""
2+
3+
from __future__ import annotations
4+
5+
from docx.oxml.xmlchemy import BaseOxmlElement
6+
7+
8+
class CT_Comments(BaseOxmlElement):
9+
"""`w:comments` element, the root element for the comments part.
10+
11+
Simply contains a collection of `w:comment` elements, each representing a single comment. Each
12+
contained comment is identified by a unique `w:id` attribute, used to reference the comment
13+
from the document text. The offset of the comment in this collection is arbitrary; it is
14+
essentially a _set_ implemented as a list.
15+
"""

src/docx/parts/comments.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,17 @@
22

33
from __future__ import annotations
44

5+
import os
6+
from typing import cast
7+
8+
from typing_extensions import Self
9+
510
from docx.comments import Comments
11+
from docx.opc.constants import CONTENT_TYPE as CT
12+
from docx.opc.packuri import PackURI
13+
from docx.oxml.comments import CT_Comments
14+
from docx.oxml.parser import parse_xml
15+
from docx.package import Package
616
from docx.parts.story import StoryPart
717

818

@@ -13,3 +23,19 @@ class CommentsPart(StoryPart):
1323
def comments(self) -> Comments:
1424
"""A |Comments| proxy object for the `w:comments` root element of this part."""
1525
raise NotImplementedError
26+
27+
@classmethod
28+
def default(cls, package: Package) -> Self:
29+
"""A newly created comments part, containing a default empty `w:comments` element."""
30+
partname = PackURI("/word/comments.xml")
31+
content_type = CT.WML_COMMENTS
32+
element = cast("CT_Comments", parse_xml(cls._default_comments_xml()))
33+
return cls(partname, content_type, element, package)
34+
35+
@classmethod
36+
def _default_comments_xml(cls) -> bytes:
37+
"""A byte-string containing XML for a default comments part."""
38+
path = os.path.join(os.path.split(__file__)[0], "..", "templates", "default-comments.xml")
39+
with open(path, "rb") as f:
40+
xml_bytes = f.read()
41+
return xml_bytes

src/docx/parts/document.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,13 @@ def _comments_part(self) -> CommentsPart:
131131
132132
Creates a default comments part if one is not present.
133133
"""
134-
raise NotImplementedError
134+
try:
135+
return cast(CommentsPart, self.part_related_by(RT.COMMENTS))
136+
except KeyError:
137+
assert self.package is not None
138+
comments_part = CommentsPart.default(self.package)
139+
self.relate_to(comments_part, RT.COMMENTS)
140+
return comments_part
135141

136142
@property
137143
def _settings_part(self) -> SettingsPart:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2+
<w:comments
3+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
4+
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
5+
mc:Ignorable="w14 w15 w16se w16cid wp14"/>

tests/parts/test_comments.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Unit test suite for the docx.parts.hdrftr module."""
2+
3+
from __future__ import annotations
4+
5+
from docx.opc.constants import CONTENT_TYPE as CT
6+
from docx.package import Package
7+
from docx.parts.comments import CommentsPart
8+
9+
10+
class DescribeCommentsPart:
11+
"""Unit test suite for `docx.parts.comments.CommentsPart` objects."""
12+
13+
def it_constructs_a_default_comments_part_to_help(self):
14+
package = Package()
15+
16+
comments_part = CommentsPart.default(package)
17+
18+
assert isinstance(comments_part, CommentsPart)
19+
assert comments_part.partname == "/word/comments.xml"
20+
assert comments_part.content_type == CT.WML_COMMENTS
21+
assert comments_part.package is package
22+
assert comments_part.element.tag == (
23+
"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}comments"
24+
)
25+
assert len(comments_part.element) == 0

tests/parts/test_document.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,39 @@ def it_can_get_the_id_of_a_style(
227227
styles_.get_style_id.assert_called_once_with(style_, WD_STYLE_TYPE.CHARACTER)
228228
assert style_id == "BodyCharacter"
229229

230+
def it_provides_access_to_its_comments_part_to_help(
231+
self, package_: Mock, part_related_by_: Mock, comments_part_: Mock
232+
):
233+
part_related_by_.return_value = comments_part_
234+
document_part = DocumentPart(
235+
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
236+
)
237+
238+
comments_part = document_part._comments_part
239+
240+
part_related_by_.assert_called_once_with(document_part, RT.COMMENTS)
241+
assert comments_part is comments_part_
242+
243+
def and_it_creates_a_default_comments_part_if_not_present(
244+
self,
245+
package_: Mock,
246+
part_related_by_: Mock,
247+
CommentsPart_: Mock,
248+
comments_part_: Mock,
249+
relate_to_: Mock,
250+
):
251+
part_related_by_.side_effect = KeyError
252+
CommentsPart_.default.return_value = comments_part_
253+
document_part = DocumentPart(
254+
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
255+
)
256+
257+
comments_part = document_part._comments_part
258+
259+
CommentsPart_.default.assert_called_once_with(package_)
260+
relate_to_.assert_called_once_with(document_part, comments_part_, RT.COMMENTS)
261+
assert comments_part is comments_part_
262+
230263
def it_provides_access_to_its_settings_part_to_help(
231264
self, part_related_by_: Mock, settings_part_: Mock, package_: Mock
232265
):

0 commit comments

Comments
 (0)