docx_plus.comments.threads¶
Threaded comments — the model Word has used since 2013 and python-docx does not expose. A thread is a root comment plus its replies, carrying a resolved flag that drives the review pane's Resolve button.
The thread graph lives in a second OOXML part,
/word/commentsExtended.xml, whose <w15:commentEx> entries key off the
w14:paraId of each comment body's last paragraph — not off the
comment's w:id. add_comment stamps that paraId and writes an
unresolved entry for every comment it inserts, so any comment is
immediately reply-able and resolvable.
Resolution is thread-wide: naming any comment in a thread resolves or reopens the whole thread, matching Word's UI.
A document with no extended part — anything written by python-docx, or by Word before 2013 — reads as one unresolved single-comment thread per comment. Replying to or resolving such a comment materializes the missing metadata in place.
docx_plus.comments.threads ¶
Threaded comments — replies, resolve / reopen, and thread reads.
Word has modelled comments as threads since 2013: a root comment plus
ordered replies, with a resolved flag driving the review pane's
"Resolve" button. python-docx exposes none of that, and neither did
docx_plus before v0.4 — :mod:docx_plus.comments.anchor writes a
flat list of comments.
This module adds the missing three operations:
- :func:
reply_to_comment— attach a reply to an existing comment - :func:
resolve_comment/ :func:reopen_comment— toggle a thread's resolved state - :func:
read_threads— read comments back as nested threads
The thread graph itself lives in commentsExtended.xml; see
:mod:docx_plus.comments._extended for that plumbing. This module
imports only from docx_plus.core and its docx_plus.comments
siblings (SPEC §9.1).
CommentThread
dataclass
¶
A root comment and every reply beneath it.
Attributes:
| Name | Type | Description |
|---|---|---|
root |
AnchoredComment
|
The thread's root comment. |
replies |
tuple[AnchoredComment, ...]
|
Every reply in the thread, breadth-first from the root.
Word only nests one level deep, but |
resolved |
bool
|
Whether the thread is marked resolved ( |
reply_to_comment ¶
reply_to_comment(
doc: Document,
parent_id: int,
text: str,
*,
author: str = "",
initials: str | None = None,
id_registry: CommentIdRegistry | None = None,
para_id_registry: ParaIdRegistry | None = None,
durable_id_registry: DurableIdRegistry | None = None,
) -> CommentRef
Add a reply beneath an existing comment.
Writes a new comment body, links it to parent_id in
commentsExtended.xml, and mirrors the parent's body-side anchors
so the reply spans the same text range — which is how Word renders a
thread as one balloon rather than two.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc
|
Document
|
The python-docx :class: |
required |
parent_id
|
int
|
|
required |
text
|
str
|
Reply body text. Whitespace is preserved
( |
required |
author
|
str
|
Author shown in the review pane. |
''
|
initials
|
str | None
|
Author initials. |
None
|
id_registry
|
CommentIdRegistry | None
|
Pre-existing comment-id registry to share across an editing session. |
None
|
para_id_registry
|
ParaIdRegistry | None
|
Pre-existing |
None
|
durable_id_registry
|
DurableIdRegistry | None
|
Pre-existing |
None
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
CommentRef
|
class: |
Raises:
| Type | Description |
|---|---|
CommentNotFoundError
|
If no comment with |
Note
If the parent is orphaned — present in comments.xml but
with no body-side range markers, the state
:func:~docx_plus.comments.read_comments reports with
paragraph_index=-1 — there is no range for the reply to
mirror, so the reply is written orphaned too. It is a valid
thread member but, like its parent, invisible in Word until the
anchors are repaired.
Example
from docx import Document from docx_plus.comments import add_comment, reply_to_comment doc = Document() p = doc.add_paragraph("Hello world") root = add_comment(p, "Is this right?", author="Reviewer") reply = reply_to_comment(doc, root.comment_id, "Yes.", author="Author")
Source code in docx_plus/comments/threads.py
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | |
resolve_comment ¶
Mark the thread containing comment_id as resolved.
Resolution is a property of the thread, not of one comment — Word's
Resolve button greys out the root and every reply together — so this
sets w15:done="1" across the whole thread no matter which member
you name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc
|
Document
|
The python-docx :class: |
required |
comment_id
|
int
|
Any comment in the thread to resolve. |
required |
Raises:
| Type | Description |
|---|---|
CommentNotFoundError
|
If no comment with |
Source code in docx_plus/comments/threads.py
reopen_comment ¶
Mark the thread containing comment_id as unresolved.
The exact inverse of :func:resolve_comment, with the same
thread-wide semantics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc
|
Document
|
The python-docx :class: |
required |
comment_id
|
int
|
Any comment in the thread to reopen. |
required |
Raises:
| Type | Description |
|---|---|
CommentNotFoundError
|
If no comment with |
Source code in docx_plus/comments/threads.py
read_threads ¶
Return every comment in doc grouped into threads.
A document with no commentsExtended.xml — anything written by
python-docx, or by Word before 2013 — yields one single-comment
thread per comment, all unresolved. That is the correct reading:
without the extended part there is no threading information to
report.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
doc
|
Document
|
The python-docx :class: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
One |
list[CommentThread]
|
class: |
list[CommentThread]
|
order. Returns |