Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

python-docx

Package Overview
Dependencies
Maintainers
1
Versions
38
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

python-docx - pypi Package Compare versions

Comparing version
1.1.2
to
1.2.0
docs/_static/img/comment-parts.png

Sorry, the diff of this file is not supported yet

+27
.. _comments_api:
Comment-related objects
=======================
.. currentmodule:: docx.comments
|Comments| objects
------------------
.. autoclass:: Comments()
:members:
:inherited-members:
:exclude-members:
part
|Comment| objects
------------------
.. autoclass:: Comment()
:members:
:inherited-members:
:exclude-members:
part
Comments
========
Word allows *comments* to be added to a document. This is an aspect of the *reviewing*
feature-set and is typically used by a second party to provide feedback to the author
without changing the document itself.
The procedure is simple:
- You select some range of text with the mouse or Shift+Arrow keys
- You press the *New Comment* button (Review toolbar)
- You type or paste in your comment
.. image:: /_static/img/comment-parts.png
**Comment Anatomy.** Each comment has two parts, the *comment-reference* and the
*comment-content*:
The *comment-refererence*, sometimes *comment-anchor*, is the text you selected before
pressing the *New Comment* button. It is a *range* in the document content delimited by
a start marker and an end marker, and containing the *id* of the comment that refers to
it.
The *comment-content* is whatever content you typed or pasted in. The content for each
comment is stored in the separate *comments-part* (part-name ``word/comments.xml``) as a
distinct comment object. Each comment has a unique id, allowing a comment reference to
be associated with its content and vice versa.
**Comment Reference.** The comment-reference is a *range*. A range must both start and
end at an even *run* boundary. Intuitively, a range corresponds to a *selection* of text
in the Word UI, one formed by dragging with the mouse or using the *Shift-Arrow* keys.
In general a range can span "run containers", such as paragraphs, such that the range
begins in one paragraph and ends in a later paragraph. However, a range must enclose
*contiguous* runs, such that a range that contains only two vertically adjacent cells in
a multi-column table is not possible (even though such a selection with the mouse is
possible).
**Comment Content.** Interestingly, although commonly used to contain a single line of
plain text, the comment-content can contain essentially any content that can appear in
the document body. This includes rich text with emphasis, runs with a different typeface
and size, both paragraph and character styles, hyperlinks, images, and tables. Note that
tables do not appear in the comment as displayed in the *comment-sidebar* although they
do apper in the *reviewing-pane*.
**Comment Metadata.** Each comment can be assigned *author*, *initals*, and *date*
metadata. In Word, these fields are assigned automatically based on values in ``Settings
> User`` of the installed Word application. These may be configured automatically in an
enterprise installation, based on the user account, but by default they are empty.
*author* metadata is required, although silently assigned the empty string by Word if
the user name is not configured. *initials* is optional, but always set by Word, to the
empty string if not configured. *date* is also optional, but always set by Word to the
date and time the comment was added (seconds resolution, UTC).
**Additional Features.** Later versions of Word allow a comment to be *resolved*. A
comment in this state will appear grayed-out in the Word UI. Later versions of Word also
allow a comment to be *replied to*, forming a *comment thread*. Neither of these
features is supported by the initial implementation of comments in *python-docx*.
The resolved-status and replies features are implemented as *extensions* and involve two
additional comment-related parts:
- `commentsExtended.xml` - contains completion (resolved) status and parent-id for
threading comment responses; keys to `w15:paraId` of comment paragraph in
`comments.xml`
- `commentsIds.xml` - maps `w16cid:paraId` to `w16cid:durableId`, not sure what that is
exactly.
**Applicability.** Note that comments cannot be added to a header or footer and cannot
be nested inside a comment itself. In general the *python-docx* API will not allow these
operations but if you outsmart it then the resulting comment will either be silently
removed or trigger a repair error when the document is loaded by Word.
Word Behavior
-------------
- A DOCX package does not contain a ``comments.xml`` part by default. It is added to the
package when the first comment is added to the document.
- A newly-created comment contains a single paragraph
- Word starts `w:id` at 0 and increments from there. It appears to use a
`max(comment_ids) + 1` algorithm rather than aggressively filling in id numbering
gaps.
- Word-behavior: looks like Word doesn't allow a "zero-length" comment reference; if you
insert a comment when no text is selected, the word prior to the insertion-point is
selected.
- Word allows a comment to be applied to a range that starts before any character and
ends after any later character. However, the XML range-markers can only be placed
between runs. Word accommodates this be breaking runs as necessary to start and stop
at the desired character positions.
MS API
------
.. highlight:: python
**Document**::
Document.Comments
**Comments**
https://learn.microsoft.com/en-us/office/vba/api/word.comments::
Comments.Add(Range, Text) -> Comment
# -- retrieve comment by array idx, not comment_id key --
Comments.Item(idx: Long) -> Comment
Comments.Count() -> Long
# -- restrict visible comments to those by a particular reviewer
Comments.ShowBy = "Travis McGuillicuddy"
**Comment**
https://learn.microsoft.com/en-us/office/vba/api/word.comment::
# -- delete comment and all replies to it --
Comment.DeleteRecursively() -> void
# -- open OLE object embedded in comment for editing --
Comment.Edit() -> void
# -- get the "parent" comment when this comment is a reply --
Comment.Ancestor() -> Comment | Nothing
# -- author of this comment, with email and name fields --
Comment.Contact -> CoAuthor
Comment.Date -> Date
Comment.Done -> bool
Comment.IsInk -> bool
# -- content of the comment, contrast with `Reference` below --
Comment.Range -> Range
# -- content within document this comment refers to --
Comment.Reference -> Range
Comment.Replies -> Comments
# -- described in API docs like the same thing as `Reference` --
Comment.Scope -> Range
Candidate Protocol
------------------
.. highlight:: python
The critical required reference for adding a comment is the *range* referred to by the
comment; i.e. the "selection" of text that is being commented on. Because this range
must start and end at an even run boundary, it is enough to specify the first and last
run in the range, where a single run can be both the start and end run::
>>> paragraph = document.add_paragraph("Hello, world!")
>>> document.add_comment(
... runs=paragraph.runs,
... text="I have this to say about that"
... author="Steve Canny",
... initials="SC",
... )
<docx.comments.Comment object at 0x02468ACE>
A single run can be provided when that is more convenient::
>>> paragraph = document.add_paragraph("Summary: ")
>>> run = paragraph.add_run("{{place-summary-here}}
>>> document.add_comment(
... run, text="The AI model will replace this placeholder with a summary"
... )
<docx.comments.Comment object at 0x02468ACE>
Note that `author` and `initials` are optional parameters; both default to the empty
string.
`text` is also an optional parameter and also defaults to the empty string. Omitting a
`text` argument (or passing `text=""`) produces a comment containing a single paragraph
you can immediately add runs to and add additional paragraphs after:
>>> paragraph = document.add_paragraph("Summary: ")
>>> run = paragraph.add_run("{{place-summary-here}}")
>>> comment = document.add_comment(run)
>>> paragraph = comment.paragraphs[0]
>>> paragraph.add_run("The ")
>>> paragraph.add_run("AI model").bold = True
>>> paragraph.add_run(" will replace this placeholder with a ")
>>> paragraph.add_run("summary").bold = True
<docx.comments.Comment object at 0x02468ACE>
A method directly on |Run| may also be convenient, since you will always have the first
run of the range in hand when adding a comment but may not have ready access to the
``document`` object::
>>> runs = find_sequence_of_one_or_more_runs_to_comment_on()
>>> runs[0].add_comment(
... last_run=runs[-1],
... text="The AI model will replace this placeholder with a summary",
... )
<docx.comments.Comment object at 0x02468ACE>
However, in this situation we would need to qualify the runs as being inside the
document part and not in a header or footer or comment, and perhaps other invalid
comment locations. I believe comments can be applied to footnotes and endnotes though.
Specimen XML
------------
.. highlight:: xml
``comments.xml`` (namespace declarations may vary)::
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:comments
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:p="... others omitted for brevity ...">
>
<w:comment w:id="0" w:author="Steve Canny" w:initials="SJC" w:date="2025-06-10T22:27:56Z">
<w:p>
<w:r>
<w:rPr>
<w:rStyle w:val="CommentReference"/>
</w:rPr>
<w:annotationRef/>
</w:r>
<w:r>
<w:t>I have this to say about that</w:t>
</w:r>
</w:p>
</w:comment>
</w:comments>
Comment reference in document body::
<w:p>
<w:commentRangeStart w:id="0"/>
<w:r>
<w:t>Hello, world!</w:t>
</w:r>
<w:commentRangeEnd w:id="0"/>
<w:r>
<w:rPr>
<w:rStyle w:val="CommentReference"/>
</w:rPr>
<w:commentReference w:id="0"/>
</w:r>
</w:p>
**Notes**
- `w:comment` is a *block-item* container, and can contain any content that can appear
in a document body or table cell, including both paragraphs and tables (and whatever
can go inside those, like images, hyperlinks, etc.
- Word places the `w:annotationRef`-containing run as the first run in the first
paragraph of the comment. I haven't been able to detect any behavior change caused by
leaving this out or placing it elsewhere in the comment content.
- Relationships referenced from within `w:comment` content are relationships *from the
comments part* to the image part, hyperlink, etc.
- `w:commentRangeStart` and `w:commentRangeEnd` elements are *optional*. The
authoritative position of the comment is the required `w:commentReference` element.
This means the *ending* location of a comment anchor can be efficiently found using
XPath.
Schema Excerpt
--------------
**Notes:**
- `commentRangeStart` and `commentRangeEnd` are both type `CT_MarkupRange` and both
belong to `EG_RunLevelElts` (peers of `w:r`) which gives them their positioning in the
document structure.
- These two markers can occur at the *block* level, at the *run* level, or at the *table
row* or *cell* level. However Word only seems to use them as peers of `w:r`. These can
occur as a sibling to:
- a *paragraph* (`w:p`)
- a *table* (`w:tbl`)
- a *run* (`w:r`)
- a *table row* (`w:tr`)
- a *table cell* (`w:tc`)
.. code-block:: xml
<!-- marker types that appear in `document.xml` to mark the referenced range -->
<xsd:element name="commentRangeStart" type="CT_MarkupRange"/>
<xsd:element name="commentRangeEnd" type="CT_MarkupRange"/>
<xsd:element name="commentReference" type="CT_Markup"/>
<xsd:complexType name="CT_MarkupRange">
<xsd:attribute name="id" type="ST_DecimalNumber" use="required"/>
<xsd:attribute name="displacedByCustomXml" type="ST_DisplacedByCustomXml" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="ST_DisplacedByCustomXml">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="next"/>
<xsd:enumeration value="prev"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Markup">
<xsd:attribute name="id" type="ST_DecimalNumber" use="required"/>
</xsd:complexType>
<!-- CT_Comment (individual comment in comments.xml) consolidated -->
<xsd:complexType name="CT_Comment"> <!-- denormalized -->
<xsd:attribute name="id" type="ST_DecimalNumber" use="required"/>
<xsd:attribute name="author" type="s:ST_String" use="required"/>
<xsd:attribute name="date" type="ST_DateTime" use="optional"/>
<xsd:attribute name="initials" type="s:ST_String" use="optional"/>
<xsd:sequence>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="customXml" type="CT_CustomXmlBlock"/>
<xsd:element name="sdt" type="CT_SdtBlock"/>
<xsd:element name="p" type="CT_P" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="tbl" type="CT_Tbl" minOccurs="0" maxOccurs="unbounded"/>
<xsd:group ref="EG_RunLevelElts" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="altChunk" type="CT_AltChunk" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_RunLevelElts">
<xsd:choice>
<xsd:element name="proofErr" minOccurs="0" type="CT_ProofErr"/>
<xsd:element name="permStart" minOccurs="0" type="CT_PermStart"/>
<xsd:element name="permEnd" minOccurs="0" type="CT_Perm"/>
<xsd:group ref="EG_RangeMarkupElements" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="ins" type="CT_RunTrackChange" minOccurs="0"/>
<xsd:element name="del" type="CT_RunTrackChange" minOccurs="0"/>
<xsd:element name="moveFrom" type="CT_RunTrackChange"/>
<xsd:element name="moveTo" type="CT_RunTrackChange"/>
<xsd:group ref="EG_MathContent" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
</xsd:group>
<!-- referenced types -->
<xsd:complexType name="CT_Comment">
<xsd:complexContent>
<xsd:extension base="CT_TrackChange">
<xsd:sequence>
<xsd:group ref="EG_BlockLevelElts" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="initials" type="s:ST_String" use="optional"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="CT_TrackChange">
<xsd:complexContent>
<xsd:extension base="CT_Markup">
<xsd:attribute name="author" type="s:ST_String" use="required"/>
<xsd:attribute name="date" type="ST_DateTime" use="optional"/>
</xsd:extension>
</xsd:complexContent>
</xsd:complexType>
<xsd:complexType name="CT_Markup">
<xsd:attribute name="id" type="ST_DecimalNumber" use="required"/>
</xsd:complexType>
<xsd:group name="EG_BlockLevelElts">
<xsd:choice>
<xsd:group ref="EG_BlockLevelChunkElts" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="altChunk" type="CT_AltChunk" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
</xsd:group>
<xsd:group name="EG_BlockLevelChunkElts">
<xsd:choice>
<xsd:group ref="EG_ContentBlockContent" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
</xsd:group>
<xsd:group name="EG_ContentBlockContent">
<xsd:choice>
<xsd:element name="customXml" type="CT_CustomXmlBlock"/>
<xsd:element name="sdt" type="CT_SdtBlock"/>
<xsd:element name="p" type="CT_P" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="tbl" type="CT_Tbl" minOccurs="0" maxOccurs="unbounded"/>
<xsd:group ref="EG_RunLevelElts" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
</xsd:group>
<xsd:group name="EG_RunLevelElts">
<xsd:choice>
<xsd:element name="proofErr" minOccurs="0" type="CT_ProofErr"/>
<xsd:element name="permStart" minOccurs="0" type="CT_PermStart"/>
<xsd:element name="permEnd" minOccurs="0" type="CT_Perm"/>
<xsd:group ref="EG_RangeMarkupElements" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="ins" type="CT_RunTrackChange" minOccurs="0"/>
<xsd:element name="del" type="CT_RunTrackChange" minOccurs="0"/>
<xsd:element name="moveFrom" type="CT_RunTrackChange"/>
<xsd:element name="moveTo" type="CT_RunTrackChange"/>
<xsd:group ref="EG_MathContent" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
</xsd:group>
.. _comments:
Working with Comments
=====================
Word allows *comments* to be added to a document. This is an aspect of the *reviewing*
feature-set and is typically used by a second party to provide feedback to the author
without changing the document itself.
The procedure is simple:
- You select some range of text with the mouse or Shift+Arrow keys
- You press the *New Comment* button (Review toolbar)
- You type or paste in your comment
.. image:: /_static/img/comment-parts.png
A comment can only be added to the main document. A comment cannot be added in a header,
a footer, or within a comment. A comment _can_ be added to a footnote or endnote, but
those are not yet supported by *python-docx*.
**Comment Anatomy.** Each comment has two parts, the *comment-reference* and the
*comment-content*:
The **comment-refererence**, sometimes *comment-anchor*, is the text in the main
document you selected before pressing the *New Comment* button. It is a so-called
*range* in the main document that starts at the first selected character and ends after
the last one.
The **comment-content**, sometimes just *comment*, is whatever content you typed or
pasted in. The content for each comment is stored in a separate comment object, and
these comment objects are stored in a separate *comments-part* (part-name
``word/comments.xml``), not in the main document. Each comment is assigned a unique id
when it is created, allowing the comment reference to be associated with its content and
vice versa.
**Comment Reference.** The comment-reference is a *range*. A range must both start and
end at an even *run* boundary. Intuitively, a range corresponds to a *selection* of text
in the Word UI, one formed by dragging with the mouse or using the *Shift-Arrow* keys.
In the XML, this range is delimited by a start marker `<w:commentRangeStart/>` and an
end marker `<w:commentRangeEnd/>`, both of which contain the *id* of the comment they
delimit. The start marker appears before the run starting with the first character of
the range and the end marker appears immediately after the run ending with the last
character of the range. Adding a comment that references an arbitrary range of text in
an existing document may require splitting runs on the desired character boundaries.
In general a range can span paragraphs, such that the range begins in one paragraph and
ends in a later paragraph. However, a range must enclose *contiguous* runs, such that a
range that contains only two vertically adjacent cells in a multi-column table is not
possible (even though Word allows such a selection with the mouse).
**Comment Content.** Interestingly, although commonly used to contain a single line of
plain text, the comment-content can contain essentially any content that can appear in
the document body. This includes rich text with emphasis, runs with a different typeface
and size, both paragraph and character styles, hyperlinks, images, and tables. Note that
tables do not appear in the comment as displayed in the *comment-sidebar* although they
do apper in the *reviewing-pane*.
**Comment Metadata.** Each comment can be assigned *author*, *initals*, and *date*
metadata. In Word, these fields are assigned automatically based on values in ``Settings
> User`` of the installed Word application. These might be configured automatically in
an enterprise installation, based on the user account, but by default they are empty.
*author* metadata is required, although silently assigned the empty string by Word if
the user name is not configured. *initials* is optional, but always set by Word, to the
empty string if not configured. *date* is also optional, but always set by Word to the
UTC date and time the comment was added, with seconds resolution (no milliseconds or
microseconds).
**Additional Features.** Later versions of Word allow a comment to be *resolved*. A
comment in this state will appear grayed-out in the Word UI. Later versions of Word also
allow a comment to be *replied to*, forming a *comment thread*. Neither of these
features is supported by the initial implementation of comments in *python-docx*.
**Applicability.** Note that comments cannot be added to a header or footer and cannot
be nested inside a comment itself. In general the *python-docx* API will not allow these
operations but if you outsmart it then the resulting comment will either be silently
removed or trigger a repair error when the document is loaded by Word.
Adding a Comment
----------------
A simple example is adding a comment to a paragraph::
>>> from docx import Document
>>> document = Document()
>>> paragraph = document.add_paragraph("Hello, world!")
>>> comment = document.add_comment(
... runs=paragraph.runs,
... text="I have this to say about that"
... author="Steve Canny",
... initials="SC",
... )
>>> comment
<docx.comments.Comment object at 0x02468ACE>
>>> comment.id
0
>>> comment.author
'Steve Canny'
>>> comment.initials
'SC'
>>> comment.date
datetime.datetime(2025, 6, 11, 20, 42, 30, 0, tzinfo=datetime.timezone.utc)
>>> comment.text
'I have this to say about that'
The API documentation for :meth:`.Document.add_comment` provides further details.
Accessing and using the Comments collection
-------------------------------------------
The comments collection is accessed via the :attr:`.Document.comments` property::
>>> comments = document.comments
>>> comments
<docx.parts.comments.Comments object at 0x02468ACE>
>>> len(comments)
1
The comments collection supports random access to a comment by its id::
>>> comment = comments.get(0)
>>> comment
<docx.comments.Comment object at 0x02468ACE>
Adding rich content to a comment
--------------------------------
A comment is a _block-item container_, just like the document body or a table cell, so
it can contain any content that can appear in those places. It does not contain
page-layout sections and cannot contain a comment reference, but it can contain multiple
paragraphs and/or tables, and runs within paragraphs can have emphasis such as bold or
italic, and have images or hyperlinks.
A comment created with `text=""` will contain a single paragraph with a single empty run
containing the so-called *annotation reference* but no text. It's probably best to leave
this run as it is but you can freely add additional runs to the paragraph that contain
whatever content you like.
The methods for adding this content are the same as those used for the document and
table cells::
>>> paragraph = document.add_paragraph("The rain in Spain.")
>>> comment = document.add_comment(
... runs=paragraph.runs,
... text="",
... )
>>> cmt_para = comment.paragraphs[0]
>>> cmt_para.add_run("Please finish this thought. I believe it should be ")
>>> cmt_para.add_run("falls mainly in the plain.").bold = True
Updating comment metadata
-------------------------
The author and initials metadata can be updated as desired::
>>> comment.author = "John Smith"
>>> comment.initials = "JS"
>>> comment.author
'John Smith'
>>> comment.initials
'JS'

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

"""Step implementations for document comments-related features."""
import datetime as dt
from behave import given, then, when
from behave.runner import Context
from docx import Document
from docx.comments import Comment, Comments
from docx.drawing import Drawing
from helpers import test_docx
# given ====================================================
@given("a Comment object")
def given_a_comment_object(context: Context):
context.comment = Document(test_docx("comments-rich-para")).comments.get(0)
@given("a Comment object containing an embedded image")
def given_a_comment_object_containing_an_embedded_image(context: Context):
context.comment = Document(test_docx("comments-rich-para")).comments.get(1)
@given("a Comments object with {count} comments")
def given_a_comments_object_with_count_comments(context: Context, count: str):
testfile_name = {"0": "doc-default", "4": "comments-rich-para"}[count]
context.comments = Document(test_docx(testfile_name)).comments
@given("a default Comment object")
def given_a_default_comment_object(context: Context):
context.comment = Document(test_docx("comments-rich-para")).comments.add_comment()
@given("a document having a comments part")
def given_a_document_having_a_comments_part(context: Context):
context.document = Document(test_docx("comments-rich-para"))
@given("a document having no comments part")
def given_a_document_having_no_comments_part(context: Context):
context.document = Document(test_docx("doc-default"))
# when =====================================================
@when('I assign "{author}" to comment.author')
def when_I_assign_author_to_comment_author(context: Context, author: str):
context.comment.author = author
@when("I assign comment = comments.add_comment()")
def when_I_assign_comment_eq_add_comment(context: Context):
context.comment = context.comments.add_comment()
@when('I assign comment = comments.add_comment(author="John Doe", initials="JD")')
def when_I_assign_comment_eq_comments_add_comment_with_author_and_initials(context: Context):
context.comment = context.comments.add_comment(author="John Doe", initials="JD")
@when('I assign comment = document.add_comment(runs, "A comment", "John Doe", "JD")')
def when_I_assign_comment_eq_document_add_comment(context: Context):
runs = list(context.document.paragraphs[0].runs)
context.comment = context.document.add_comment(
runs=runs,
text="A comment",
author="John Doe",
initials="JD",
)
@when('I assign "{initials}" to comment.initials')
def when_I_assign_initials(context: Context, initials: str):
context.comment.initials = initials
@when("I assign para_text = comment.paragraphs[0].text")
def when_I_assign_para_text(context: Context):
context.para_text = context.comment.paragraphs[0].text
@when("I assign paragraph = comment.add_paragraph()")
def when_I_assign_default_add_paragraph(context: Context):
context.paragraph = context.comment.add_paragraph()
@when("I assign paragraph = comment.add_paragraph(text, style)")
def when_I_assign_add_paragraph_with_text_and_style(context: Context):
context.para_text = text = "Comment text"
context.para_style = style = "Normal"
context.paragraph = context.comment.add_paragraph(text, style)
@when("I assign run = paragraph.add_run()")
def when_I_assign_paragraph_add_run(context: Context):
context.run = context.paragraph.add_run()
@when("I call comments.get(2)")
def when_I_call_comments_get_2(context: Context):
context.comment = context.comments.get(2)
# then =====================================================
@then("comment is a Comment object")
def then_comment_is_a_Comment_object(context: Context):
assert type(context.comment) is Comment
@then('comment.author == "{author}"')
def then_comment_author_eq_author(context: Context, author: str):
actual = context.comment.author
assert actual == author, f"expected author '{author}', got '{actual}'"
@then("comment.author is the author of the comment")
def then_comment_author_is_the_author_of_the_comment(context: Context):
actual = context.comment.author
assert actual == "Steve Canny", f"expected author 'Steve Canny', got '{actual}'"
@then("comment.comment_id == 0")
def then_comment_id_is_0(context: Context):
assert context.comment.comment_id == 0
@then("comment.comment_id is the comment identifier")
def then_comment_comment_id_is_the_comment_identifier(context: Context):
assert context.comment.comment_id == 0
@then("comment.initials is the initials of the comment author")
def then_comment_initials_is_the_initials_of_the_comment_author(context: Context):
initials = context.comment.initials
assert initials == "SJC", f"expected initials 'SJC', got '{initials}'"
@then('comment.initials == "{initials}"')
def then_comment_initials_eq_initials(context: Context, initials: str):
actual = context.comment.initials
assert actual == initials, f"expected initials '{initials}', got '{actual}'"
@then("comment.paragraphs[{idx}] == paragraph")
def then_comment_paragraphs_idx_eq_paragraph(context: Context, idx: str):
actual = context.comment.paragraphs[int(idx)]._p
expected = context.paragraph._p
assert actual == expected, "paragraphs do not compare equal"
@then('comment.paragraphs[{idx}].style.name == "{style}"')
def then_comment_paragraphs_idx_style_name_eq_style(context: Context, idx: str, style: str):
actual = context.comment.paragraphs[int(idx)]._p.style
expected = style
assert actual == expected, f"expected style name '{expected}', got '{actual}'"
@then('comment.text == "{text}"')
def then_comment_text_eq_text(context: Context, text: str):
actual = context.comment.text
expected = text
assert actual == expected, f"expected text '{expected}', got '{actual}'"
@then("comment.timestamp is the date and time the comment was authored")
def then_comment_timestamp_is_the_date_and_time_the_comment_was_authored(context: Context):
assert context.comment.timestamp == dt.datetime(2025, 6, 7, 11, 20, 0, tzinfo=dt.timezone.utc)
@then("comments.get({id}) == comment")
def then_comments_get_comment_id_eq_comment(context: Context, id: str):
comment_id = int(id)
comment = context.comments.get(comment_id)
assert type(comment) is Comment, f"expected a Comment object, got {type(comment)}"
assert comment.comment_id == comment_id, (
f"expected comment_id '{comment_id}', got '{comment.comment_id}'"
)
@then("document.comments is a Comments object")
def then_document_comments_is_a_Comments_object(context: Context):
document = context.document
assert type(document.comments) is Comments
@then("I can extract the image from the comment")
def then_I_can_extract_the_image_from_the_comment(context: Context):
paragraph = context.comment.paragraphs[0]
run = paragraph.runs[2]
drawing = next(d for d in run.iter_inner_content() if isinstance(d, Drawing))
assert drawing.has_picture
image = drawing.image
assert image.content_type == "image/jpeg", f"got {image.content_type}"
assert image.filename == "image.jpg", f"got {image.filename}"
assert image.sha1 == "1be010ea47803b00e140b852765cdf84f491da47", f"got {image.sha1}"
@then("iterating comments yields {count} Comment objects")
def then_iterating_comments_yields_count_comments(context: Context, count: str):
comment_iter = iter(context.comments)
comment = next(comment_iter)
assert type(comment) is Comment, f"expected a Comment object, got {type(comment)}"
remaining = list(comment_iter)
assert len(remaining) == int(count) - 1, "iterating comments did not yield the expected count"
@then("len(comment.paragraphs) == {count}")
def then_len_comment_paragraphs_eq_count(context: Context, count: str):
actual = len(context.comment.paragraphs)
expected = int(count)
assert actual == expected, f"expected len(comment.paragraphs) of {expected}, got {actual}"
@then("len(comments) == {count}")
def then_len_comments_eq_count(context: Context, count: str):
actual = len(context.comments)
expected = int(count)
assert actual == expected, f"expected len(comments) of {expected}, got {actual}"
@then("para_text is the text of the first paragraph in the comment")
def then_para_text_is_the_text_of_the_first_paragraph_in_the_comment(context: Context):
actual = context.para_text
expected = "Text with hyperlink https://google.com embedded."
assert actual == expected, f"expected para_text '{expected}', got '{actual}'"
@then("paragraph.style == style")
def then_paragraph_style_eq_known_style(context: Context):
actual = context.paragraph.style.name
expected = context.para_style
assert actual == expected, f"expected paragraph.style '{expected}', got '{actual}'"
@then('paragraph.style == "{style}"')
def then_paragraph_style_eq_style(context: Context, style: str):
actual = context.paragraph._p.style
expected = style
assert actual == expected, f"expected paragraph.style '{expected}', got '{actual}'"
@then("paragraph.text == text")
def then_paragraph_text_eq_known_text(context: Context):
actual = context.paragraph.text
expected = context.para_text
assert actual == expected, f"expected paragraph.text '{expected}', got '{actual}'"
@then('paragraph.text == ""')
def then_paragraph_text_eq_text(context: Context):
actual = context.paragraph.text
expected = ""
assert actual == expected, f"expected paragraph.text '{expected}', got '{actual}'"
@then("run.iter_inner_content() yields a single Picture drawing")
def then_run_iter_inner_content_yields_a_single_picture_drawing(context: Context):
inner_content = list(context.run.iter_inner_content())
assert len(inner_content) == 1, (
f"expected a single inner content element, got {len(inner_content)}"
)
inner_content_item = inner_content[0]
assert isinstance(inner_content_item, Drawing)
assert inner_content_item.has_picture
@then("the result is a Comment object with id 2")
def then_the_result_is_a_comment_object_with_id_2(context: Context):
comment = context.comment
assert type(comment) is Comment, f"expected a Comment object, got {type(comment)}"
assert comment.comment_id == 2, f"expected comment_id `2`, got '{comment.comment_id}'"

Sorry, the diff of this file is not supported yet

"""Collection providing access to comments added to this document."""
from __future__ import annotations
import datetime as dt
from typing import TYPE_CHECKING, Iterator
from docx.blkcntnr import BlockItemContainer
if TYPE_CHECKING:
from docx.oxml.comments import CT_Comment, CT_Comments
from docx.parts.comments import CommentsPart
from docx.styles.style import ParagraphStyle
from docx.text.paragraph import Paragraph
class Comments:
"""Collection containing the comments added to this document."""
def __init__(self, comments_elm: CT_Comments, comments_part: CommentsPart):
self._comments_elm = comments_elm
self._comments_part = comments_part
def __iter__(self) -> Iterator[Comment]:
"""Iterator over the comments in this collection."""
return (
Comment(comment_elm, self._comments_part)
for comment_elm in self._comments_elm.comment_lst
)
def __len__(self) -> int:
"""The number of comments in this collection."""
return len(self._comments_elm.comment_lst)
def add_comment(self, text: str = "", author: str = "", initials: str | None = "") -> Comment:
"""Add a new comment to the document and return it.
The comment is added to the end of the comments collection and is assigned a unique
comment-id.
If `text` is provided, it is added to the comment. This option provides for the common
case where a comment contains a modest passage of plain text. Multiple paragraphs can be
added using the `text` argument by separating their text with newlines (`"\\\\n"`).
Between newlines, text is interpreted as it is in `Document.add_paragraph(text=...)`.
The default is to place a single empty paragraph in the comment, which is the same
behavior as the Word UI when you add a comment. New runs can be added to the first
paragraph in the empty comment with `comments.paragraphs[0].add_run()` to adding more
complex text with emphasis or images. Additional paragraphs can be added using
`.add_paragraph()`.
`author` is a required attribute, set to the empty string by default.
`initials` is an optional attribute, set to the empty string by default. Passing |None|
for the `initials` parameter causes that attribute to be omitted from the XML.
"""
comment_elm = self._comments_elm.add_comment()
comment_elm.author = author
comment_elm.initials = initials
comment_elm.date = dt.datetime.now(dt.timezone.utc)
comment = Comment(comment_elm, self._comments_part)
if text == "":
return comment
para_text_iter = iter(text.split("\n"))
first_para_text = next(para_text_iter)
first_para = comment.paragraphs[0]
first_para.add_run(first_para_text)
for s in para_text_iter:
comment.add_paragraph(text=s)
return comment
def get(self, comment_id: int) -> Comment | None:
"""Return the comment identified by `comment_id`, or |None| if not found."""
comment_elm = self._comments_elm.get_comment_by_id(comment_id)
return Comment(comment_elm, self._comments_part) if comment_elm is not None else None
class Comment(BlockItemContainer):
"""Proxy for a single comment in the document.
Provides methods to access comment metadata such as author, initials, and date.
A comment is also a block-item container, similar to a table cell, so it can contain both
paragraphs and tables and its paragraphs can contain rich text, hyperlinks and images,
although the common case is that a comment contains a single paragraph of plain text like a
sentence or phrase.
Note that certain content like tables may not be displayed in the Word comment sidebar due to
space limitations. Such "over-sized" content can still be viewed in the review pane.
"""
def __init__(self, comment_elm: CT_Comment, comments_part: CommentsPart):
super().__init__(comment_elm, comments_part)
self._comment_elm = comment_elm
def add_paragraph(self, text: str = "", style: str | ParagraphStyle | None = None) -> Paragraph:
"""Return paragraph newly added to the end of the content in this container.
The paragraph has `text` in a single run if present, and is given paragraph style `style`.
When `style` is |None| or ommitted, the "CommentText" paragraph style is applied, which is
the default style for comments.
"""
paragraph = super().add_paragraph(text, style)
# -- have to assign style directly to element because `paragraph.style` raises when
# -- a style is not present in the styles part
if style is None:
paragraph._p.style = "CommentText" # pyright: ignore[reportPrivateUsage]
return paragraph
@property
def author(self) -> str:
"""Read/write. The recorded author of this comment.
This field is required but can be set to the empty string.
"""
return self._comment_elm.author
@author.setter
def author(self, value: str):
self._comment_elm.author = value
@property
def comment_id(self) -> int:
"""The unique identifier of this comment."""
return self._comment_elm.id
@property
def initials(self) -> str | None:
"""Read/write. The recorded initials of the comment author.
This attribute is optional in the XML, returns |None| if not set. Assigning |None| removes
any existing initials from the XML.
"""
return self._comment_elm.initials
@initials.setter
def initials(self, value: str | None):
self._comment_elm.initials = value
@property
def text(self) -> str:
"""The text content of this comment as a string.
Only content in paragraphs is included and of course all emphasis and styling is stripped.
Paragraph boundaries are indicated with a newline (`"\\\\n"`)
"""
return "\n".join(p.text for p in self.paragraphs)
@property
def timestamp(self) -> dt.datetime | None:
"""The date and time this comment was authored.
This attribute is optional in the XML, returns |None| if not set.
"""
return self._comment_elm.date
"""Custom element classes related to document comments."""
from __future__ import annotations
import datetime as dt
from typing import TYPE_CHECKING, Callable, cast
from docx.oxml.ns import nsdecls
from docx.oxml.parser import parse_xml
from docx.oxml.simpletypes import ST_DateTime, ST_DecimalNumber, ST_String
from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute, ZeroOrMore
if TYPE_CHECKING:
from docx.oxml.table import CT_Tbl
from docx.oxml.text.paragraph import CT_P
class CT_Comments(BaseOxmlElement):
"""`w:comments` element, the root element for the comments part.
Simply contains a collection of `w:comment` elements, each representing a single comment. Each
contained comment is identified by a unique `w:id` attribute, used to reference the comment
from the document text. The offset of the comment in this collection is arbitrary; it is
essentially a _set_ implemented as a list.
"""
# -- type-declarations to fill in the gaps for metaclass-added methods --
comment_lst: list[CT_Comment]
comment = ZeroOrMore("w:comment")
def add_comment(self) -> CT_Comment:
"""Return newly added `w:comment` child of this `w:comments`.
The returned `w:comment` element is the minimum valid value, having a `w:id` value unique
within the existing comments and the required `w:author` attribute present but set to the
empty string. It's content is limited to a single run containing the necessary annotation
reference but no text. Content is added by adding runs to this first paragraph and by
adding additional paragraphs as needed.
"""
next_id = self._next_available_comment_id()
comment = cast(
CT_Comment,
parse_xml(
f'<w:comment {nsdecls("w")} w:id="{next_id}" w:author="">'
f" <w:p>"
f" <w:pPr>"
f' <w:pStyle w:val="CommentText"/>'
f" </w:pPr>"
f" <w:r>"
f" <w:rPr>"
f' <w:rStyle w:val="CommentReference"/>'
f" </w:rPr>"
f" <w:annotationRef/>"
f" </w:r>"
f" </w:p>"
f"</w:comment>"
),
)
self.append(comment)
return comment
def get_comment_by_id(self, comment_id: int) -> CT_Comment | None:
"""Return the `w:comment` element identified by `comment_id`, or |None| if not found."""
comment_elms = self.xpath(f"(./w:comment[@w:id='{comment_id}'])[1]")
return comment_elms[0] if comment_elms else None
def _next_available_comment_id(self) -> int:
"""The next available comment id.
According to the schema, this can be any positive integer, as big as you like, and the
default mechanism is to use `max() + 1`. However, if that yields a value larger than will
fit in a 32-bit signed integer, we take a more deliberate approach to use the first
ununsed integer starting from 0.
"""
used_ids = [int(x) for x in self.xpath("./w:comment/@w:id")]
next_id = max(used_ids, default=-1) + 1
if next_id <= 2**31 - 1:
return next_id
# -- fall-back to enumerating all used ids to find the first unused one --
for expected, actual in enumerate(sorted(used_ids)):
if expected != actual:
return expected
return len(used_ids)
class CT_Comment(BaseOxmlElement):
"""`w:comment` element, representing a single comment.
A comment is a so-called "story" and can contain paragraphs and tables much like a table-cell.
While probably most often used for a single sentence or phrase, a comment can contain rich
content, including multiple rich-text paragraphs, hyperlinks, images, and tables.
"""
# -- attributes on `w:comment` --
id: int = RequiredAttribute("w:id", ST_DecimalNumber) # pyright: ignore[reportAssignmentType]
author: str = RequiredAttribute("w:author", ST_String) # pyright: ignore[reportAssignmentType]
initials: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"w:initials", ST_String
)
date: dt.datetime | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"w:date", ST_DateTime
)
# -- children --
p = ZeroOrMore("w:p", successors=())
tbl = ZeroOrMore("w:tbl", successors=())
# -- type-declarations for methods added by metaclass --
add_p: Callable[[], CT_P]
p_lst: list[CT_P]
tbl_lst: list[CT_Tbl]
_insert_tbl: Callable[[CT_Tbl], CT_Tbl]
@property
def inner_content_elements(self) -> list[CT_P | CT_Tbl]:
"""Generate all `w:p` and `w:tbl` elements in this comment."""
return self.xpath("./w:p | ./w:tbl")
"""Contains comments added to the document."""
from __future__ import annotations
import os
from typing import TYPE_CHECKING, cast
from typing_extensions import Self
from docx.comments import Comments
from docx.opc.constants import CONTENT_TYPE as CT
from docx.opc.packuri import PackURI
from docx.oxml.comments import CT_Comments
from docx.oxml.parser import parse_xml
from docx.package import Package
from docx.parts.story import StoryPart
if TYPE_CHECKING:
from docx.oxml.comments import CT_Comments
from docx.package import Package
class CommentsPart(StoryPart):
"""Container part for comments added to the document."""
def __init__(
self, partname: PackURI, content_type: str, element: CT_Comments, package: Package
):
super().__init__(partname, content_type, element, package)
self._comments = element
@property
def comments(self) -> Comments:
"""A |Comments| proxy object for the `w:comments` root element of this part."""
return Comments(self._comments, self)
@classmethod
def default(cls, package: Package) -> Self:
"""A newly created comments part, containing a default empty `w:comments` element."""
partname = PackURI("/word/comments.xml")
content_type = CT.WML_COMMENTS
element = cast("CT_Comments", parse_xml(cls._default_comments_xml()))
return cls(partname, content_type, element, package)
@classmethod
def _default_comments_xml(cls) -> bytes:
"""A byte-string containing XML for a default comments part."""
path = os.path.join(os.path.split(__file__)[0], "..", "templates", "default-comments.xml")
with open(path, "rb") as f:
xml_bytes = f.read()
return xml_bytes
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:comments
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main"
xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas"
/>
# pyright: reportPrivateUsage=false
"""Unit-test suite for `docx.oxml.comments` module."""
from __future__ import annotations
from typing import cast
import pytest
from docx.oxml.comments import CT_Comments
from ..unitutil.cxml import element
class DescribeCT_Comments:
"""Unit-test suite for `docx.oxml.comments.CT_Comments`."""
@pytest.mark.parametrize(
("cxml", "expected_value"),
[
("w:comments", 0),
("w:comments/(w:comment{w:id=1})", 2),
("w:comments/(w:comment{w:id=4},w:comment{w:id=2147483646})", 2147483647),
("w:comments/(w:comment{w:id=1},w:comment{w:id=2147483647})", 0),
("w:comments/(w:comment{w:id=1},w:comment{w:id=2},w:comment{w:id=3})", 4),
],
)
def it_finds_the_next_available_comment_id_to_help(self, cxml: str, expected_value: int):
comments_elm = cast(CT_Comments, element(cxml))
assert comments_elm._next_available_comment_id() == expected_value
"""Unit test suite for the docx.parts.hdrftr module."""
from __future__ import annotations
from typing import cast
import pytest
from docx.comments import Comments
from docx.opc.constants import CONTENT_TYPE as CT
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docx.opc.packuri import PackURI
from docx.opc.part import PartFactory
from docx.oxml.comments import CT_Comments
from docx.package import Package
from docx.parts.comments import CommentsPart
from ..unitutil.cxml import element
from ..unitutil.mock import FixtureRequest, Mock, class_mock, instance_mock, method_mock
class DescribeCommentsPart:
"""Unit test suite for `docx.parts.comments.CommentsPart` objects."""
def it_is_used_by_the_part_loader_to_construct_a_comments_part(
self, package_: Mock, CommentsPart_load_: Mock, comments_part_: Mock
):
partname = PackURI("/word/comments.xml")
content_type = CT.WML_COMMENTS
reltype = RT.COMMENTS
blob = b"<w:comments/>"
CommentsPart_load_.return_value = comments_part_
part = PartFactory(partname, content_type, reltype, blob, package_)
CommentsPart_load_.assert_called_once_with(partname, content_type, blob, package_)
assert part is comments_part_
def it_provides_access_to_its_comments_collection(
self, Comments_: Mock, comments_: Mock, package_: Mock
):
Comments_.return_value = comments_
comments_elm = cast(CT_Comments, element("w:comments"))
comments_part = CommentsPart(
PackURI("/word/comments.xml"), CT.WML_COMMENTS, comments_elm, package_
)
comments = comments_part.comments
Comments_.assert_called_once_with(comments_part.element, comments_part)
assert comments is comments_
def it_constructs_a_default_comments_part_to_help(self):
package = Package()
comments_part = CommentsPart.default(package)
assert isinstance(comments_part, CommentsPart)
assert comments_part.partname == "/word/comments.xml"
assert comments_part.content_type == CT.WML_COMMENTS
assert comments_part.package is package
assert comments_part.element.tag == (
"{http://schemas.openxmlformats.org/wordprocessingml/2006/main}comments"
)
assert len(comments_part.element) == 0
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def Comments_(self, request: FixtureRequest) -> Mock:
return class_mock(request, "docx.parts.comments.Comments")
@pytest.fixture
def comments_(self, request: FixtureRequest) -> Mock:
return instance_mock(request, Comments)
@pytest.fixture
def comments_part_(self, request: FixtureRequest) -> Mock:
return instance_mock(request, CommentsPart)
@pytest.fixture
def CommentsPart_load_(self, request: FixtureRequest) -> Mock:
return method_mock(request, CommentsPart, "load", autospec=False)
@pytest.fixture
def package_(self, request: FixtureRequest) -> Mock:
return instance_mock(request, Package)
# pyright: reportPrivateUsage=false
"""Unit test suite for the `docx.comments` module."""
from __future__ import annotations
import datetime as dt
from typing import cast
import pytest
from docx.comments import Comment, Comments
from docx.opc.constants import CONTENT_TYPE as CT
from docx.opc.packuri import PackURI
from docx.oxml.comments import CT_Comment, CT_Comments
from docx.oxml.ns import qn
from docx.package import Package
from docx.parts.comments import CommentsPart
from .unitutil.cxml import element
from .unitutil.mock import FixtureRequest, Mock, instance_mock
class DescribeComments:
"""Unit-test suite for `docx.comments.Comments` objects."""
@pytest.mark.parametrize(
("cxml", "count"),
[
("w:comments", 0),
("w:comments/w:comment", 1),
("w:comments/(w:comment,w:comment,w:comment)", 3),
],
)
def it_knows_how_many_comments_it_contains(self, cxml: str, count: int, package_: Mock):
comments_elm = cast(CT_Comments, element(cxml))
comments = Comments(
comments_elm,
CommentsPart(
PackURI("/word/comments.xml"),
CT.WML_COMMENTS,
comments_elm,
package_,
),
)
assert len(comments) == count
def it_is_iterable_over_the_comments_it_contains(self, package_: Mock):
comments_elm = cast(CT_Comments, element("w:comments/(w:comment,w:comment)"))
comments = Comments(
comments_elm,
CommentsPart(
PackURI("/word/comments.xml"),
CT.WML_COMMENTS,
comments_elm,
package_,
),
)
comment_iter = iter(comments)
comment1 = next(comment_iter)
assert type(comment1) is Comment, "expected a `Comment` object"
comment2 = next(comment_iter)
assert type(comment2) is Comment, "expected a `Comment` object"
with pytest.raises(StopIteration):
next(comment_iter)
def it_can_get_a_comment_by_id(self, package_: Mock):
comments_elm = cast(
CT_Comments,
element("w:comments/(w:comment{w:id=1},w:comment{w:id=2},w:comment{w:id=3})"),
)
comments = Comments(
comments_elm,
CommentsPart(
PackURI("/word/comments.xml"),
CT.WML_COMMENTS,
comments_elm,
package_,
),
)
comment = comments.get(2)
assert type(comment) is Comment, "expected a `Comment` object"
assert comment._comment_elm is comments_elm.comment_lst[1]
def but_it_returns_None_when_no_comment_with_that_id_exists(self, package_: Mock):
comments_elm = cast(
CT_Comments,
element("w:comments/(w:comment{w:id=1},w:comment{w:id=2},w:comment{w:id=3})"),
)
comments = Comments(
comments_elm,
CommentsPart(
PackURI("/word/comments.xml"),
CT.WML_COMMENTS,
comments_elm,
package_,
),
)
comment = comments.get(4)
assert comment is None, "expected None when no comment with that id exists"
def it_can_add_a_new_comment(self, package_: Mock):
comments_elm = cast(CT_Comments, element("w:comments"))
comments_part = CommentsPart(
PackURI("/word/comments.xml"),
CT.WML_COMMENTS,
comments_elm,
package_,
)
now_before = dt.datetime.now(dt.timezone.utc).replace(microsecond=0)
comments = Comments(comments_elm, comments_part)
comment = comments.add_comment()
now_after = dt.datetime.now(dt.timezone.utc).replace(microsecond=0)
# -- a comment is unconditionally added, and returned for any further adjustment --
assert isinstance(comment, Comment)
# -- it is "linked" to the comments part so it can add images and hyperlinks, etc. --
assert comment.part is comments_part
# -- comment numbering starts at 0, and is incremented for each new comment --
assert comment.comment_id == 0
# -- author is a required attribut, but is the empty string by default --
assert comment.author == ""
# -- initials is an optional attribute, but defaults to the empty string, same as Word --
assert comment.initials == ""
# -- timestamp is also optional, but defaults to now-UTC --
assert comment.timestamp is not None
assert now_before <= comment.timestamp <= now_after
# -- by default, a new comment contains a single empty paragraph --
assert [p.text for p in comment.paragraphs] == [""]
# -- that paragraph has the "CommentText" style, same as Word applies --
comment_elm = comment._comment_elm
assert len(comment_elm.p_lst) == 1
p = comment_elm.p_lst[0]
assert p.style == "CommentText"
# -- and that paragraph contains a single run with the necessary annotation reference --
assert len(p.r_lst) == 1
r = comment_elm.p_lst[0].r_lst[0]
assert r.style == "CommentReference"
assert r[-1].tag == qn("w:annotationRef")
def and_it_can_add_text_to_the_comment_when_adding_it(self, comments: Comments, package_: Mock):
comment = comments.add_comment(text="para 1\n\npara 2")
assert len(comment.paragraphs) == 3
assert [p.text for p in comment.paragraphs] == ["para 1", "", "para 2"]
assert all(p._p.style == "CommentText" for p in comment.paragraphs)
def and_it_sets_the_author_and_their_initials_when_adding_a_comment_when_provided(
self, comments: Comments, package_: Mock
):
comment = comments.add_comment(author="Steve Canny", initials="SJC")
assert comment.author == "Steve Canny"
assert comment.initials == "SJC"
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def comments(self, package_: Mock) -> Comments:
comments_elm = cast(CT_Comments, element("w:comments"))
comments_part = CommentsPart(
PackURI("/word/comments.xml"),
CT.WML_COMMENTS,
comments_elm,
package_,
)
return Comments(comments_elm, comments_part)
@pytest.fixture
def package_(self, request: FixtureRequest):
return instance_mock(request, Package)
class DescribeComment:
"""Unit-test suite for `docx.comments.Comment`."""
def it_knows_its_comment_id(self, comments_part_: Mock):
comment_elm = cast(CT_Comment, element("w:comment{w:id=42}"))
comment = Comment(comment_elm, comments_part_)
assert comment.comment_id == 42
def it_knows_its_author(self, comments_part_: Mock):
comment_elm = cast(CT_Comment, element("w:comment{w:id=42,w:author=Steve Canny}"))
comment = Comment(comment_elm, comments_part_)
assert comment.author == "Steve Canny"
def it_knows_the_initials_of_its_author(self, comments_part_: Mock):
comment_elm = cast(CT_Comment, element("w:comment{w:id=42,w:initials=SJC}"))
comment = Comment(comment_elm, comments_part_)
assert comment.initials == "SJC"
def it_knows_the_date_and_time_it_was_authored(self, comments_part_: Mock):
comment_elm = cast(
CT_Comment,
element("w:comment{w:id=42,w:date=2023-10-01T12:34:56Z}"),
)
comment = Comment(comment_elm, comments_part_)
assert comment.timestamp == dt.datetime(2023, 10, 1, 12, 34, 56, tzinfo=dt.timezone.utc)
@pytest.mark.parametrize(
("cxml", "expected_value"),
[
("w:comment{w:id=42}", ""),
('w:comment{w:id=42}/w:p/w:r/w:t"Comment text."', "Comment text."),
(
'w:comment{w:id=42}/(w:p/w:r/w:t"First para",w:p/w:r/w:t"Second para")',
"First para\nSecond para",
),
(
'w:comment{w:id=42}/(w:p/w:r/w:t"First para",w:p,w:p/w:r/w:t"Second para")',
"First para\n\nSecond para",
),
],
)
def it_can_summarize_its_content_as_text(
self, cxml: str, expected_value: str, comments_part_: Mock
):
assert Comment(cast(CT_Comment, element(cxml)), comments_part_).text == expected_value
def it_provides_access_to_the_paragraphs_it_contains(self, comments_part_: Mock):
comment_elm = cast(
CT_Comment,
element('w:comment{w:id=42}/(w:p/w:r/w:t"First para",w:p/w:r/w:t"Second para")'),
)
comment = Comment(comment_elm, comments_part_)
paragraphs = comment.paragraphs
assert len(paragraphs) == 2
assert [para.text for para in paragraphs] == ["First para", "Second para"]
def it_can_update_the_comment_author(self, comments_part_: Mock):
comment_elm = cast(CT_Comment, element("w:comment{w:id=42,w:author=Old Author}"))
comment = Comment(comment_elm, comments_part_)
comment.author = "New Author"
assert comment.author == "New Author"
@pytest.mark.parametrize(
"initials",
[
# -- valid initials --
"XYZ",
# -- empty string is valid
"",
# -- None is valid, removes existing initials
None,
],
)
def it_can_update_the_comment_initials(self, initials: str | None, comments_part_: Mock):
comment_elm = cast(CT_Comment, element("w:comment{w:id=42,w:initials=ABC}"))
comment = Comment(comment_elm, comments_part_)
comment.initials = initials
assert comment.initials == initials
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def comments_part_(self, request: FixtureRequest):
return instance_mock(request, CommentsPart)
# pyright: reportPrivateUsage=false
"""Unit test suite for the `docx.drawing` module."""
from __future__ import annotations
from typing import cast
import pytest
from docx.drawing import Drawing
from docx.image.image import Image
from docx.oxml.drawing import CT_Drawing
from docx.parts.document import DocumentPart
from docx.parts.image import ImagePart
from .unitutil.cxml import element
from .unitutil.mock import FixtureRequest, Mock, instance_mock
class DescribeDrawing:
"""Unit-test suite for `docx.drawing.Drawing` objects."""
@pytest.mark.parametrize(
("cxml", "expected_value"),
[
("w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic", True),
("w:drawing/wp:anchor/a:graphic/a:graphicData/pic:pic", True),
("w:drawing/wp:inline/a:graphic/a:graphicData/a:grpSp", False),
("w:drawing/wp:anchor/a:graphic/a:graphicData/a:chart", False),
],
)
def it_knows_when_it_contains_a_Picture(
self, cxml: str, expected_value: bool, document_part_: Mock
):
drawing = Drawing(cast(CT_Drawing, element(cxml)), document_part_)
assert drawing.has_picture == expected_value
def it_provides_access_to_the_image_in_a_Picture_drawing(
self, document_part_: Mock, image_part_: Mock, image_: Mock
):
image_part_.image = image_
document_part_.part.related_parts = {"rId1": image_part_}
cxml = (
"w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip{r:embed=rId1}"
)
drawing = Drawing(cast(CT_Drawing, element(cxml)), document_part_)
image = drawing.image
assert image is image_
def but_it_raises_when_the_drawing_does_not_contain_a_Picture(self, document_part_: Mock):
drawing = Drawing(
cast(CT_Drawing, element("w:drawing/wp:inline/a:graphic/a:graphicData/a:grpSp")),
document_part_,
)
with pytest.raises(ValueError, match="drawing does not contain a picture"):
drawing.image
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def document_part_(self, request: FixtureRequest):
return instance_mock(request, DocumentPart)
@pytest.fixture
def image_(self, request: FixtureRequest):
return instance_mock(request, Image)
@pytest.fixture
def image_part_(self, request: FixtureRequest):
return instance_mock(request, ImagePart)
+5
-3

@@ -94,2 +94,6 @@ # -*- coding: utf-8 -*-

.. |Comment| replace:: :class:`.Comment`
.. |Comments| replace:: :class:`.Comments`
.. |CoreProperties| replace:: :class:`.CoreProperties`

@@ -274,5 +278,3 @@

# html_sidebars = {}
html_sidebars = {
"**": ["localtoc.html", "relations.html", "sidebarlinks.html", "searchbox.html"]
}
html_sidebars = {"**": ["localtoc.html", "relations.html", "sidebarlinks.html", "searchbox.html"]}

@@ -279,0 +281,0 @@ # Additional templates that should be rendered to pages, maps page names to

@@ -13,2 +13,3 @@

features/comments
features/header

@@ -15,0 +16,0 @@ features/settings

@@ -84,2 +84,3 @@

user/styles-using
user/comments
user/shapes

@@ -100,2 +101,3 @@

api/section
api/comments
api/shape

@@ -102,0 +104,0 @@ api/dml

@@ -16,5 +16,3 @@ """Step implementations for block content containers."""

def given_a_cell_with_paragraphs_and_tables(context: Context):
context.cell = (
Document(test_docx("blk-paras-and-tables")).tables[1].rows[0].cells[0]
)
context.cell = Document(test_docx("blk-paras-and-tables")).tables[1].rows[0].cells[0]

@@ -21,0 +19,0 @@

@@ -129,5 +129,3 @@ """Step implementations for document-related features."""

document = context.document
context.picture = document.add_picture(
test_file("monty-truth.png"), height=Inches(1.5)
)
context.picture = document.add_picture(test_file("monty-truth.png"), height=Inches(1.5))

@@ -138,5 +136,3 @@

document = context.document
context.picture = document.add_picture(
test_file("monty-truth.png"), width=Inches(1.5)
)
context.picture = document.add_picture(test_file("monty-truth.png"), width=Inches(1.5))

@@ -143,0 +139,0 @@

@@ -30,5 +30,3 @@ """Step implementations for hyperlink-related features."""

@given("a hyperlink having address {address} and fragment {fragment}")
def given_a_hyperlink_having_address_and_fragment(
context: Context, address: str, fragment: str
):
def given_a_hyperlink_having_address_and_fragment(context: Context, address: str, fragment: str):
paragraph_idxs: Dict[Tuple[str, str], int] = {

@@ -77,5 +75,3 @@ ("''", "linkedBookmark"): 1,

expected_value = "http://yahoo.com/"
assert (
actual_value == expected_value
), f"expected: {expected_value}, got: {actual_value}"
assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}"

@@ -87,5 +83,3 @@

expected_value = {"True": True, "False": False}[value]
assert (
actual_value == expected_value
), f"expected: {expected_value}, got: {actual_value}"
assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}"

@@ -97,5 +91,3 @@

expected_value = "linkedBookmark"
assert (
actual_value == expected_value
), f"expected: {expected_value}, got: {actual_value}"
assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}"

@@ -107,5 +99,3 @@

expected_value = ["Run" for _ in context.hyperlink.runs]
assert (
actual_value == expected_value
), f"expected: {expected_value}, got: {actual_value}"
assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}"

@@ -117,5 +107,3 @@

expected_value = int(value)
assert (
actual_value == expected_value
), f"expected: {expected_value}, got: {actual_value}"
assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}"

@@ -127,5 +115,3 @@

expected_value = "awesome hyperlink"
assert (
actual_value == expected_value
), f"expected: {expected_value}, got: {actual_value}"
assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}"

@@ -137,4 +123,2 @@

expected_value = "" if value == "''" else value
assert (
actual_value == expected_value
), f"expected: {expected_value}, got: {actual_value}"
assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}"

@@ -41,29 +41,19 @@ """Step implementations for rendered page-break related features."""

expected_value = "Paragraph"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"
actual_value = para_frag.text
expected_value = "Page break in>><<this hyperlink"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"
actual_value = para_frag.alignment
expected_value = WD_PARAGRAPH_ALIGNMENT.RIGHT # pyright: ignore
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"
actual_value = para_frag.hyperlinks[0].runs[0].style.name
expected_value = "Hyperlink"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"
actual_value = para_frag.hyperlinks[0].address
expected_value = "http://google.com/"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"

@@ -79,23 +69,15 @@

expected_value = "Paragraph"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"
actual_value = para_frag.text
expected_value = "Page break here>>"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"
actual_value = para_frag.alignment
expected_value = WD_PARAGRAPH_ALIGNMENT.CENTER # pyright: ignore
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"
actual_value = para_frag.runs[0].style.name
expected_value = "Default Paragraph Font"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"

@@ -112,5 +94,3 @@

expected_value = "Paragraph"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"

@@ -120,5 +100,3 @@ # -- paragraph text is only the fragment after the page-break --

expected_value = " and another one here>><<with text following"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"

@@ -128,5 +106,3 @@ # -- paragraph properties are preserved --

expected_value = WD_PARAGRAPH_ALIGNMENT.RIGHT # pyright: ignore
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"

@@ -136,5 +112,3 @@ # -- paragraph has no hyperlinks --

expected_value = []
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"

@@ -144,5 +118,3 @@ # -- following paragraph fragment retains any remaining page-breaks --

expected_value = ["RenderedPageBreak"]
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"

@@ -158,22 +130,14 @@

expected_value = "Paragraph"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"
actual_value = para_frag.text
expected_value = "<<followed by more text."
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"
actual_value = para_frag.alignment
expected_value = WD_PARAGRAPH_ALIGNMENT.CENTER # pyright: ignore
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"
actual_value = para_frag.runs[0].style.name
expected_value = "Default Paragraph Font"
assert (
actual_value == expected_value
), f"expected: '{expected_value}', got: '{actual_value}'"
assert actual_value == expected_value, f"expected: '{expected_value}', got: '{actual_value}'"

@@ -123,5 +123,3 @@ """Step implementations for paragraph-related features."""

expected_value = {"True": True, "False": False}[value]
assert (
actual_value == expected_value
), f"expected: {expected_value}, got: {actual_value}"
assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}"

@@ -131,5 +129,3 @@

def then_paragraph_hyperlinks_contains_only_Hyperlink_instances(context: Context):
assert all(
type(item).__name__ == "Hyperlink" for item in context.paragraph.hyperlinks
)
assert all(type(item).__name__ == "Hyperlink" for item in context.paragraph.hyperlinks)

@@ -168,5 +164,3 @@

expected_value = int(value)
assert (
actual_value == expected_value
), f"got: {actual_value}, expected: {expected_value}"
assert actual_value == expected_value, f"got: {actual_value}, expected: {expected_value}"

@@ -222,5 +216,3 @@

@then("the paragraph alignment property value is {align_value}")
def then_the_paragraph_alignment_prop_value_is_value(
context: Context, align_value: str
):
def then_the_paragraph_alignment_prop_value_is_value(context: Context, align_value: str):
expected_value: Any = {

@@ -227,0 +219,0 @@ "None": None,

@@ -163,5 +163,3 @@ """Step implementations for paragraph format-related features."""

def then_paragraph_format_line_spacing_is_value(context, value):
expected_value = (
None if value == "None" else float(value) if "." in value else int(value)
)
expected_value = None if value == "None" else float(value) if "." in value else int(value)
paragraph_format = context.paragraph_format

@@ -168,0 +166,0 @@

@@ -76,5 +76,3 @@ """Step implementations for section-related features."""

@when("I assign {bool_val} to section.different_first_page_header_footer")
def when_I_assign_value_to_section_different_first_page_hdrftr(
context: Context, bool_val: str
):
def when_I_assign_value_to_section_different_first_page_hdrftr(context: Context, bool_val: str):
context.section.different_first_page_header_footer = eval(bool_val)

@@ -162,5 +160,3 @@

expected = eval(bool_val)
assert actual == expected, (
"section.different_first_page_header_footer is %s" % actual
)
assert actual == expected, "section.different_first_page_header_footer is %s" % actual

@@ -218,5 +214,3 @@

@then("section.{propname}.is_linked_to_previous is True")
def then_section_hdrftr_prop_is_linked_to_previous_is_True(
context: Context, propname: str
):
def then_section_hdrftr_prop_is_linked_to_previous_is_True(context: Context, propname: str):
actual = getattr(context.section, propname).is_linked_to_previous

@@ -247,5 +241,3 @@ expected = True

@then("the reported page orientation is {orientation}")
def then_the_reported_page_orientation_is_orientation(
context: Context, orientation: str
):
def then_the_reported_page_orientation_is_orientation(context: Context, orientation: str):
expected_value = {

@@ -252,0 +244,0 @@ "WD_ORIENT.LANDSCAPE": WD_ORIENT.LANDSCAPE,

"""Step implementations for document settings-related features."""
from behave import given, then, when
from behave.runner import Context

@@ -14,3 +15,3 @@ from docx import Document

@given("a document having a settings part")
def given_a_document_having_a_settings_part(context):
def given_a_document_having_a_settings_part(context: Context):
context.document = Document(test_docx("doc-word-default-blank"))

@@ -20,3 +21,3 @@

@given("a document having no settings part")
def given_a_document_having_no_settings_part(context):
def given_a_document_having_no_settings_part(context: Context):
context.document = Document(test_docx("set-no-settings-part"))

@@ -26,6 +27,6 @@

@given("a Settings object {with_or_without} odd and even page headers as settings")
def given_a_Settings_object_with_or_without_odd_and_even_hdrs(context, with_or_without):
testfile_name = {"with": "doc-odd-even-hdrs", "without": "sct-section-props"}[
with_or_without
]
def given_a_Settings_object_with_or_without_odd_and_even_hdrs(
context: Context, with_or_without: str
):
testfile_name = {"with": "doc-odd-even-hdrs", "without": "sct-section-props"}[with_or_without]
context.settings = Document(test_docx(testfile_name)).settings

@@ -38,3 +39,5 @@

@when("I assign {bool_val} to settings.odd_and_even_pages_header_footer")
def when_I_assign_value_to_settings_odd_and_even_pages_header_footer(context, bool_val):
def when_I_assign_value_to_settings_odd_and_even_pages_header_footer(
context: Context, bool_val: str
):
context.settings.odd_and_even_pages_header_footer = eval(bool_val)

@@ -47,3 +50,3 @@

@then("document.settings is a Settings object")
def then_document_settings_is_a_Settings_object(context):
def then_document_settings_is_a_Settings_object(context: Context):
document = context.document

@@ -54,7 +57,5 @@ assert type(document.settings) is Settings

@then("settings.odd_and_even_pages_header_footer is {bool_val}")
def then_settings_odd_and_even_pages_header_footer_is(context, bool_val):
def then_settings_odd_and_even_pages_header_footer_is(context: Context, bool_val: str):
actual = context.settings.odd_and_even_pages_header_footer
expected = eval(bool_val)
assert actual == expected, (
"settings.odd_and_even_pages_header_footer is %s" % actual
)
assert actual == expected, "settings.odd_and_even_pages_header_footer is %s" % actual

@@ -114,5 +114,6 @@ """Step implementations for graphical object (shape) related features."""

expected_sha1 = "79769f1e202add2e963158b532e36c2c0f76a70c"
assert (
image_sha1 == expected_sha1
), "image SHA1 doesn't match, expected %s, got %s" % (expected_sha1, image_sha1)
assert image_sha1 == expected_sha1, "image SHA1 doesn't match, expected %s, got %s" % (
expected_sha1,
image_sha1,
)

@@ -119,0 +120,0 @@

@@ -63,5 +63,3 @@ """Step implementations for text-related features."""

<w:t>stu</w:t>
</w:r>""" % nsdecls(
"w"
)
</w:r>""" % nsdecls("w")
r = parse_xml(r_xml)

@@ -239,5 +237,3 @@ context.run = Run(r, None)

expected_value = ["str", "RenderedPageBreak", "str", "RenderedPageBreak", "str"]
assert (
actual_value == expected_value
), f"expected: {expected_value}, got: {actual_value}"
assert actual_value == expected_value, f"expected: {expected_value}, got: {actual_value}"

@@ -272,4 +268,3 @@

blip_rId = r.xpath(
"./w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/"
"a:blip/@r:embed"
"./w:drawing/wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip/@r:embed"
)[0]

@@ -279,5 +274,6 @@ image_part = run.part.related_parts[blip_rId]

expected_sha1 = "79769f1e202add2e963158b532e36c2c0f76a70c"
assert (
image_sha1 == expected_sha1
), "image SHA1 doesn't match, expected %s, got %s" % (expected_sha1, image_sha1)
assert image_sha1 == expected_sha1, "image SHA1 doesn't match, expected %s, got %s" % (
expected_sha1,
image_sha1,
)

@@ -284,0 +280,0 @@

@@ -6,2 +6,9 @@ .. :changelog:

1.2.0 (2025-06-16)
++++++++++++++++++
- Add support for comments
- Drop support for Python 3.8, add testing for Python 3.13
1.1.2 (2024-05-01)

@@ -14,2 +21,3 @@ ++++++++++++++++++

1.1.1 (2024-04-29)

@@ -16,0 +24,0 @@ ++++++++++++++++++

@@ -1,4 +0,4 @@

Metadata-Version: 2.1
Metadata-Version: 2.4
Name: python-docx
Version: 1.1.2
Version: 1.2.0
Summary: Create, read, and update Microsoft Word .docx files.

@@ -26,3 +26,3 @@ Author-email: Steve Canny <stcanny@gmail.com>

Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.7
Requires-Python: >=3.9
Description-Content-Type: text/markdown

@@ -32,2 +32,3 @@ License-File: LICENSE

Requires-Dist: typing_extensions>=4.9.0
Dynamic: license-file

@@ -34,0 +35,0 @@ # python-docx

@@ -33,4 +33,20 @@ [build-system]

readme = "README.md"
requires-python = ">=3.7"
requires-python = ">=3.9"
[dependency-groups]
dev = [
"Jinja2==2.11.3",
"MarkupSafe==0.23",
"Sphinx==1.8.6",
"alabaster<0.7.14",
"behave>=1.2.6",
"pyparsing>=3.2.3",
"pyright>=1.1.401",
"pytest>=8.4.0",
"ruff>=0.11.13",
"tox>=4.26.0",
"twine>=6.1.0",
"types-lxml-multi-subclass>=2025.3.30",
]
[project.urls]

@@ -42,11 +58,7 @@ Changelog = "https://github.com/python-openxml/python-docx/blob/master/HISTORY.rst"

[tool.black]
line-length = 100
target-version = ["py37", "py38", "py39", "py310", "py311"]
[tool.pyright]
include = ["src/docx", "tests"]
pythonPlatform = "All"
pythonVersion = "3.8"
reportImportCycles = true
pythonVersion = "3.9"
reportImportCycles = false
reportUnnecessaryCast = true

@@ -57,2 +69,4 @@ reportUnnecessaryTypeIgnoreComment = true

verboseOutput = true
venvPath = "."
venv = ".venv"

@@ -94,3 +108,2 @@ [tool.pytest.ini_options]

"PT001", # -- wants @pytest.fixture() instead of @pytest.fixture --
"PT005", # -- wants @pytest.fixture() instead of @pytest.fixture --
]

@@ -118,1 +131,2 @@ select = [

version = {attr = "docx.__version__"}

@@ -16,3 +16,3 @@ """Initialize `docx` package.

__version__ = "1.1.2"
__version__ = "1.2.0"

@@ -29,2 +29,3 @@

from docx.opc.parts.coreprops import CorePropertiesPart
from docx.parts.comments import CommentsPart
from docx.parts.document import DocumentPart

@@ -46,2 +47,3 @@ from docx.parts.hdrftr import FooterPart, HeaderPart

PartFactory.part_type_for[CT.OPC_CORE_PROPERTIES] = CorePropertiesPart
PartFactory.part_type_for[CT.WML_COMMENTS] = CommentsPart
PartFactory.part_type_for[CT.WML_DOCUMENT_MAIN] = DocumentPart

@@ -57,2 +59,3 @@ PartFactory.part_type_for[CT.WML_FOOTER] = FooterPart

CorePropertiesPart,
CommentsPart,
DocumentPart,

@@ -59,0 +62,0 @@ FooterPart,

@@ -22,2 +22,3 @@ # pyright: reportImportCycles=false

import docx.types as t
from docx.oxml.comments import CT_Comment
from docx.oxml.document import CT_Body

@@ -30,3 +31,3 @@ from docx.oxml.section import CT_HdrFtr

BlockItemElement: TypeAlias = "CT_Body | CT_HdrFtr | CT_Tc"
BlockItemElement: TypeAlias = "CT_Body | CT_Comment | CT_HdrFtr | CT_Tc"

@@ -72,3 +73,3 @@

tbl = CT_Tbl.new_tbl(rows, cols, width)
self._element._insert_tbl(tbl) # # pyright: ignore[reportPrivateUsage]
self._element._insert_tbl(tbl) # pyright: ignore[reportPrivateUsage]
return Table(tbl, self)

@@ -75,0 +76,0 @@

"""DrawingML objects related to color, ColorFormat being the most prominent."""
from ..enum.dml import MSO_COLOR_TYPE
from ..oxml.simpletypes import ST_HexColorAuto
from ..shared import ElementProxy
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from typing_extensions import TypeAlias
from docx.enum.dml import MSO_COLOR_TYPE
from docx.oxml.simpletypes import ST_HexColorAuto
from docx.shared import ElementProxy, RGBColor
if TYPE_CHECKING:
from docx.enum.dml import MSO_THEME_COLOR
from docx.oxml.text.font import CT_Color
from docx.oxml.text.run import CT_R
# -- other element types can be a parent of an `w:rPr` element, but for now only `w:r` is --
RPrParent: TypeAlias = "CT_R"
class ColorFormat(ElementProxy):
"""Provides access to color settings such as RGB color, theme color, and luminance
adjustments."""
"""Provides access to color settings like RGB color, theme color, and luminance adjustments."""
def __init__(self, rPr_parent):
def __init__(self, rPr_parent: RPrParent):
super(ColorFormat, self).__init__(rPr_parent)
self._element = rPr_parent
@property
def rgb(self):
def rgb(self) -> RGBColor | None:
"""An |RGBColor| value or |None| if no RGB color is specified.
When :attr:`type` is `MSO_COLOR_TYPE.RGB`, the value of this property will
always be an |RGBColor| value. It may also be an |RGBColor| value if
:attr:`type` is `MSO_COLOR_TYPE.THEME`, as Word writes the current value of a
theme color when one is assigned. In that case, the RGB value should be
interpreted as no more than a good guess however, as the theme color takes
precedence at rendering time. Its value is |None| whenever :attr:`type` is
either |None| or `MSO_COLOR_TYPE.AUTO`.
When :attr:`type` is `MSO_COLOR_TYPE.RGB`, the value of this property will always be an
|RGBColor| value. It may also be an |RGBColor| value if :attr:`type` is
`MSO_COLOR_TYPE.THEME`, as Word writes the current value of a theme color when one is
assigned. In that case, the RGB value should be interpreted as no more than a good guess
however, as the theme color takes precedence at rendering time. Its value is |None|
whenever :attr:`type` is either |None| or `MSO_COLOR_TYPE.AUTO`.
Assigning an |RGBColor| value causes :attr:`type` to become `MSO_COLOR_TYPE.RGB`
and any theme color is removed. Assigning |None| causes any color to be removed
such that the effective color is inherited from the style hierarchy.
Assigning an |RGBColor| value causes :attr:`type` to become `MSO_COLOR_TYPE.RGB` and any
theme color is removed. Assigning |None| causes any color to be removed such that the
effective color is inherited from the style hierarchy.
"""

@@ -36,10 +49,10 @@ color = self._color

return None
return color.val
return cast(RGBColor, color.val)
@rgb.setter
def rgb(self, value):
def rgb(self, value: RGBColor | None):
if value is None and self._color is None:
return
rPr = self._element.get_or_add_rPr()
rPr._remove_color()
rPr._remove_color() # pyright: ignore[reportPrivateUsage]
if value is not None:

@@ -49,16 +62,16 @@ rPr.get_or_add_color().val = value

@property
def theme_color(self):
def theme_color(self) -> MSO_THEME_COLOR | None:
"""Member of :ref:`MsoThemeColorIndex` or |None| if no theme color is specified.
When :attr:`type` is `MSO_COLOR_TYPE.THEME`, the value of this property will
always be a member of :ref:`MsoThemeColorIndex`. When :attr:`type` has any other
value, the value of this property is |None|.
When :attr:`type` is `MSO_COLOR_TYPE.THEME`, the value of this property will always be a
member of :ref:`MsoThemeColorIndex`. When :attr:`type` has any other value, the value of
this property is |None|.
Assigning a member of :ref:`MsoThemeColorIndex` causes :attr:`type` to become
`MSO_COLOR_TYPE.THEME`. Any existing RGB value is retained but ignored by Word.
Assigning |None| causes any color specification to be removed such that the
effective color is inherited from the style hierarchy.
`MSO_COLOR_TYPE.THEME`. Any existing RGB value is retained but ignored by Word. Assigning
|None| causes any color specification to be removed such that the effective color is
inherited from the style hierarchy.
"""
color = self._color
if color is None or color.themeColor is None:
if color is None:
return None

@@ -68,6 +81,6 @@ return color.themeColor

@theme_color.setter
def theme_color(self, value):
def theme_color(self, value: MSO_THEME_COLOR | None):
if value is None:
if self._color is not None:
self._element.rPr._remove_color()
if self._color is not None and self._element.rPr is not None:
self._element.rPr._remove_color() # pyright: ignore[reportPrivateUsage]
return

@@ -77,9 +90,8 @@ self._element.get_or_add_rPr().get_or_add_color().themeColor = value

@property
def type(self) -> MSO_COLOR_TYPE:
def type(self) -> MSO_COLOR_TYPE | None:
"""Read-only.
A member of :ref:`MsoColorType`, one of RGB, THEME, or AUTO, corresponding to
the way this color is defined. Its value is |None| if no color is applied at
this level, which causes the effective color to be inherited from the style
hierarchy.
A member of :ref:`MsoColorType`, one of RGB, THEME, or AUTO, corresponding to the way this
color is defined. Its value is |None| if no color is applied at this level, which causes
the effective color to be inherited from the style hierarchy.
"""

@@ -96,3 +108,3 @@ color = self._color

@property
def _color(self):
def _color(self) -> CT_Color | None:
"""Return `w:rPr/w:color` or |None| if not present.

@@ -99,0 +111,0 @@

@@ -8,3 +8,3 @@ # pyright: reportImportCycles=false

from typing import IO, TYPE_CHECKING, Iterator, List
from typing import IO, TYPE_CHECKING, Iterator, List, Sequence

@@ -15,10 +15,11 @@ from docx.blkcntnr import BlockItemContainer

from docx.section import Section, Sections
from docx.shared import ElementProxy, Emu
from docx.shared import ElementProxy, Emu, Inches, Length
from docx.text.run import Run
if TYPE_CHECKING:
import docx.types as t
from docx.comments import Comment, Comments
from docx.oxml.document import CT_Body, CT_Document
from docx.parts.document import DocumentPart
from docx.settings import Settings
from docx.shared import Length
from docx.styles.style import ParagraphStyle, _TableStyle

@@ -42,2 +43,51 @@ from docx.table import Table

def add_comment(
self,
runs: Run | Sequence[Run],
text: str | None = "",
author: str = "",
initials: str | None = "",
) -> Comment:
"""Add a comment to the document, anchored to the specified runs.
`runs` can be a single `Run` object or a non-empty sequence of `Run` objects. Only the
first and last run of a sequence are used, it's just more convenient to pass a whole
sequence when that's what you have handy, like `paragraph.runs` for example. When `runs`
contains a single `Run` object, that run serves as both the first and last run.
A comment can be anchored only on an even run boundary, meaning the text the comment
"references" must be a non-zero integer number of consecutive runs. The runs need not be
_contiguous_ per se, like the first can be in one paragraph and the last in the next
paragraph, but all runs between the first and the last will be included in the reference.
The comment reference range is delimited by placing a `w:commentRangeStart` element before
the first run and a `w:commentRangeEnd` element after the last run. This is why only the
first and last run are required and why a single run can serve as both first and last.
Word works out which text to highlight in the UI based on these range markers.
`text` allows the contents of a simple comment to be provided in the call, providing for
the common case where a comment is a single phrase or sentence without special formatting
such as bold or italics. More complex comments can be added using the returned `Comment`
object in much the same way as a `Document` or (table) `Cell` object, using methods like
`.add_paragraph()`, .add_run()`, etc.
The `author` and `initials` parameters allow that metadata to be set for the comment.
`author` is a required attribute on a comment and is the empty string by default.
`initials` is optional on a comment and may be omitted by passing |None|, but Word adds an
`initials` attribute by default and we follow that convention by using the empty string
when no `initials` argument is provided.
"""
# -- normalize `runs` to a sequence of runs --
runs = [runs] if isinstance(runs, Run) else runs
first_run = runs[0]
last_run = runs[-1]
# -- Note that comments can only appear in the document part --
comment = self.comments.add_comment(text=text, author=author, initials=initials)
# -- let the first run orchestrate placement of the comment range start and end --
first_run.mark_comment_range(last_run, comment.comment_id)
return comment
def add_heading(self, text: str = "", level: int = 1):

@@ -114,2 +164,7 @@ """Return a heading paragraph newly added to the end of the document.

@property
def comments(self) -> Comments:
"""A |Comments| object providing access to comments added to the document."""
return self._part.comments
@property
def core_properties(self):

@@ -185,3 +240,6 @@ """A |CoreProperties| object providing Dublin Core properties of document."""

section = self.sections[-1]
return Emu(section.page_width - section.left_margin - section.right_margin)
page_width = section.page_width or Inches(8.5)
left_margin = section.left_margin or Inches(1)
right_margin = section.right_margin or Inches(1)
return Emu(page_width - left_margin - right_margin)

@@ -206,3 +264,3 @@ @property

def clear_content(self):
def clear_content(self) -> _Body:
"""Return this |_Body| instance after clearing it of all content.

@@ -209,0 +267,0 @@

@@ -12,2 +12,3 @@ """DrawingML-related objects are in this subpackage."""

import docx.types as t
from docx.image.image import Image

@@ -22,1 +23,39 @@

self._drawing = self._element = drawing
@property
def has_picture(self) -> bool:
"""True when `drawing` contains an embedded picture.
A drawing can contain a picture, but it can also contain a chart, SmartArt, or a
drawing canvas. Methods related to a picture, like `.image`, will raise when the drawing
does not contain a picture. Use this value to determine whether image methods will succeed.
This value is `False` when a linked picture is present. This should be relatively rare and
the image would only be retrievable from the filesystem.
Note this does not distinguish between inline and floating images. The presence of either
one will cause this value to be `True`.
"""
xpath_expr = (
# -- an inline picture --
"./wp:inline/a:graphic/a:graphicData/pic:pic"
# -- a floating picture --
" | ./wp:anchor/a:graphic/a:graphicData/pic:pic"
)
# -- xpath() will return a list, empty if there are no matches --
return bool(self._drawing.xpath(xpath_expr))
@property
def image(self) -> Image:
"""An `Image` proxy object for the image in this (picture) drawing.
Raises `ValueError` when this drawing does contains something other than a picture. Use
`.has_picture` to qualify drawing objects before using this property.
"""
picture_rIds = self._drawing.xpath(".//pic:blipFill/a:blip/@r:embed")
if not picture_rIds:
raise ValueError("drawing does not contain a picture")
rId = picture_rIds[0]
doc_part = self.part
image_part = doc_part.related_parts[rId]
return image_part.image

@@ -40,5 +40,5 @@ """Base classes and other objects used by enumerations."""

xml_value: str
xml_value: str | None
def __new__(cls, ms_api_value: int, xml_value: str, docstr: str):
def __new__(cls, ms_api_value: int, xml_value: str | None, docstr: str):
self = int.__new__(cls, ms_api_value)

@@ -74,3 +74,7 @@ self._value_ = ms_api_value

# -- member by its value using EnumCls(val) works as usual.
return cls(value).xml_value
member = cls(value)
xml_value = member.xml_value
if not xml_value:
raise ValueError(f"{cls.__name__}.{member.name} has no XML representation")
return xml_value

@@ -77,0 +81,0 @@

@@ -15,3 +15,3 @@ """Provides objects that can characterize image streams.

# class, offset, signature_bytes
(Png, 0, b"\x89PNG\x0D\x0A\x1A\x0A"),
(Png, 0, b"\x89PNG\x0d\x0a\x1a\x0a"),
(Jfif, 6, b"JFIF"),

@@ -18,0 +18,0 @@ (Exif, 6, b"Exif"),

@@ -8,54 +8,54 @@ """Constants specific the the image sub-package."""

TEM = b"\x01"
DHT = b"\xC4"
DAC = b"\xCC"
JPG = b"\xC8"
DHT = b"\xc4"
DAC = b"\xcc"
JPG = b"\xc8"
SOF0 = b"\xC0"
SOF1 = b"\xC1"
SOF2 = b"\xC2"
SOF3 = b"\xC3"
SOF5 = b"\xC5"
SOF6 = b"\xC6"
SOF7 = b"\xC7"
SOF9 = b"\xC9"
SOFA = b"\xCA"
SOFB = b"\xCB"
SOFD = b"\xCD"
SOFE = b"\xCE"
SOFF = b"\xCF"
SOF0 = b"\xc0"
SOF1 = b"\xc1"
SOF2 = b"\xc2"
SOF3 = b"\xc3"
SOF5 = b"\xc5"
SOF6 = b"\xc6"
SOF7 = b"\xc7"
SOF9 = b"\xc9"
SOFA = b"\xca"
SOFB = b"\xcb"
SOFD = b"\xcd"
SOFE = b"\xce"
SOFF = b"\xcf"
RST0 = b"\xD0"
RST1 = b"\xD1"
RST2 = b"\xD2"
RST3 = b"\xD3"
RST4 = b"\xD4"
RST5 = b"\xD5"
RST6 = b"\xD6"
RST7 = b"\xD7"
RST0 = b"\xd0"
RST1 = b"\xd1"
RST2 = b"\xd2"
RST3 = b"\xd3"
RST4 = b"\xd4"
RST5 = b"\xd5"
RST6 = b"\xd6"
RST7 = b"\xd7"
SOI = b"\xD8"
EOI = b"\xD9"
SOS = b"\xDA"
DQT = b"\xDB" # Define Quantization Table(s)
DNL = b"\xDC"
DRI = b"\xDD"
DHP = b"\xDE"
EXP = b"\xDF"
SOI = b"\xd8"
EOI = b"\xd9"
SOS = b"\xda"
DQT = b"\xdb" # Define Quantization Table(s)
DNL = b"\xdc"
DRI = b"\xdd"
DHP = b"\xde"
EXP = b"\xdf"
APP0 = b"\xE0"
APP1 = b"\xE1"
APP2 = b"\xE2"
APP3 = b"\xE3"
APP4 = b"\xE4"
APP5 = b"\xE5"
APP6 = b"\xE6"
APP7 = b"\xE7"
APP8 = b"\xE8"
APP9 = b"\xE9"
APPA = b"\xEA"
APPB = b"\xEB"
APPC = b"\xEC"
APPD = b"\xED"
APPE = b"\xEE"
APPF = b"\xEF"
APP0 = b"\xe0"
APP1 = b"\xe1"
APP2 = b"\xe2"
APP3 = b"\xe3"
APP4 = b"\xe4"
APP5 = b"\xe5"
APP6 = b"\xe6"
APP7 = b"\xe7"
APP8 = b"\xe8"
APP9 = b"\xe9"
APPA = b"\xea"
APPB = b"\xeb"
APPC = b"\xec"
APPD = b"\xed"
APPE = b"\xee"
APPF = b"\xef"

@@ -82,14 +82,14 @@ STANDALONE_MARKERS = (TEM, SOI, EOI, RST0, RST1, RST2, RST3, RST4, RST5, RST6, RST7)

b"\x00": "UNKNOWN",
b"\xC0": "SOF0",
b"\xC2": "SOF2",
b"\xC4": "DHT",
b"\xDA": "SOS", # start of scan
b"\xD8": "SOI", # start of image
b"\xD9": "EOI", # end of image
b"\xDB": "DQT",
b"\xE0": "APP0",
b"\xE1": "APP1",
b"\xE2": "APP2",
b"\xED": "APP13",
b"\xEE": "APP14",
b"\xc0": "SOF0",
b"\xc2": "SOF2",
b"\xc4": "DHT",
b"\xda": "SOS", # start of scan
b"\xd8": "SOI", # start of image
b"\xd9": "EOI", # end of image
b"\xdb": "DQT",
b"\xe0": "APP0",
b"\xe1": "APP1",
b"\xe2": "APP2",
b"\xed": "APP13",
b"\xee": "APP14",
}

@@ -96,0 +96,0 @@

@@ -197,3 +197,3 @@ """Provides objects that can characterize image streams.

"""Abstract property definition, must be implemented by all subclasses."""
msg = "content_type property must be implemented by all subclasses of " "BaseImageHeader"
msg = "content_type property must be implemented by all subclasses of BaseImageHeader"
raise NotImplementedError(msg)

@@ -208,3 +208,3 @@

raise NotImplementedError(
"default_ext property must be implemented by all subclasses of " "BaseImageHeader"
"default_ext property must be implemented by all subclasses of BaseImageHeader"
)

@@ -211,0 +211,0 @@

@@ -191,5 +191,5 @@ """Objects related to parsing headers of JPEG image streams.

"""Return an offset, byte 2-tuple for the next byte in `stream` that is not
'\xFF', starting with the byte at offset `start`.
'\xff', starting with the byte at offset `start`.
If the byte at offset `start` is not '\xFF', `start` and the returned `offset`
If the byte at offset `start` is not '\xff', `start` and the returned `offset`
will be the same.

@@ -199,3 +199,3 @@ """

byte_ = self._read_byte()
while byte_ == b"\xFF":
while byte_ == b"\xff":
byte_ = self._read_byte()

@@ -206,3 +206,3 @@ offset_of_non_ff_byte = self._stream.tell() - 1

def _offset_of_next_ff_byte(self, start):
"""Return the offset of the next '\xFF' byte in `stream` starting with the byte
"""Return the offset of the next '\xff' byte in `stream` starting with the byte
at offset `start`.

@@ -215,3 +215,3 @@

byte_ = self._read_byte()
while byte_ != b"\xFF":
while byte_ != b"\xff":
byte_ = self._read_byte()

@@ -270,3 +270,3 @@ offset_of_ff_byte = self._stream.tell() - 1

def marker_code(self):
"""The single-byte code that identifies the type of this marker, e.g. ``'\xE0'``
"""The single-byte code that identifies the type of this marker, e.g. ``'\xe0'``
for start of image (SOI)."""

@@ -292,5 +292,3 @@ return self._marker_code

def __init__(
self, marker_code, offset, length, density_units, x_density, y_density
):
def __init__(self, marker_code, offset, length, density_units, x_density, y_density):
super(_App0Marker, self).__init__(marker_code, offset, length)

@@ -341,5 +339,3 @@ self._density_units = density_units

y_density = stream.read_short(offset, 12)
return cls(
marker_code, offset, segment_length, density_units, x_density, y_density
)
return cls(marker_code, offset, segment_length, density_units, x_density, y_density)

@@ -346,0 +342,0 @@

@@ -12,23 +12,11 @@ """Constant values related to the Open Packaging Convention.

DML_CHART = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
DML_CHARTSHAPES = (
"application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml"
)
DML_DIAGRAM_COLORS = (
"application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml"
)
DML_DIAGRAM_DATA = (
"application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml"
)
DML_DIAGRAM_LAYOUT = (
"application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml"
)
DML_DIAGRAM_STYLE = (
"application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml"
)
DML_CHARTSHAPES = "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml"
DML_DIAGRAM_COLORS = "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml"
DML_DIAGRAM_DATA = "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml"
DML_DIAGRAM_LAYOUT = "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml"
DML_DIAGRAM_STYLE = "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml"
GIF = "image/gif"
JPEG = "image/jpeg"
MS_PHOTO = "image/vnd.ms-photo"
OFC_CUSTOM_PROPERTIES = (
"application/vnd.openxmlformats-officedocument.custom-properties+xml"
)
OFC_CUSTOM_PROPERTIES = "application/vnd.openxmlformats-officedocument.custom-properties+xml"
OFC_CUSTOM_XML_PROPERTIES = (

@@ -44,5 +32,3 @@ "application/vnd.openxmlformats-officedocument.customXmlProperties+xml"

OFC_THEME = "application/vnd.openxmlformats-officedocument.theme+xml"
OFC_THEME_OVERRIDE = (
"application/vnd.openxmlformats-officedocument.themeOverride+xml"
)
OFC_THEME_OVERRIDE = "application/vnd.openxmlformats-officedocument.themeOverride+xml"
OFC_VML_DRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing"

@@ -53,5 +39,3 @@ OPC_CORE_PROPERTIES = "application/vnd.openxmlformats-package.core-properties+xml"

)
OPC_DIGITAL_SIGNATURE_ORIGIN = (
"application/vnd.openxmlformats-package.digital-signature-origin"
)
OPC_DIGITAL_SIGNATURE_ORIGIN = "application/vnd.openxmlformats-package.digital-signature-origin"
OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = (

@@ -61,190 +45,111 @@ "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml"

OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml"
PML_COMMENTS = (
"application/vnd.openxmlformats-officedocument.presentationml.comments+xml"
)
PML_COMMENTS = "application/vnd.openxmlformats-officedocument.presentationml.comments+xml"
PML_COMMENT_AUTHORS = (
"application/vnd.openxmlformats-officedocument.presentationml.commen"
"tAuthors+xml"
"application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml"
)
PML_HANDOUT_MASTER = (
"application/vnd.openxmlformats-officedocument.presentationml.handou"
"tMaster+xml"
"application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml"
)
PML_NOTES_MASTER = (
"application/vnd.openxmlformats-officedocument.presentationml.notesM"
"aster+xml"
"application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml"
)
PML_NOTES_SLIDE = (
"application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"
)
PML_NOTES_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml"
PML_PRESENTATION_MAIN = (
"application/vnd.openxmlformats-officedocument.presentationml.presen"
"tation.main+xml"
"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml"
)
PML_PRES_PROPS = (
"application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"
)
PML_PRES_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml"
PML_PRINTER_SETTINGS = (
"application/vnd.openxmlformats-officedocument.presentationml.printe"
"rSettings"
"application/vnd.openxmlformats-officedocument.presentationml.printerSettings"
)
PML_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml"
PML_SLIDESHOW_MAIN = (
"application/vnd.openxmlformats-officedocument.presentationml.slides"
"how.main+xml"
"application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml"
)
PML_SLIDE_LAYOUT = (
"application/vnd.openxmlformats-officedocument.presentationml.slideL"
"ayout+xml"
"application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"
)
PML_SLIDE_MASTER = (
"application/vnd.openxmlformats-officedocument.presentationml.slideM"
"aster+xml"
"application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"
)
PML_SLIDE_UPDATE_INFO = (
"application/vnd.openxmlformats-officedocument.presentationml.slideU"
"pdateInfo+xml"
"application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml"
)
PML_TABLE_STYLES = (
"application/vnd.openxmlformats-officedocument.presentationml.tableS"
"tyles+xml"
"application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml"
)
PML_TAGS = "application/vnd.openxmlformats-officedocument.presentationml.tags+xml"
PML_TEMPLATE_MAIN = (
"application/vnd.openxmlformats-officedocument.presentationml.templa"
"te.main+xml"
"application/vnd.openxmlformats-officedocument.presentationml.template.main+xml"
)
PML_VIEW_PROPS = (
"application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml"
)
PML_VIEW_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml"
PNG = "image/png"
SML_CALC_CHAIN = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"
)
SML_CHARTSHEET = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
)
SML_COMMENTS = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
)
SML_CONNECTIONS = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml"
)
SML_CALC_CHAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"
SML_CHARTSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"
SML_COMMENTS = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
SML_CONNECTIONS = "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml"
SML_CUSTOM_PROPERTY = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty"
)
SML_DIALOGSHEET = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml"
)
SML_DIALOGSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml"
SML_EXTERNAL_LINK = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.externa"
"lLink+xml"
"application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml"
)
SML_PIVOT_CACHE_DEFINITION = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa"
"cheDefinition+xml"
"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"
)
SML_PIVOT_CACHE_RECORDS = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCa"
"cheRecords+xml"
"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"
)
SML_PIVOT_TABLE = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
)
SML_PIVOT_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"
SML_PRINTER_SETTINGS = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings"
)
SML_QUERY_TABLE = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml"
)
SML_QUERY_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml"
SML_REVISION_HEADERS = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.revisio"
"nHeaders+xml"
"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml"
)
SML_REVISION_LOG = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml"
)
SML_REVISION_LOG = "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml"
SML_SHARED_STRINGS = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedS"
"trings+xml"
"application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
)
SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
SML_SHEET_MAIN = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
)
SML_SHEET_MAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
SML_SHEET_METADATA = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMe"
"tadata+xml"
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml"
)
SML_STYLES = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"
)
SML_STYLES = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"
SML_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"
SML_TABLE_SINGLE_CELLS = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.tableSi"
"ngleCells+xml"
"application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml"
)
SML_TEMPLATE_MAIN = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.templat"
"e.main+xml"
"application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml"
)
SML_USER_NAMES = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml"
)
SML_USER_NAMES = "application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml"
SML_VOLATILE_DEPENDENCIES = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.volatil"
"eDependencies+xml"
"application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml"
)
SML_WORKSHEET = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
)
SML_WORKSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"
TIFF = "image/tiff"
WML_COMMENTS = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"
)
WML_DOCUMENT = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
WML_COMMENTS = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml"
WML_DOCUMENT = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
WML_DOCUMENT_GLOSSARY = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.docu"
"ment.glossary+xml"
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml"
)
WML_DOCUMENT_MAIN = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.docu"
"ment.main+xml"
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
)
WML_ENDNOTES = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"
)
WML_FONT_TABLE = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.font"
"Table+xml"
)
WML_FOOTER = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"
)
WML_FOOTNOTES = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.foot"
"notes+xml"
)
WML_HEADER = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"
)
WML_NUMBERING = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.numb"
"ering+xml"
)
WML_ENDNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml"
WML_FONT_TABLE = "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"
WML_FOOTER = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"
WML_FOOTNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"
WML_HEADER = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml"
WML_NUMBERING = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"
WML_PRINTER_SETTINGS = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.prin"
"terSettings"
"application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettings"
)
WML_SETTINGS = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"
)
WML_STYLES = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"
)
WML_SETTINGS = "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"
WML_STYLES = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"
WML_WEB_SETTINGS = (
"application/vnd.openxmlformats-officedocument.wordprocessingml.webS"
"ettings+xml"
"application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"
)

@@ -264,5 +169,3 @@ XML = "application/xml"

)
OFC_RELATIONSHIPS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
)
OFC_RELATIONSHIPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
OPC_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships"

@@ -282,150 +185,74 @@ OPC_CONTENT_TYPES = "http://schemas.openxmlformats.org/package/2006/content-types"

AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio"
A_F_CHUNK = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk"
)
CALC_CHAIN = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/calcChain"
)
A_F_CHUNK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk"
CALC_CHAIN = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain"
CERTIFICATE = (
"http://schemas.openxmlformats.org/package/2006/relationships/digita"
"l-signature/certificate"
"http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/certificate"
)
CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
CHARTSHEET = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/chartsheet"
)
CHARTSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet"
CHART_USER_SHAPES = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/chartUserShapes"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUserShapes"
)
COMMENTS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/comments"
)
COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
COMMENT_AUTHORS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/commentAuthors"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors"
)
CONNECTIONS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/connections"
)
CONTROL = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/control"
)
CONNECTIONS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connections"
CONTROL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control"
CORE_PROPERTIES = (
"http://schemas.openxmlformats.org/package/2006/relationships/metada"
"ta/core-properties"
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"
)
CUSTOM_PROPERTIES = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/custom-properties"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties"
)
CUSTOM_PROPERTY = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/customProperty"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customProperty"
)
CUSTOM_XML = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/customXml"
)
CUSTOM_XML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml"
CUSTOM_XML_PROPS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/customXmlProps"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps"
)
DIAGRAM_COLORS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/diagramColors"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors"
)
DIAGRAM_DATA = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/diagramData"
)
DIAGRAM_DATA = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData"
DIAGRAM_LAYOUT = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/diagramLayout"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout"
)
DIAGRAM_QUICK_STYLE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/diagramQuickStyle"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle"
)
DIALOGSHEET = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/dialogsheet"
)
DRAWING = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
)
ENDNOTES = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/endnotes"
)
DIALOGSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet"
DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
ENDNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes"
EXTENDED_PROPERTIES = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/extended-properties"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
)
EXTERNAL_LINK = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/externalLink"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink"
)
FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font"
FONT_TABLE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/fontTable"
)
FOOTER = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
)
FOOTNOTES = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/footnotes"
)
FONT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable"
FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer"
FOOTNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes"
GLOSSARY_DOCUMENT = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/glossaryDocument"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument"
)
HANDOUT_MASTER = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/handoutMaster"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster"
)
HEADER = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
)
HYPERLINK = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/hyperlink"
)
HEADER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header"
HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
NOTES_MASTER = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/notesMaster"
)
NOTES_SLIDE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/notesSlide"
)
NUMBERING = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/numbering"
)
NOTES_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster"
NOTES_SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"
NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering"
OFFICE_DOCUMENT = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/officeDocument"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
)
OLE_OBJECT = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/oleObject"
)
ORIGIN = (
"http://schemas.openxmlformats.org/package/2006/relationships/digita"
"l-signature/origin"
)
PACKAGE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/package"
)
OLE_OBJECT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject"
ORIGIN = "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin"
PACKAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package"
PIVOT_CACHE_DEFINITION = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/pivotCacheDefinition"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition"
)

@@ -436,104 +263,51 @@ PIVOT_CACHE_RECORDS = (

)
PIVOT_TABLE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/pivotTable"
)
PRES_PROPS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/presProps"
)
PIVOT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable"
PRES_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps"
PRINTER_SETTINGS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/printerSettings"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings"
)
QUERY_TABLE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/queryTable"
)
QUERY_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTable"
REVISION_HEADERS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/revisionHeaders"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionHeaders"
)
REVISION_LOG = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/revisionLog"
)
SETTINGS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/settings"
)
REVISION_LOG = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionLog"
SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"
SHARED_STRINGS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/sharedStrings"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"
)
SHEET_METADATA = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/sheetMetadata"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata"
)
SIGNATURE = (
"http://schemas.openxmlformats.org/package/2006/relationships/digita"
"l-signature/signature"
"http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/signature"
)
SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"
SLIDE_LAYOUT = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/slideLayout"
)
SLIDE_MASTER = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/slideMaster"
)
SLIDE_LAYOUT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"
SLIDE_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"
SLIDE_UPDATE_INFO = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/slideUpdateInfo"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpdateInfo"
)
STYLES = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
)
STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
TABLE_SINGLE_CELLS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/tableSingleCells"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSingleCells"
)
TABLE_STYLES = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/tableStyles"
)
TABLE_STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles"
TAGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags"
THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
THEME_OVERRIDE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/themeOverride"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOverride"
)
THUMBNAIL = (
"http://schemas.openxmlformats.org/package/2006/relationships/metada"
"ta/thumbnail"
)
USERNAMES = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/usernames"
)
THUMBNAIL = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"
USERNAMES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/usernames"
VIDEO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video"
VIEW_PROPS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/viewProps"
)
VML_DRAWING = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/vmlDrawing"
)
VIEW_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps"
VML_DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
VOLATILE_DEPENDENCIES = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/volatileDependencies"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/volatileDependencies"
)
WEB_SETTINGS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/webSettings"
)
WEB_SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings"
WORKSHEET_SOURCE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
"/worksheetSource"
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheetSource"
)
XML_MAPS = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps"
)
XML_MAPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps"

@@ -8,2 +8,3 @@ """Provides CoreProperties, Dublin-Core attributes of the document.

import datetime as dt
from typing import TYPE_CHECKING

@@ -61,3 +62,3 @@

@created.setter
def created(self, value):
def created(self, value: dt.datetime):
self._element.created_datetime = value

@@ -102,3 +103,3 @@

@last_printed.setter
def last_printed(self, value):
def last_printed(self, value: dt.datetime):
self._element.lastPrinted_datetime = value

@@ -111,3 +112,3 @@

@modified.setter
def modified(self, value):
def modified(self, value: dt.datetime):
self._element.modified_datetime = value

@@ -120,3 +121,3 @@

@revision.setter
def revision(self, value):
def revision(self, value: int):
self._element.revision_number = value

@@ -123,0 +124,0 @@

@@ -41,3 +41,3 @@ # pyright: reportPrivateUsage=false

def qn(tag):
def qn(tag: str) -> str:
"""Stands for "qualified name", a utility function to turn a namespace prefixed tag

@@ -54,3 +54,3 @@ name into a Clark-notation qualified tag name for lxml.

def serialize_part_xml(part_elm: etree._Element):
def serialize_part_xml(part_elm: etree._Element) -> bytes:
"""Serialize `part_elm` etree element to XML suitable for storage as an XML part.

@@ -64,3 +64,3 @@

def serialize_for_reading(element):
def serialize_for_reading(element: etree._Element) -> str:
"""Serialize `element` to human-readable XML suitable for tests.

@@ -83,3 +83,3 @@

@property
def xml(self):
def xml(self) -> str:
"""Return XML string for this element, suitable for testing purposes.

@@ -93,5 +93,7 @@

class CT_Default(BaseOxmlElement):
"""``<Default>`` element, specifying the default content type to be applied to a
part with the specified extension."""
"""`<Default>` element that appears in `[Content_Types].xml` part.
Used to specify a default content type to be applied to any part with the specified extension.
"""
@property

@@ -109,5 +111,4 @@ def content_type(self):

@staticmethod
def new(ext, content_type):
"""Return a new ``<Default>`` element with attributes set to parameter
values."""
def new(ext: str, content_type: str):
"""Return a new ``<Default>`` element with attributes set to parameter values."""
xml = '<Default xmlns="%s"/>' % nsmap["ct"]

@@ -132,4 +133,3 @@ default = parse_xml(xml)

def new(partname, content_type):
"""Return a new ``<Override>`` element with attributes set to parameter
values."""
"""Return a new ``<Override>`` element with attributes set to parameter values."""
xml = '<Override xmlns="%s"/>' % nsmap["ct"]

@@ -148,4 +148,3 @@ override = parse_xml(xml)

class CT_Relationship(BaseOxmlElement):
"""``<Relationship>`` element, representing a single relationship from a source to a
target part."""
"""`<Relationship>` element, representing a single relationship from source to target part."""

@@ -152,0 +151,0 @@ @staticmethod

@@ -17,2 +17,4 @@ """Objects that implement reading and writing OPC packages."""

if TYPE_CHECKING:
from typing_extensions import Self
from docx.opc.coreprops import CoreProperties

@@ -30,5 +32,2 @@ from docx.opc.part import Part

def __init__(self):
super(OpcPackage, self).__init__()
def after_unmarshal(self):

@@ -127,3 +126,3 @@ """Entry point for any post-unmarshaling processing.

@classmethod
def open(cls, pkg_file: str | IO[bytes]) -> OpcPackage:
def open(cls, pkg_file: str | IO[bytes]) -> Self:
"""Return an |OpcPackage| instance loaded with the contents of `pkg_file`."""

@@ -130,0 +129,0 @@ pkg_reader = PackageReader.from_file(pkg_file)

@@ -13,4 +13,3 @@ """Provides the PackURI value type.

class PackURI(str):
"""Provides access to pack URI components such as the baseURI and the filename
slice.
"""Provides access to pack URI components such as the baseURI and the filename slice.

@@ -17,0 +16,0 @@ Behaves as |str| otherwise.

@@ -25,5 +25,3 @@ """Low-level, read-only API to a serialized Open Packaging Convention (OPC) package."""

pkg_srels = PackageReader._srels_for(phys_reader, PACKAGE_URI)
sparts = PackageReader._load_serialized_parts(
phys_reader, pkg_srels, content_types
)
sparts = PackageReader._load_serialized_parts(phys_reader, pkg_srels, content_types)
phys_reader.close()

@@ -84,5 +82,3 @@ return PackageReader(content_types, pkg_srels, sparts)

yield (partname, blob, reltype, part_srels)
next_walker = PackageReader._walk_phys_parts(
phys_reader, part_srels, visited_partnames
)
next_walker = PackageReader._walk_phys_parts(phys_reader, part_srels, visited_partnames)
for partname, blob, reltype, srels in next_walker:

@@ -89,0 +85,0 @@ yield (partname, blob, reltype, srels)

@@ -82,5 +82,3 @@ """Relationship-related objects."""

rel_target = rel.target_ref if rel.is_external else rel.target_part
if rel_target != target:
return False
return True
return rel_target == target

@@ -146,3 +144,3 @@ for rel in self.values():

raise ValueError(
"target_part property on _Relationship is undef" "ined when target mode is External"
"target_part property on _Relationship is undefined when target mode is External"
)

@@ -149,0 +147,0 @@ return cast("Part", self._target)

@@ -0,1 +1,3 @@

# ruff: noqa: E402, I001
"""Initializes oxml sub-package.

@@ -87,7 +89,12 @@

from .coreprops import CT_CoreProperties # noqa
from .comments import CT_Comments, CT_Comment
register_element_cls("w:comments", CT_Comments)
register_element_cls("w:comment", CT_Comment)
from .coreprops import CT_CoreProperties
register_element_cls("cp:coreProperties", CT_CoreProperties)
from .document import CT_Body, CT_Document # noqa
from .document import CT_Body, CT_Document

@@ -97,3 +104,3 @@ register_element_cls("w:body", CT_Body)

from .numbering import CT_Num, CT_Numbering, CT_NumLvl, CT_NumPr # noqa
from .numbering import CT_Num, CT_Numbering, CT_NumLvl, CT_NumPr

@@ -109,3 +116,3 @@ register_element_cls("w:abstractNumId", CT_DecimalNumber)

from .section import ( # noqa
from .section import (
CT_HdrFtr,

@@ -128,7 +135,7 @@ CT_HdrFtrRef,

from .settings import CT_Settings # noqa
from .settings import CT_Settings
register_element_cls("w:settings", CT_Settings)
from .styles import CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles # noqa
from .styles import CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles

@@ -148,3 +155,3 @@ register_element_cls("w:basedOn", CT_String)

from .table import ( # noqa
from .table import (
CT_Height,

@@ -186,3 +193,3 @@ CT_Row,

from .text.font import ( # noqa
from .text.font import (
CT_Color,

@@ -226,7 +233,7 @@ CT_Fonts,

from .text.paragraph import CT_P # noqa
from .text.paragraph import CT_P
register_element_cls("w:p", CT_P)
from .text.parfmt import ( # noqa
from .text.parfmt import (
CT_Ind,

@@ -244,2 +251,3 @@ CT_Jc,

register_element_cls("w:keepNext", CT_OnOff)
register_element_cls("w:outlineLvl", CT_DecimalNumber)
register_element_cls("w:pageBreakBefore", CT_OnOff)

@@ -246,0 +254,0 @@ register_element_cls("w:pPr", CT_PPr)

@@ -7,3 +7,3 @@ """Custom element classes for core properties-related XML elements."""

import re
from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING, Any, Callable, cast

@@ -49,10 +49,10 @@ from docx.oxml.ns import nsdecls, qn

@classmethod
def new(cls):
def new(cls) -> CT_CoreProperties:
"""Return a new `<cp:coreProperties>` element."""
xml = cls._coreProperties_tmpl
coreProperties = parse_xml(xml)
coreProperties = cast(CT_CoreProperties, parse_xml(xml))
return coreProperties
@property
def author_text(self):
def author_text(self) -> str:
"""The text in the `dc:creator` child element."""

@@ -82,3 +82,3 @@ return self._text_of_element("creator")

@property
def contentStatus_text(self):
def contentStatus_text(self) -> str:
return self._text_of_element("contentStatus")

@@ -91,3 +91,3 @@

@property
def created_datetime(self):
def created_datetime(self) -> dt.datetime | None:
return self._datetime_of_element("created")

@@ -100,3 +100,3 @@

@property
def identifier_text(self):
def identifier_text(self) -> str:
return self._text_of_element("identifier")

@@ -109,3 +109,3 @@

@property
def keywords_text(self):
def keywords_text(self) -> str:
return self._text_of_element("keywords")

@@ -118,3 +118,3 @@

@property
def language_text(self):
def language_text(self) -> str:
return self._text_of_element("language")

@@ -127,3 +127,3 @@

@property
def lastModifiedBy_text(self):
def lastModifiedBy_text(self) -> str:
return self._text_of_element("lastModifiedBy")

@@ -136,3 +136,3 @@

@property
def lastPrinted_datetime(self):
def lastPrinted_datetime(self) -> dt.datetime | None:
return self._datetime_of_element("lastPrinted")

@@ -153,3 +153,3 @@

@property
def revision_number(self):
def revision_number(self) -> int:
"""Integer value of revision property."""

@@ -180,3 +180,3 @@ revision = self.revision

@property
def subject_text(self):
def subject_text(self) -> str:
return self._text_of_element("subject")

@@ -189,3 +189,3 @@

@property
def title_text(self):
def title_text(self) -> str:
return self._text_of_element("title")

@@ -198,3 +198,3 @@

@property
def version_text(self):
def version_text(self) -> str:
return self._text_of_element("version")

@@ -273,3 +273,3 @@

def _set_element_datetime(self, prop_name: str, value: dt.datetime):
def _set_element_datetime(self, prop_name: str, value: dt.datetime) -> None:
"""Set date/time value of child element having `prop_name` to `value`."""

@@ -276,0 +276,0 @@ if not isinstance(value, dt.datetime): # pyright: ignore[reportUnnecessaryIsInstance]

@@ -5,3 +5,3 @@ """Namespace-related objects."""

from typing import Any, Dict
from typing import Dict

@@ -33,3 +33,3 @@ nsmap = {

def __new__(cls, nstag: str, *args: Any):
def __new__(cls, nstag: str):
return super(NamespacePrefixedTag, cls).__new__(cls, nstag)

@@ -36,0 +36,0 @@

@@ -103,3 +103,2 @@ """Custom element classes for shape-related elements like `<w:inline>`."""

inline = cls.new(cx, cy, shape_id, pic)
inline.graphic.graphicData._insert_pic(pic)
return inline

@@ -149,6 +148,4 @@

@classmethod
def new(cls, pic_id, filename, rId, cx, cy):
"""Return a new ``<pic:pic>`` element populated with the minimal contents
required to define a viable picture element, based on the values passed as
parameters."""
def new(cls, pic_id: int, filename: str, rId: str, cx: Length, cy: Length) -> CT_Picture:
"""A new minimum viable `<pic:pic>` (picture) element."""
pic = parse_xml(cls._pic_xml())

@@ -155,0 +152,0 @@ pic.nvPicPr.cNvPr.id = pic_id

@@ -49,6 +49,5 @@ """Objects shared by modules in the docx.oxml subpackage."""

def new(cls, nsptagname: str, val: str):
"""Return a new ``CT_String`` element with tagname `nsptagname` and ``val``
attribute set to `val`."""
"""A new `CT_String`` element with tagname `nsptagname` and `val` attribute set to `val`."""
elm = cast(CT_String, OxmlElement(nsptagname))
elm.val = val
return elm

@@ -12,2 +12,3 @@ # pyright: reportImportCycles=false

import datetime as dt
from typing import TYPE_CHECKING, Any, Tuple

@@ -129,3 +130,3 @@

raise TypeError(
"only True or False (and possibly None) may be assigned, got" " '%s'" % value
"only True or False (and possibly None) may be assigned, got '%s'" % value
)

@@ -218,2 +219,54 @@

class ST_DateTime(BaseSimpleType):
@classmethod
def convert_from_xml(cls, str_value: str) -> dt.datetime:
"""Convert an xsd:dateTime string to a datetime object."""
def parse_xsd_datetime(dt_str: str) -> dt.datetime:
# -- handle trailing 'Z' (Zulu/UTC), common in Word files --
if dt_str.endswith("Z"):
try:
# -- optional fractional seconds case --
return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace(
tzinfo=dt.timezone.utc
)
except ValueError:
return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%SZ").replace(
tzinfo=dt.timezone.utc
)
# -- handles explicit offsets like +00:00, -05:00, or naive datetimes --
try:
return dt.datetime.fromisoformat(dt_str)
except ValueError:
# -- fall-back to parsing as naive datetime (with or without fractional seconds) --
try:
return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%f")
except ValueError:
return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S")
try:
# -- parse anything reasonable, but never raise, just use default epoch time --
return parse_xsd_datetime(str_value)
except Exception:
return dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc)
@classmethod
def convert_to_xml(cls, value: dt.datetime) -> str:
# -- convert naive datetime to timezon-aware assuming local timezone --
if value.tzinfo is None:
value = value.astimezone()
# -- convert to UTC if not already --
value = value.astimezone(dt.timezone.utc)
# -- format with 'Z' suffix for UTC --
return value.strftime("%Y-%m-%dT%H:%M:%SZ")
@classmethod
def validate(cls, value: Any) -> None:
if not isinstance(value, dt.datetime):
raise TypeError("only a datetime.datetime object may be assigned, got '%s'" % value)
class ST_DecimalNumber(XsdInt):

@@ -220,0 +273,0 @@ pass

@@ -522,3 +522,3 @@ """Custom element classes for tables."""

"""A new `w:tc` element, containing an empty paragraph as the required EG_BlockLevelElt."""
return cast(CT_Tc, parse_xml("<w:tc %s>\n" " <w:p/>\n" "</w:tc>" % nsdecls("w")))
return cast(CT_Tc, parse_xml("<w:tc %s><w:p/></w:tc>" % nsdecls("w")))

@@ -587,3 +587,5 @@ @property

if top_tc is not self
else None if height == 1 else ST_Merge.RESTART
else None
if height == 1
else ST_Merge.RESTART
)

@@ -614,5 +616,3 @@

only_item = block_items[0]
if isinstance(only_item, CT_P) and len(only_item.r_lst) == 0:
return True
return False
return isinstance(only_item, CT_P) and len(only_item.r_lst) == 0

@@ -619,0 +619,0 @@ def _move_content_to(self, other_tc: CT_Tc):

@@ -0,1 +1,3 @@

# pyright: reportAssignmentType=false
"""Custom element classes related to run properties (font)."""

@@ -23,2 +25,3 @@

)
from docx.shared import RGBColor

@@ -33,4 +36,4 @@ if TYPE_CHECKING:

val = RequiredAttribute("w:val", ST_HexColor)
themeColor = OptionalAttribute("w:themeColor", MSO_THEME_COLOR)
val: RGBColor | str = RequiredAttribute("w:val", ST_HexColor)
themeColor: MSO_THEME_COLOR | None = OptionalAttribute("w:themeColor", MSO_THEME_COLOR)

@@ -44,8 +47,4 @@

ascii: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"w:ascii", ST_String
)
hAnsi: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"w:hAnsi", ST_String
)
ascii: str | None = OptionalAttribute("w:ascii", ST_String)
hAnsi: str | None = OptionalAttribute("w:hAnsi", ST_String)

@@ -56,5 +55,3 @@

val: WD_COLOR_INDEX = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues]
"w:val", WD_COLOR_INDEX
)
val: WD_COLOR_INDEX = RequiredAttribute("w:val", WD_COLOR_INDEX)

@@ -65,5 +62,3 @@

val: Length = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues]
"w:val", ST_HpsMeasure
)
val: Length = RequiredAttribute("w:val", ST_HpsMeasure)

@@ -74,2 +69,3 @@

get_or_add_color: Callable[[], CT_Color]
get_or_add_highlight: Callable[[], CT_Highlight]

@@ -81,2 +77,3 @@ get_or_add_rFonts: Callable[[], CT_Fonts]

_add_u: Callable[[], CT_Underline]
_remove_color: Callable[[], None]
_remove_highlight: Callable[[], None]

@@ -130,11 +127,5 @@ _remove_rFonts: Callable[[], None]

)
rStyle: CT_String | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
"w:rStyle", successors=_tag_seq[1:]
)
rFonts: CT_Fonts | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
"w:rFonts", successors=_tag_seq[2:]
)
b: CT_OnOff | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
"w:b", successors=_tag_seq[3:]
)
rStyle: CT_String | None = ZeroOrOne("w:rStyle", successors=_tag_seq[1:])
rFonts: CT_Fonts | None = ZeroOrOne("w:rFonts", successors=_tag_seq[2:])
b: CT_OnOff | None = ZeroOrOne("w:b", successors=_tag_seq[3:])
bCs = ZeroOrOne("w:bCs", successors=_tag_seq[4:])

@@ -155,15 +146,7 @@ i = ZeroOrOne("w:i", successors=_tag_seq[5:])

webHidden = ZeroOrOne("w:webHidden", successors=_tag_seq[18:])
color = ZeroOrOne("w:color", successors=_tag_seq[19:])
sz: CT_HpsMeasure | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
"w:sz", successors=_tag_seq[24:]
)
highlight: CT_Highlight | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
"w:highlight", successors=_tag_seq[26:]
)
u: CT_Underline | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
"w:u", successors=_tag_seq[27:]
)
vertAlign: CT_VerticalAlignRun | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
"w:vertAlign", successors=_tag_seq[32:]
)
color: CT_Color | None = ZeroOrOne("w:color", successors=_tag_seq[19:])
sz: CT_HpsMeasure | None = ZeroOrOne("w:sz", successors=_tag_seq[24:])
highlight: CT_Highlight | None = ZeroOrOne("w:highlight", successors=_tag_seq[26:])
u: CT_Underline | None = ZeroOrOne("w:u", successors=_tag_seq[27:])
vertAlign: CT_VerticalAlignRun | None = ZeroOrOne("w:vertAlign", successors=_tag_seq[32:])
rtl = ZeroOrOne("w:rtl", successors=_tag_seq[33:])

@@ -265,5 +248,3 @@ cs = ZeroOrOne("w:cs", successors=_tag_seq[34:])

return None
if vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT:
return True
return False
return vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT

@@ -290,5 +271,3 @@ @subscript.setter

return None
if vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT:
return True
return False
return vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT

@@ -357,5 +336,3 @@ @superscript.setter

val: WD_UNDERLINE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"w:val", WD_UNDERLINE
)
val: WD_UNDERLINE | None = OptionalAttribute("w:val", WD_UNDERLINE)

@@ -366,4 +343,2 @@

val: str = RequiredAttribute( # pyright: ignore[reportGeneralTypeIssues]
"w:val", ST_VerticalAlignRun
)
val: str = RequiredAttribute("w:val", ST_VerticalAlignRun)

@@ -49,5 +49,3 @@ """Custom element class for rendered page-break (CT_LastRenderedPageBreak)."""

return (
self._following_frag_in_hlink
if self._is_in_hyperlink
else self._following_frag_in_run
self._following_frag_in_hlink if self._is_in_hyperlink else self._following_frag_in_run
)

@@ -120,5 +118,3 @@

return (
self._preceding_frag_in_hlink
if self._is_in_hyperlink
else self._preceding_frag_in_run
self._preceding_frag_in_hlink if self._is_in_hyperlink else self._preceding_frag_in_run
)

@@ -144,5 +140,3 @@

"""
lrpbs = p.xpath(
"./w:r/w:lastRenderedPageBreak | ./w:hyperlink/w:r/w:lastRenderedPageBreak"
)
lrpbs = p.xpath("./w:r/w:lastRenderedPageBreak | ./w:hyperlink/w:r/w:lastRenderedPageBreak")
if not lrpbs:

@@ -149,0 +143,0 @@ raise ValueError("no rendered page-breaks in paragraph element")

@@ -13,2 +13,3 @@ """Custom element classes related to paragraph properties (CT_PPr)."""

)
from docx.oxml.shared import CT_DecimalNumber
from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure

@@ -59,2 +60,3 @@ from docx.oxml.xmlchemy import (

get_or_add_pStyle: Callable[[], CT_String]
get_or_add_sectPr: Callable[[], CT_SectPr]
_insert_sectPr: Callable[[CT_SectPr], None]

@@ -116,2 +118,5 @@ _remove_pStyle: Callable[[], None]

jc = ZeroOrOne("w:jc", successors=_tag_seq[27:])
outlineLvl: CT_DecimalNumber = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"w:outlineLvl", successors=_tag_seq[31:]
)
sectPr = ZeroOrOne("w:sectPr", successors=_tag_seq[35:])

@@ -118,0 +123,0 @@ del _tag_seq

@@ -5,6 +5,7 @@ """Custom element classes related to text runs (CT_R)."""

from typing import TYPE_CHECKING, Callable, Iterator, List
from typing import TYPE_CHECKING, Callable, Iterator, List, cast
from docx.oxml.drawing import CT_Drawing
from docx.oxml.ns import qn
from docx.oxml.parser import OxmlElement
from docx.oxml.simpletypes import ST_BrClear, ST_BrType

@@ -91,2 +92,15 @@ from docx.oxml.text.font import CT_RPr

def insert_comment_range_end_and_reference_below(self, comment_id: int) -> None:
"""Insert a `w:commentRangeEnd` and `w:commentReference` element after this run.
The `w:commentRangeEnd` element is the immediate sibling of this `w:r` and is followed by
a `w:r` containing the `w:commentReference` element.
"""
self.addnext(self._new_comment_reference_run(comment_id))
self.addnext(OxmlElement("w:commentRangeEnd", attrs={qn("w:id"): str(comment_id)}))
def insert_comment_range_start_above(self, comment_id: int) -> None:
"""Insert a `w:commentRangeStart` element with `comment_id` before this run."""
self.addprevious(OxmlElement("w:commentRangeStart", attrs={qn("w:id"): str(comment_id)}))
@property

@@ -137,3 +151,20 @@ def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]:

def _new_comment_reference_run(self, comment_id: int) -> CT_R:
"""Return a new `w:r` element with `w:commentReference` referencing `comment_id`.
Should look like this:
<w:r>
<w:rPr><w:rStyle w:val="CommentReference"/></w:rPr>
<w:commentReference w:id="0"/>
</w:r>
"""
r = cast(CT_R, OxmlElement("w:r"))
rPr = r.get_or_add_rPr()
rPr.style = "CommentReference"
r.append(OxmlElement("w:commentReference", attrs={qn("w:id"): str(comment_id)}))
return r
# ------------------------------------------------------------------------------------

@@ -140,0 +171,0 @@ # Run inner-content elements

@@ -8,13 +8,3 @@ # pyright: reportImportCycles=false

import re
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
List,
Sequence,
Tuple,
Type,
TypeVar,
)
from typing import TYPE_CHECKING, Any, Callable, Sequence, Type, TypeVar

@@ -69,3 +59,3 @@ from lxml import etree

def _attr_seq(self, attrs: str) -> List[str]:
def _attr_seq(self, attrs: str) -> list[str]:
"""Return a sequence of attribute strings parsed from `attrs`.

@@ -90,8 +80,6 @@

return False
if text != text_2:
return False
return True
return text == text_2
@classmethod
def _parse_line(cls, line: str) -> Tuple[str, str, str, str]:
def _parse_line(cls, line: str) -> tuple[str, str, str, str]:
"""(front, attrs, close, text) 4-tuple result of parsing XML element `line`."""

@@ -111,3 +99,3 @@ match = cls._xml_elm_line_patt.match(line)

def __init__(cls, clsname: str, bases: Tuple[type, ...], namespace: Dict[str, Any]):
def __init__(cls, clsname: str, bases: tuple[type, ...], namespace: dict[str, Any]):
dispatchable = (

@@ -287,3 +275,3 @@ OneAndOnlyOne,

def __init__(self, nsptagname: str, successors: Tuple[str, ...] = ()):
def __init__(self, nsptagname: str, successors: tuple[str, ...] = ()):
super(_BaseChildElement, self).__init__()

@@ -443,4 +431,3 @@ self._nsptagname = nsptagname

class Choice(_BaseChildElement):
"""Defines a child element belonging to a group, only one of which may appear as a
child."""
"""Defines a child element belonging to a group, only one of which may appear as a child."""

@@ -455,3 +442,3 @@ @property

group_prop_name: str,
successors: Tuple[str, ...],
successors: tuple[str, ...],
) -> None:

@@ -484,3 +471,3 @@ """Add the appropriate methods to `element_cls`."""

get_or_change_to_child.__doc__ = (
"Return the ``<%s>`` child, replacing any other group element if" " found."
"Return the ``<%s>`` child, replacing any other group element if found."
) % self._nsptagname

@@ -608,3 +595,3 @@ self._add_to_class(self._get_or_change_to_method_name, get_or_change_to_child)

def __init__(self, choices: Sequence[Choice], successors: Tuple[str, ...] = ()):
def __init__(self, choices: Sequence[Choice], successors: tuple[str, ...] = ()):
self._choices = choices

@@ -611,0 +598,0 @@ self._successors = successors

@@ -8,4 +8,4 @@ """|DocumentPart| and closely related objects."""

from docx.document import Document
from docx.enum.style import WD_STYLE_TYPE
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docx.parts.comments import CommentsPart
from docx.parts.hdrftr import FooterPart, HeaderPart

@@ -20,2 +20,4 @@ from docx.parts.numbering import NumberingPart

if TYPE_CHECKING:
from docx.comments import Comments
from docx.enum.style import WD_STYLE_TYPE
from docx.opc.coreprops import CoreProperties

@@ -48,2 +50,7 @@ from docx.settings import Settings

@property
def comments(self) -> Comments:
"""|Comments| object providing access to the comments added to this document."""
return self._comments_part.comments
@property
def core_properties(self) -> CoreProperties:

@@ -95,5 +102,4 @@ """A |CoreProperties| object providing read/write access to the core properties

@lazyproperty
def numbering_part(self):
"""A |NumberingPart| object providing access to the numbering definitions for
this document.
def numbering_part(self) -> NumberingPart:
"""A |NumberingPart| object providing access to the numbering definitions for this document.

@@ -103,3 +109,3 @@ Creates an empty numbering part if one is not present.

try:
return self.part_related_by(RT.NUMBERING)
return cast(NumberingPart, self.part_related_by(RT.NUMBERING))
except KeyError:

@@ -128,2 +134,16 @@ numbering_part = NumberingPart.new()

@property
def _comments_part(self) -> CommentsPart:
"""A |CommentsPart| object providing access to the comments added to this document.
Creates a default comments part if one is not present.
"""
try:
return cast(CommentsPart, self.part_related_by(RT.COMMENTS))
except KeyError:
assert self.package is not None
comments_part = CommentsPart.default(self.package)
self.relate_to(comments_part, RT.COMMENTS)
return comments_part
@property
def _settings_part(self) -> SettingsPart:

@@ -130,0 +150,0 @@ """A |SettingsPart| object providing access to the document-level settings for

@@ -12,5 +12,4 @@ """|NumberingPart| and closely related objects."""

@classmethod
def new(cls):
"""Return newly created empty numbering part, containing only the root
``<w:numbering>`` element."""
def new(cls) -> "NumberingPart":
"""Newly created numbering part, containing only the root ``<w:numbering>`` element."""
raise NotImplementedError

@@ -17,0 +16,0 @@

@@ -30,4 +30,3 @@ """|SettingsPart| and closely related objects."""

def default(cls, package: Package):
"""Return a newly created settings part, containing a default `w:settings`
element tree."""
"""Return a newly created settings part, containing a default `w:settings` element tree."""
partname = PackURI("/word/settings.xml")

@@ -34,0 +33,0 @@ content_type = CT.WML_SETTINGS

@@ -39,7 +39,5 @@ """Provides StylesPart and related objects."""

"""Return a bytestream containing XML for a default styles part."""
path = os.path.join(
os.path.split(__file__)[0], "..", "templates", "default-styles.xml"
)
path = os.path.join(os.path.split(__file__)[0], "..", "templates", "default-styles.xml")
with open(path, "rb") as f:
xml_bytes = f.read()
return xml_bytes

@@ -130,7 +130,5 @@ """Objects shared by docx modules."""

for val in (r, g, b):
if (
not isinstance(val, int) # pyright: ignore[reportUnnecessaryIsInstance]
or val < 0
or val > 255
):
if not isinstance(val, int): # pyright: ignore[reportUnnecessaryIsInstance]
raise TypeError(msg)
if val < 0 or val > 255:
raise ValueError(msg)

@@ -334,3 +332,3 @@ return super(RGBColor, cls).__new__(cls, (r, g, b))

@property
def part(self):
def part(self) -> XmlPart:
"""The package part containing this object."""

@@ -337,0 +335,0 @@ return self._parent.part

@@ -43,6 +43,3 @@ """Styles object, container for all objects in the styles part."""

if style_elm is not None:
msg = (
"style lookup by style_id is deprecated. Use style name as "
"key instead."
)
msg = "style lookup by style_id is deprecated. Use style name as key instead."
warn(msg, UserWarning, stacklevel=2)

@@ -122,5 +119,3 @@ return StyleFactory(style_elm)

def _get_style_id_from_name(
self, style_name: str, style_type: WD_STYLE_TYPE
) -> str | None:
def _get_style_id_from_name(self, style_name: str, style_type: WD_STYLE_TYPE) -> str | None:
"""Return the id of the style of `style_type` corresponding to `style_name`.

@@ -134,5 +129,3 @@

def _get_style_id_from_style(
self, style: BaseStyle, style_type: WD_STYLE_TYPE
) -> str | None:
def _get_style_id_from_style(self, style: BaseStyle, style_type: WD_STYLE_TYPE) -> str | None:
"""Id of `style`, or |None| if it is the default style of `style_type`.

@@ -143,7 +136,5 @@

if style.type != style_type:
raise ValueError(
"assigned style is type %s, need type %s" % (style.type, style_type)
)
raise ValueError("assigned style is type %s, need type %s" % (style.type, style_type))
if style == self.default(style_type):
return None
return style.style_id

@@ -401,7 +401,3 @@ """Font-related proxy objects."""

val = (
WD_UNDERLINE.SINGLE
if value is True
else WD_UNDERLINE.NONE
if value is False
else value
WD_UNDERLINE.SINGLE if value is True else WD_UNDERLINE.NONE if value is False else value
)

@@ -408,0 +404,0 @@ rPr.u_val = val

@@ -176,2 +176,14 @@ """Run-related proxy objects for python-docx, Run in particular."""

def mark_comment_range(self, last_run: Run, comment_id: int) -> None:
"""Mark the range of runs from this run to `last_run` (inclusive) as belonging to a comment.
`comment_id` identfies the comment that references this range.
"""
# -- insert `w:commentRangeStart` with `comment_id` before this (first) run --
self._r.insert_comment_range_start_above(comment_id)
# -- insert `w:commentRangeEnd` and `w:commentReference` run with `comment_id` after
# -- `last_run`
last_run._r.insert_comment_range_end_and_reference_below(comment_id)
@property

@@ -237,3 +249,3 @@ def style(self) -> CharacterStyle:

@underline.setter
def underline(self, value: bool):
def underline(self, value: bool | WD_UNDERLINE | None):
self.font.underline = value

@@ -240,0 +252,0 @@

@@ -53,5 +53,3 @@ """Tabstop-related proxy types."""

def add_tab_stop(
self, position, alignment=WD_TAB_ALIGNMENT.LEFT, leader=WD_TAB_LEADER.SPACES
):
def add_tab_stop(self, position, alignment=WD_TAB_ALIGNMENT.LEFT, leader=WD_TAB_LEADER.SPACES):
"""Add a new tab stop at `position`, a |Length| object specifying the location

@@ -58,0 +56,0 @@ of the tab stop relative to the paragraph edge.

@@ -22,4 +22,3 @@ """Abstract types used by `python-docx`."""

@property
def part(self) -> StoryPart:
...
def part(self) -> StoryPart: ...

@@ -36,3 +35,2 @@

@property
def part(self) -> XmlPart:
...
def part(self) -> XmlPart: ...

@@ -1,4 +0,4 @@

Metadata-Version: 2.1
Metadata-Version: 2.4
Name: python-docx
Version: 1.1.2
Version: 1.2.0
Summary: Create, read, and update Microsoft Word .docx files.

@@ -26,3 +26,3 @@ Author-email: Steve Canny <stcanny@gmail.com>

Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.7
Requires-Python: >=3.9
Description-Content-Type: text/markdown

@@ -32,2 +32,3 @@ License-File: LICENSE

Requires-Dist: typing_extensions>=4.9.0
Dynamic: license-file

@@ -34,0 +35,0 @@ # python-docx

@@ -16,2 +16,3 @@ HISTORY.rst

docs/_static/.gitignore
docs/_static/img/comment-parts.png
docs/_static/img/example-docx-01.png

@@ -27,2 +28,3 @@ docs/_static/img/hdrftr-01.png

docs/_themes/armstrong/static/rtd.css_t
docs/api/comments.rst
docs/api/dml.rst

@@ -55,2 +57,3 @@ docs/api/document.rst

docs/dev/analysis/index.rst
docs/dev/analysis/features/comments.rst
docs/dev/analysis/features/coreprops.rst

@@ -90,2 +93,3 @@ docs/dev/analysis/features/header.rst

docs/user/api-concepts.rst
docs/user/comments.rst
docs/user/documents.rst

@@ -105,4 +109,7 @@ docs/user/hdrftr.rst

features/blk-iter-inner-content.feature
features/cmt-mutations.feature
features/cmt-props.feature
features/doc-access-collections.feature
features/doc-access-sections.feature
features/doc-add-comment.feature
features/doc-add-heading.feature

@@ -114,2 +121,3 @@ features/doc-add-page-break.feature

features/doc-add-table.feature
features/doc-comments.feature
features/doc-coreprops.feature

@@ -187,2 +195,3 @@ features/doc-settings.feature

features/steps/block.py
features/steps/comments.py
features/steps/coreprops.py

@@ -209,2 +218,3 @@ features/steps/document.py

features/steps/test_files/blk-paras-and-tables.docx
features/steps/test_files/comments-rich-para.docx
features/steps/test_files/court-exif.jpg

@@ -263,2 +273,3 @@ features/steps/test_files/doc-access-sections.docx

src/docx/blkcntnr.py
src/docx/comments.py
src/docx/document.py

@@ -312,2 +323,3 @@ src/docx/exceptions.py

src/docx/oxml/__init__.py
src/docx/oxml/comments.py
src/docx/oxml/coreprops.py

@@ -336,2 +348,3 @@ src/docx/oxml/document.py

src/docx/parts/__init__.py
src/docx/parts/comments.py
src/docx/parts/document.py

@@ -348,2 +361,3 @@ src/docx/parts/hdrftr.py

src/docx/styles/styles.py
src/docx/templates/default-comments.xml
src/docx/templates/default-footer.xml

@@ -388,3 +402,5 @@ src/docx/templates/default-header.xml

tests/test_blkcntnr.py
tests/test_comments.py
tests/test_document.py
tests/test_drawing.py
tests/test_enum.py

@@ -425,2 +441,3 @@ tests/test_package.py

tests/oxml/test__init__.py
tests/oxml/test_comments.py
tests/oxml/test_document.py

@@ -440,3 +457,2 @@ tests/oxml/test_ns.py

tests/oxml/unitdata/__init__.py
tests/oxml/unitdata/dml.py
tests/oxml/unitdata/numbering.py

@@ -448,2 +464,3 @@ tests/oxml/unitdata/section.py

tests/parts/__init__.py
tests/parts/test_comments.py
tests/parts/test_document.py

@@ -450,0 +467,0 @@ tests/parts/test_hdrftr.py

@@ -1,3 +0,9 @@

"""Test suite for docx.dml.color module."""
# pyright: reportPrivateUsage=false
"""Unit-test suite for the `docx.dml.color` module."""
from __future__ import annotations
from typing import cast
import pytest

@@ -7,2 +13,3 @@

from docx.enum.dml import MSO_COLOR_TYPE, MSO_THEME_COLOR
from docx.oxml.text.run import CT_R
from docx.shared import RGBColor

@@ -14,28 +21,24 @@

class DescribeColorFormat:
def it_knows_its_color_type(self, type_fixture):
color_format, expected_value = type_fixture
assert color_format.type == expected_value
"""Unit-test suite for `docx.dml.color.ColorFormat` objects."""
def it_knows_its_RGB_value(self, rgb_get_fixture):
color_format, expected_value = rgb_get_fixture
assert color_format.rgb == expected_value
@pytest.mark.parametrize(
("r_cxml", "expected_value"),
[
("w:r", None),
("w:r/w:rPr", None),
("w:r/w:rPr/w:color{w:val=auto}", MSO_COLOR_TYPE.AUTO),
("w:r/w:rPr/w:color{w:val=4224FF}", MSO_COLOR_TYPE.RGB),
("w:r/w:rPr/w:color{w:themeColor=dark1}", MSO_COLOR_TYPE.THEME),
(
"w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=accent1}",
MSO_COLOR_TYPE.THEME,
),
],
)
def it_knows_its_color_type(self, r_cxml: str, expected_value: MSO_COLOR_TYPE | None):
assert ColorFormat(cast(CT_R, element(r_cxml))).type == expected_value
def it_can_change_its_RGB_value(self, rgb_set_fixture):
color_format, new_value, expected_xml = rgb_set_fixture
color_format.rgb = new_value
assert color_format._element.xml == expected_xml
def it_knows_its_theme_color(self, theme_color_get_fixture):
color_format, expected_value = theme_color_get_fixture
assert color_format.theme_color == expected_value
def it_can_change_its_theme_color(self, theme_color_set_fixture):
color_format, new_value, expected_xml = theme_color_set_fixture
color_format.theme_color = new_value
assert color_format._element.xml == expected_xml
# fixtures ---------------------------------------------
@pytest.fixture(
params=[
@pytest.mark.parametrize(
("r_cxml", "rgb"),
[
("w:r", None),

@@ -47,12 +50,11 @@ ("w:r/w:rPr", None),

("w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=accent1}", "f00ba9"),
]
],
)
def rgb_get_fixture(self, request):
r_cxml, rgb = request.param
color_format = ColorFormat(element(r_cxml))
expected_value = None if rgb is None else RGBColor.from_string(rgb)
return color_format, expected_value
def it_knows_its_RGB_value(self, r_cxml: str, rgb: str | None):
expected_value = RGBColor.from_string(rgb) if rgb else None
assert ColorFormat(cast(CT_R, element(r_cxml))).rgb == expected_value
@pytest.fixture(
params=[
@pytest.mark.parametrize(
("r_cxml", "new_value", "expected_cxml"),
[
("w:r", RGBColor(10, 20, 30), "w:r/w:rPr/w:color{w:val=0A141E}"),

@@ -77,12 +79,14 @@ ("w:r/w:rPr", RGBColor(1, 2, 3), "w:r/w:rPr/w:color{w:val=010203}"),

("w:r", None, "w:r"),
]
],
)
def rgb_set_fixture(self, request):
r_cxml, new_value, expected_cxml = request.param
color_format = ColorFormat(element(r_cxml))
expected_xml = xml(expected_cxml)
return color_format, new_value, expected_xml
def it_can_change_its_RGB_value(
self, r_cxml: str, new_value: RGBColor | None, expected_cxml: str
):
color_format = ColorFormat(cast(CT_R, element(r_cxml)))
color_format.rgb = new_value
assert color_format._element.xml == xml(expected_cxml)
@pytest.fixture(
params=[
@pytest.mark.parametrize(
("r_cxml", "expected_value"),
[
("w:r", None),

@@ -92,18 +96,21 @@ ("w:r/w:rPr", None),

("w:r/w:rPr/w:color{w:val=4224FF}", None),
("w:r/w:rPr/w:color{w:themeColor=accent1}", "ACCENT_1"),
("w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=dark1}", "DARK_1"),
]
("w:r/w:rPr/w:color{w:themeColor=accent1}", MSO_THEME_COLOR.ACCENT_1),
("w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=dark1}", MSO_THEME_COLOR.DARK_1),
],
)
def theme_color_get_fixture(self, request):
r_cxml, value = request.param
color_format = ColorFormat(element(r_cxml))
expected_value = None if value is None else getattr(MSO_THEME_COLOR, value)
return color_format, expected_value
def it_knows_its_theme_color(self, r_cxml: str, expected_value: MSO_THEME_COLOR | None):
color_format = ColorFormat(cast(CT_R, element(r_cxml)))
assert color_format.theme_color == expected_value
@pytest.fixture(
params=[
("w:r", "ACCENT_1", "w:r/w:rPr/w:color{w:val=000000,w:themeColor=accent1}"),
@pytest.mark.parametrize(
("r_cxml", "new_value", "expected_cxml"),
[
(
"w:r",
MSO_THEME_COLOR.ACCENT_1,
"w:r/w:rPr/w:color{w:val=000000,w:themeColor=accent1}",
),
(
"w:r/w:rPr",
"ACCENT_2",
MSO_THEME_COLOR.ACCENT_2,
"w:r/w:rPr/w:color{w:val=000000,w:themeColor=accent2}",

@@ -113,3 +120,3 @@ ),

"w:r/w:rPr/w:color{w:val=101112}",
"ACCENT_3",
MSO_THEME_COLOR.ACCENT_3,
"w:r/w:rPr/w:color{w:val=101112,w:themeColor=accent3}",

@@ -119,3 +126,3 @@ ),

"w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=dark1}",
"LIGHT_2",
MSO_THEME_COLOR.LIGHT_2,
"w:r/w:rPr/w:color{w:val=234bcd,w:themeColor=light2}",

@@ -125,27 +132,9 @@ ),

("w:r", None, "w:r"),
]
],
)
def theme_color_set_fixture(self, request):
r_cxml, member, expected_cxml = request.param
color_format = ColorFormat(element(r_cxml))
new_value = None if member is None else getattr(MSO_THEME_COLOR, member)
expected_xml = xml(expected_cxml)
return color_format, new_value, expected_xml
@pytest.fixture(
params=[
("w:r", None),
("w:r/w:rPr", None),
("w:r/w:rPr/w:color{w:val=auto}", MSO_COLOR_TYPE.AUTO),
("w:r/w:rPr/w:color{w:val=4224FF}", MSO_COLOR_TYPE.RGB),
("w:r/w:rPr/w:color{w:themeColor=dark1}", MSO_COLOR_TYPE.THEME),
(
"w:r/w:rPr/w:color{w:val=F00BA9,w:themeColor=accent1}",
MSO_COLOR_TYPE.THEME,
),
]
)
def type_fixture(self, request):
r_cxml, expected_value = request.param
color_format = ColorFormat(element(r_cxml))
return color_format, expected_value
def it_can_change_its_theme_color(
self, r_cxml: str, new_value: MSO_THEME_COLOR | None, expected_cxml: str
):
color_format = ColorFormat(cast(CT_R, element(r_cxml)))
color_format.theme_color = new_value
assert color_format._element.xml == xml(expected_cxml)

@@ -17,4 +17,4 @@ """Test suite for docx.image.bmp module."""

bytes_ = (
b"fillerfillerfiller\x1A\x00\x00\x00\x2B\x00\x00\x00"
b"fillerfiller\xB8\x1E\x00\x00\x00\x00\x00\x00"
b"fillerfillerfiller\x1a\x00\x00\x00\x2b\x00\x00\x00"
b"fillerfiller\xb8\x1e\x00\x00\x00\x00\x00\x00"
)

@@ -21,0 +21,0 @@ stream = io.BytesIO(bytes_)

@@ -16,3 +16,3 @@ """Unit test suite for docx.image.gif module."""

cx, cy = 42, 24
bytes_ = b"filler\x2A\x00\x18\x00"
bytes_ = b"filler\x2a\x00\x18\x00"
stream = io.BytesIO(bytes_)

@@ -19,0 +19,0 @@

@@ -31,4 +31,4 @@ """Test suite for docx.image.helpers module."""

params=[
(BIG_ENDIAN, b"\xBE\x00\x00\x00\x2A\xEF", 1, 42),
(LITTLE_ENDIAN, b"\xBE\xEF\x2A\x00\x00\x00", 2, 42),
(BIG_ENDIAN, b"\xbe\x00\x00\x00\x2a\xef", 1, 42),
(LITTLE_ENDIAN, b"\xbe\xef\x2a\x00\x00\x00", 2, 42),
]

@@ -35,0 +35,0 @@ )

@@ -30,5 +30,3 @@ """Unit test suite for docx.image package"""

class DescribeImage:
def it_can_construct_from_an_image_blob(
self, blob_, BytesIO_, _from_stream_, stream_, image_
):
def it_can_construct_from_an_image_blob(self, blob_, BytesIO_, _from_stream_, stream_, image_):
image = Image.from_blob(blob_)

@@ -235,5 +233,3 @@

def _from_stream_(self, request, image_):
return method_mock(
request, Image, "_from_stream", autospec=False, return_value=image_
)
return method_mock(request, Image, "_from_stream", autospec=False, return_value=image_)

@@ -240,0 +236,0 @@ @pytest.fixture

@@ -250,3 +250,3 @@ """Unit test suite for docx.image.jpeg module"""

marker_code, offset, length = request.param
bytes_ = b"\xFF\xD8\xFF\xE0\x00\x10"
bytes_ = b"\xff\xd8\xff\xe0\x00\x10"
stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)

@@ -262,3 +262,3 @@ return stream_reader, marker_code, offset, _Marker__init_, length

def it_can_construct_from_a_stream_and_offset(self, _App0Marker__init_):
bytes_ = b"\x00\x10JFIF\x00\x01\x01\x01\x00\x2A\x00\x18"
bytes_ = b"\x00\x10JFIF\x00\x01\x01\x01\x00\x2a\x00\x18"
marker_code, offset, length = JPEG_MARKER_CODE.APP0, 0, 16

@@ -323,5 +323,3 @@ density_units, x_density, y_density = 1, 42, 24

_App1Marker__init_.assert_called_once_with(
ANY, marker_code, offset, length, 72, 72
)
_App1Marker__init_.assert_called_once_with(ANY, marker_code, offset, length, 72, 72)
assert isinstance(app1_marker, _App1Marker)

@@ -354,5 +352,3 @@

stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)
BytesIO_ = class_mock(
request, "docx.image.jpeg.io.BytesIO", return_value=substream_
)
BytesIO_ = class_mock(request, "docx.image.jpeg.io.BytesIO", return_value=substream_)
offset, segment_length, segment_bytes = 0, 16, bytes_[8:]

@@ -397,3 +393,3 @@ return (

def it_can_construct_from_a_stream_and_offset(self, request, _SofMarker__init_):
bytes_ = b"\x00\x11\x00\x00\x2A\x00\x18"
bytes_ = b"\x00\x11\x00\x00\x2a\x00\x18"
marker_code, offset, length = JPEG_MARKER_CODE.SOF0, 0, 17

@@ -517,3 +513,3 @@ px_width, px_height = 24, 42

start, marker_code, segment_offset = request.param
bytes_ = b"\xFF\xD8\xFF\xE0\x00\x01\xFF\x00\xFF\xFF\xFF\xD9"
bytes_ = b"\xff\xd8\xff\xe0\x00\x01\xff\x00\xff\xff\xff\xd9"
stream_reader = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)

@@ -635,5 +631,3 @@ marker_finder = _MarkerFinder(stream_reader)

def StreamReader_(self, request, stream_reader_):
return class_mock(
request, "docx.image.jpeg.StreamReader", return_value=stream_reader_
)
return class_mock(request, "docx.image.jpeg.StreamReader", return_value=stream_reader_)

@@ -640,0 +634,0 @@ @pytest.fixture

@@ -33,5 +33,3 @@ """Unit test suite for docx.image.png module."""

class DescribePng:
def it_can_construct_from_a_png_stream(
self, stream_, _PngParser_, png_parser_, Png__init__
):
def it_can_construct_from_a_png_stream(self, stream_, _PngParser_, png_parser_, Png__init__):
px_width, px_height, horz_dpi, vert_dpi = 42, 24, 36, 63

@@ -46,5 +44,3 @@ png_parser_.px_width = px_width

_PngParser_.parse.assert_called_once_with(stream_)
Png__init__.assert_called_once_with(
ANY, px_width, px_height, horz_dpi, vert_dpi
)
Png__init__.assert_called_once_with(ANY, px_width, px_height, horz_dpi, vert_dpi)
assert isinstance(png, Png)

@@ -162,5 +158,3 @@

class Describe_Chunks:
def it_can_construct_from_a_stream(
self, stream_, _ChunkParser_, chunk_parser_, _Chunks__init_
):
def it_can_construct_from_a_stream(self, stream_, _ChunkParser_, chunk_parser_, _Chunks__init_):
chunk_lst = [1, 2]

@@ -283,5 +277,3 @@ chunk_parser_.iter_chunks.return_value = iter(chunk_lst)

def _ChunkFactory_(self, request, chunk_lst_):
return function_mock(
request, "docx.image.png._ChunkFactory", side_effect=chunk_lst_
)
return function_mock(request, "docx.image.png._ChunkFactory", side_effect=chunk_lst_)

@@ -322,5 +314,3 @@ @pytest.fixture

def StreamReader_(self, request, stream_rdr_):
return class_mock(
request, "docx.image.png.StreamReader", return_value=stream_rdr_
)
return class_mock(request, "docx.image.png.StreamReader", return_value=stream_rdr_)

@@ -417,3 +407,3 @@ @pytest.fixture

def from_offset_fixture(self):
bytes_ = b"\x00\x00\x00\x2A\x00\x00\x00\x18"
bytes_ = b"\x00\x00\x00\x2a\x00\x00\x00\x18"
stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)

@@ -439,5 +429,5 @@ offset, px_width, px_height = 0, 42, 24

def from_offset_fixture(self):
bytes_ = b"\x00\x00\x00\x2A\x00\x00\x00\x18\x01"
bytes_ = b"\x00\x00\x00\x2a\x00\x00\x00\x18\x01"
stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)
offset, horz_px_per_unit, vert_px_per_unit, units_specifier = (0, 42, 24, 1)
return (stream_rdr, offset, horz_px_per_unit, vert_px_per_unit, units_specifier)

@@ -35,5 +35,3 @@ """Unit test suite for docx.image.tiff module"""

class DescribeTiff:
def it_can_construct_from_a_tiff_stream(
self, stream_, _TiffParser_, tiff_parser_, Tiff__init_
):
def it_can_construct_from_a_tiff_stream(self, stream_, _TiffParser_, tiff_parser_, Tiff__init_):
px_width, px_height = 111, 222

@@ -49,5 +47,3 @@ horz_dpi, vert_dpi = 333, 444

_TiffParser_.parse.assert_called_once_with(stream_)
Tiff__init_.assert_called_once_with(
ANY, px_width, px_height, horz_dpi, vert_dpi
)
Tiff__init_.assert_called_once_with(ANY, px_width, px_height, horz_dpi, vert_dpi)
assert isinstance(tiff, Tiff)

@@ -191,5 +187,3 @@

def StreamReader_(self, request, stream_rdr_):
return class_mock(
request, "docx.image.tiff.StreamReader", return_value=stream_rdr_
)
return class_mock(request, "docx.image.tiff.StreamReader", return_value=stream_rdr_)

@@ -250,5 +244,3 @@ @pytest.fixture

def _IfdParser_(self, request, ifd_parser_):
return class_mock(
request, "docx.image.tiff._IfdParser", return_value=ifd_parser_
)
return class_mock(request, "docx.image.tiff._IfdParser", return_value=ifd_parser_)

@@ -393,5 +385,3 @@ @pytest.fixture

class Describe_IfdEntry:
def it_can_construct_from_a_stream_and_offset(
self, _parse_value_, _IfdEntry__init_, value_
):
def it_can_construct_from_a_stream_and_offset(self, _parse_value_, _IfdEntry__init_, value_):
bytes_ = b"\x00\x01\x66\x66\x00\x00\x00\x02\x00\x00\x00\x03"

@@ -404,5 +394,3 @@ stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)

_parse_value_.assert_called_once_with(
stream_rdr, offset, value_count, value_offset
)
_parse_value_.assert_called_once_with(stream_rdr, offset, value_count, value_offset)
_IfdEntry__init_.assert_called_once_with(ANY, tag_code, value_)

@@ -441,3 +429,3 @@ assert isinstance(ifd_entry, _IfdEntry)

def it_can_parse_a_short_int_IFD_entry(self):
bytes_ = b"foobaroo\x00\x2A"
bytes_ = b"foobaroo\x00\x2a"
stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)

@@ -450,3 +438,3 @@ val = _ShortIfdEntry._parse_value(stream_rdr, 0, 1, None)

def it_can_parse_a_long_int_IFD_entry(self):
bytes_ = b"foobaroo\x00\x00\x00\x2A"
bytes_ = b"foobaroo\x00\x00\x00\x2a"
stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)

@@ -459,5 +447,5 @@ val = _LongIfdEntry._parse_value(stream_rdr, 0, 1, None)

def it_can_parse_a_rational_IFD_entry(self):
bytes_ = b"\x00\x00\x00\x2A\x00\x00\x00\x54"
bytes_ = b"\x00\x00\x00\x2a\x00\x00\x00\x54"
stream_rdr = StreamReader(io.BytesIO(bytes_), BIG_ENDIAN)
val = _RationalIfdEntry._parse_value(stream_rdr, None, 1, 0)
assert val == 0.5

@@ -47,5 +47,3 @@ """Unit test suite for docx.opc.pkgreader module."""

_srels_for.assert_called_once_with(phys_reader, "/")
_load_serialized_parts.assert_called_once_with(
phys_reader, pkg_srels, content_types
)
_load_serialized_parts.assert_called_once_with(phys_reader, pkg_srels, content_types)
phys_reader.close.assert_called_once_with()

@@ -98,13 +96,7 @@ _init_.assert_called_once_with(ANY, content_types, pkg_srels, sparts)

# exercise ---------------------
retval = PackageReader._load_serialized_parts(
phys_reader, pkg_srels, content_types
)
retval = PackageReader._load_serialized_parts(phys_reader, pkg_srels, content_types)
# verify -----------------------
expected_calls = [
call(
"/part/name1.xml", "app/vnd.type_1", "<Part_1/>", "reltype1", "srels_1"
),
call(
"/part/name2.xml", "app/vnd.type_2", "<Part_2/>", "reltype2", "srels_2"
),
call("/part/name1.xml", "app/vnd.type_1", "<Part_1/>", "reltype1", "srels_1"),
call("/part/name2.xml", "app/vnd.type_2", "<Part_2/>", "reltype2", "srels_2"),
]

@@ -213,5 +205,3 @@ assert _SerializedPart_.call_args_list == expected_calls

@pytest.fixture
def iter_sparts_fixture(
self, sparts_, partnames_, content_types_, reltypes_, blobs_
):
def iter_sparts_fixture(self, sparts_, partnames_, content_types_, reltypes_, blobs_):
pkg_reader = PackageReader(None, None, sparts_)

@@ -226,5 +216,3 @@ expected_iter_spart_items = [

def _load_serialized_parts(self, request):
return method_mock(
request, PackageReader, "_load_serialized_parts", autospec=False
)
return method_mock(request, PackageReader, "_load_serialized_parts", autospec=False)

@@ -290,11 +278,7 @@ @pytest.fixture

def it_matches_an_override_on_case_insensitive_partname(
self, match_override_fixture
):
def it_matches_an_override_on_case_insensitive_partname(self, match_override_fixture):
ct_map, partname, content_type = match_override_fixture
assert ct_map[partname] == content_type
def it_falls_back_to_case_insensitive_extension_default_match(
self, match_default_fixture
):
def it_falls_back_to_case_insensitive_extension_default_match(self, match_default_fixture):
ct_map, partname, content_type = match_default_fixture

@@ -301,0 +285,0 @@ assert ct_map[partname] == content_type

@@ -80,5 +80,3 @@ # pyright: reportPrivateUsage=false

def it_can_find_or_add_a_relationship(
self, rels_with_matching_rel_, rels_with_missing_rel_
):
def it_can_find_or_add_a_relationship(self, rels_with_matching_rel_, rels_with_missing_rel_):
rels, reltype, part, matching_rel = rels_with_matching_rel_

@@ -90,5 +88,3 @@ assert rels.get_or_add(reltype, part) == matching_rel

def it_can_find_or_add_an_external_relationship(
self, add_matching_ext_rel_fixture_
):
def it_can_find_or_add_an_external_relationship(self, add_matching_ext_rel_fixture_):
rels, reltype, url, rId = add_matching_ext_rel_fixture_

@@ -240,8 +236,4 @@ _rId = rels.get_or_add_ext_rel(reltype, url)

rels = Relationships(None)
rel_with_rId1 = instance_mock(
request, _Relationship, name="rel_with_rId1", rId="rId1"
)
rel_with_rId3 = instance_mock(
request, _Relationship, name="rel_with_rId3", rId="rId3"
)
rel_with_rId1 = instance_mock(request, _Relationship, name="rel_with_rId1", rId="rId1")
rel_with_rId3 = instance_mock(request, _Relationship, name="rel_with_rId3", rId="rId3")
rels["rId1"] = rel_with_rId1

@@ -252,5 +244,3 @@ rels["rId3"] = rel_with_rId3

@pytest.fixture
def rels_with_target_known_by_reltype(
self, rels, _rel_with_target_known_by_reltype
):
def rels_with_target_known_by_reltype(self, rels, _rel_with_target_known_by_reltype):
rel, reltype, target_part = _rel_with_target_known_by_reltype

@@ -257,0 +247,0 @@ rels[1] = rel

@@ -41,7 +41,4 @@ """Test suite for the docx.oxml.parts module."""

expected_xml = xml(
"w:body/("
" w:p/w:pPr/w:sectPr/w:type{w:val=foobar},"
" w:sectPr/w:type{w:val=foobar}"
")"
"w:body/(w:p/w:pPr/w:sectPr/w:type{w:val=foobar},w:sectPr/w:type{w:val=foobar})"
)
return body, expected_xml

@@ -15,5 +15,3 @@ """Test suite for pptx.oxml.__init__.py module, primarily XML parser-related."""

assert isinstance(element, etree._Element)
assert element.tag == (
"{http://schemas.openxmlformats.org/drawingml/2006/main}foo"
)
assert element.tag == ("{http://schemas.openxmlformats.org/drawingml/2006/main}foo")

@@ -23,4 +21,3 @@ def it_adds_supplied_attributes(self):

assert etree.tostring(element) == (
'<a:foo xmlns:a="http://schemas.openxmlformats.org/drawingml/200'
'6/main" a="b" c="d"/>'
'<a:foo xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" a="b" c="d"/>'
).encode("utf-8")

@@ -48,3 +45,3 @@

def whitespace_fixture(self):
pretty_xml_text = "<foø>\n" " <bår>text</bår>\n" "</foø>\n"
pretty_xml_text = "<foø>\n <bår>text</bår>\n</foø>\n"
stripped_xml_text = "<foø><bår>text</bår></foø>"

@@ -51,0 +48,0 @@ return pretty_xml_text, stripped_xml_text

@@ -34,4 +34,3 @@ """Test suite for the docx.oxml.styles module."""

True,
"w:styles/w:style{w:type=paragraph,w:styleId=Heading1}/w:name{w:val"
"=heading 1}",
"w:styles/w:style{w:type=paragraph,w:styleId=Heading1}/w:name{w:val=heading 1}",
),

@@ -38,0 +37,0 @@ ]

@@ -22,3 +22,2 @@ # pyright: reportPrivateUsage=false

class DescribeCT_Row:
@pytest.mark.parametrize(

@@ -235,3 +234,3 @@ ("tr_cxml", "expected_cxml"),

2,
'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p/w:r/w:t"a",' 'w:p/w:r/w:t"b"))',
'w:tr/(w:tc/(w:tcPr/w:gridSpan{w:val=2},w:p/w:r/w:t"a",w:p/w:r/w:t"b"))',
),

@@ -271,3 +270,3 @@ (

2,
"w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=2880,w:type=dxa}," "w:gridSpan{w:val=2}),w:p))",
"w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=2880,w:type=dxa},w:gridSpan{w:val=2}),w:p))",
),

@@ -283,3 +282,3 @@ # neither have a width

(
"w:tr/(w:tc/w:p," "w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p))",
"w:tr/(w:tc/w:p,w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p))",
0,

@@ -291,6 +290,6 @@ 2,

(
"w:tr/(w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p)," "w:tc/w:p)",
"w:tr/(w:tc/(w:tcPr/w:tcW{w:w=1440,w:type=dxa},w:p),w:tc/w:p)",
0,
2,
"w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=1440,w:type=dxa}," "w:gridSpan{w:val=2}),w:p))",
"w:tr/(w:tc/(w:tcPr/(w:tcW{w:w=1440,w:type=dxa},w:gridSpan{w:val=2}),w:p))",
),

@@ -297,0 +296,0 @@ ],

@@ -134,3 +134,3 @@ """Test suite for docx.oxml.xmlchemy."""

def pretty_fixture(self, element):
expected_xml_text = "<foø>\n" " <bår>text</bår>\n" "</foø>\n"
expected_xml_text = "<foø>\n <bår>text</bår>\n</foø>\n"
return element, expected_xml_text

@@ -180,4 +180,3 @@

(
'<dcterms:created xsi:type="dcterms:W3CDTF">2013-12-23T23:15:00Z</d'
"cterms:created>",
'<dcterms:created xsi:type="dcterms:W3CDTF">2013-12-23T23:15:00Z</dcterms:created>',
"<dcterms:created",

@@ -255,5 +254,3 @@ ' xsi:type="dcterms:W3CDTF"',

assert parent.xml == expected_xml
assert parent._insert_choice.__doc__.startswith(
"Return the passed ``<w:choice>`` "
)
assert parent._insert_choice.__doc__.startswith("Return the passed ``<w:choice>`` ")

@@ -265,9 +262,5 @@ def it_adds_an_add_method_for_the_child_element(self, add_fixture):

assert isinstance(choice, CT_Choice)
assert parent._add_choice.__doc__.startswith(
"Add a new ``<w:choice>`` child element "
)
assert parent._add_choice.__doc__.startswith("Add a new ``<w:choice>`` child element ")
def it_adds_a_get_or_change_to_method_for_the_child_element(
self, get_or_change_to_fixture
):
def it_adds_a_get_or_change_to_method_for_the_child_element(self, get_or_change_to_fixture):
parent, expected_xml = get_or_change_to_fixture

@@ -309,6 +302,3 @@ choice = parent.get_or_change_to_choice()

parent = (
a_parent()
.with_nsdecls()
.with_child(an_oomChild())
.with_child(an_oooChild())
a_parent().with_nsdecls().with_child(an_oomChild()).with_child(an_oooChild())
).element

@@ -370,5 +360,3 @@ choice = a_choice().with_nsdecls().element

assert parent.xml == expected_xml
assert parent._insert_oomChild.__doc__.startswith(
"Return the passed ``<w:oomChild>`` "
)
assert parent._insert_oomChild.__doc__.startswith("Return the passed ``<w:oomChild>`` ")

@@ -380,5 +368,3 @@ def it_adds_a_private_add_method_for_the_child_element(self, add_fixture):

assert isinstance(oomChild, CT_OomChild)
assert parent._add_oomChild.__doc__.startswith(
"Add a new ``<w:oomChild>`` child element "
)
assert parent._add_oomChild.__doc__.startswith("Add a new ``<w:oomChild>`` child element ")

@@ -390,5 +376,3 @@ def it_adds_a_public_add_method_for_the_child_element(self, add_fixture):

assert isinstance(oomChild, CT_OomChild)
assert parent._add_oomChild.__doc__.startswith(
"Add a new ``<w:oomChild>`` child element "
)
assert parent._add_oomChild.__doc__.startswith("Add a new ``<w:oomChild>`` child element ")

@@ -455,5 +439,3 @@ # fixtures -------------------------------------------------------

def it_adds_a_docstring_for_the_property(self):
assert CT_Parent.optAttr.__doc__.startswith(
"ST_IntegerType type-converted value of "
)
assert CT_Parent.optAttr.__doc__.startswith("ST_IntegerType type-converted value of ")

@@ -489,5 +471,3 @@ # fixtures -------------------------------------------------------

def it_adds_a_docstring_for_the_property(self):
assert CT_Parent.reqAttr.__doc__.startswith(
"ST_IntegerType type-converted value of "
)
assert CT_Parent.reqAttr.__doc__.startswith("ST_IntegerType type-converted value of ")

@@ -545,5 +525,3 @@ def it_raises_on_get_when_attribute_not_present(self):

assert parent.xml == expected_xml
assert parent._insert_zomChild.__doc__.startswith(
"Return the passed ``<w:zomChild>`` "
)
assert parent._insert_zomChild.__doc__.startswith("Return the passed ``<w:zomChild>`` ")

@@ -555,5 +533,3 @@ def it_adds_an_add_method_for_the_child_element(self, add_fixture):

assert isinstance(zomChild, CT_ZomChild)
assert parent._add_zomChild.__doc__.startswith(
"Add a new ``<w:zomChild>`` child element "
)
assert parent._add_zomChild.__doc__.startswith("Add a new ``<w:zomChild>`` child element ")

@@ -565,5 +541,3 @@ def it_adds_a_public_add_method_for_the_child_element(self, add_fixture):

assert isinstance(zomChild, CT_ZomChild)
assert parent._add_zomChild.__doc__.startswith(
"Add a new ``<w:zomChild>`` child element "
)
assert parent._add_zomChild.__doc__.startswith("Add a new ``<w:zomChild>`` child element ")

@@ -630,5 +604,3 @@ def it_removes_the_property_root_name_used_for_declaration(self):

assert isinstance(zooChild, CT_ZooChild)
assert parent._add_zooChild.__doc__.startswith(
"Add a new ``<w:zooChild>`` child element "
)
assert parent._add_zooChild.__doc__.startswith("Add a new ``<w:zooChild>`` child element ")

@@ -639,5 +611,3 @@ def it_adds_an_insert_method_for_the_child_element(self, insert_fixture):

assert parent.xml == expected_xml
assert parent._insert_zooChild.__doc__.startswith(
"Return the passed ``<w:zooChild>`` "
)
assert parent._insert_zooChild.__doc__.startswith("Return the passed ``<w:zooChild>`` ")

@@ -761,5 +731,3 @@ def it_adds_a_get_or_add_method_for_the_child_element(self, get_or_add_fixture):

)
oomChild = OneOrMore(
"w:oomChild", successors=("w:oooChild", "w:zomChild", "w:zooChild")
)
oomChild = OneOrMore("w:oomChild", successors=("w:oooChild", "w:zomChild", "w:zooChild"))
oooChild = OneAndOnlyOne("w:oooChild")

@@ -766,0 +734,0 @@ zomChild = ZeroOrMore("w:zomChild", successors=("w:zooChild",))

@@ -33,5 +33,3 @@ """Test suite for the docx.oxml.text.hyperlink module."""

)
def it_knows_whether_it_has_been_clicked_on_aka_visited(
self, cxml: str, expected_value: bool
):
def it_knows_whether_it_has_been_clicked_on_aka_visited(self, cxml: str, expected_value: bool):
hyperlink = cast(CT_Hyperlink, element(cxml))

@@ -38,0 +36,0 @@ assert hyperlink.history is expected_value

@@ -0,1 +1,3 @@

# pyright: reportPrivateUsage=false
"""Unit test suite for the docx.parts.document module."""

@@ -5,6 +7,10 @@

from docx.comments import Comments
from docx.enum.style import WD_STYLE_TYPE
from docx.opc.constants import CONTENT_TYPE as CT
from docx.opc.constants import RELATIONSHIP_TYPE as RT
from docx.opc.coreprops import CoreProperties
from docx.opc.packuri import PackURI
from docx.package import Package
from docx.parts.comments import CommentsPart
from docx.parts.document import DocumentPart

@@ -19,11 +25,22 @@ from docx.parts.hdrftr import FooterPart, HeaderPart

from ..oxml.parts.unitdata.document import a_body, a_document
from ..unitutil.mock import class_mock, instance_mock, method_mock, property_mock
from ..unitutil.cxml import element
from ..unitutil.mock import (
FixtureRequest,
Mock,
class_mock,
instance_mock,
method_mock,
property_mock,
)
class DescribeDocumentPart:
def it_can_add_a_footer_part(self, package_, FooterPart_, footer_part_, relate_to_):
def it_can_add_a_footer_part(
self, package_: Mock, FooterPart_: Mock, footer_part_: Mock, relate_to_: Mock
):
FooterPart_.new.return_value = footer_part_
relate_to_.return_value = "rId12"
document_part = DocumentPart(None, None, None, package_)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -37,6 +54,10 @@ footer_part, rId = document_part.add_footer_part()

def it_can_add_a_header_part(self, package_, HeaderPart_, header_part_, relate_to_):
def it_can_add_a_header_part(
self, package_: Mock, HeaderPart_: Mock, header_part_: Mock, relate_to_: Mock
):
HeaderPart_.new.return_value = header_part_
relate_to_.return_value = "rId7"
document_part = DocumentPart(None, None, None, package_)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -50,4 +71,6 @@ header_part, rId = document_part.add_header_part()

def it_can_drop_a_specified_header_part(self, drop_rel_):
document_part = DocumentPart(None, None, None, None)
def it_can_drop_a_specified_header_part(self, drop_rel_: Mock, package_: Mock):
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -59,7 +82,9 @@ document_part.drop_header_part("rId42")

def it_provides_access_to_a_footer_part_by_rId(
self, related_parts_prop_, related_parts_, footer_part_
self, related_parts_prop_: Mock, related_parts_: Mock, footer_part_: Mock, package_: Mock
):
related_parts_prop_.return_value = related_parts_
related_parts_.__getitem__.return_value = footer_part_
document_part = DocumentPart(None, None, None, None)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -72,7 +97,9 @@ footer_part = document_part.footer_part("rId9")

def it_provides_access_to_a_header_part_by_rId(
self, related_parts_prop_, related_parts_, header_part_
self, related_parts_prop_: Mock, related_parts_: Mock, header_part_: Mock, package_: Mock
):
related_parts_prop_.return_value = related_parts_
related_parts_.__getitem__.return_value = header_part_
document_part = DocumentPart(None, None, None, None)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -84,35 +111,73 @@ header_part = document_part.header_part("rId11")

def it_can_save_the_package_to_a_file(self, save_fixture):
document, file_ = save_fixture
document.save(file_)
document._package.save.assert_called_once_with(file_)
def it_can_save_the_package_to_a_file(self, package_: Mock):
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)
def it_provides_access_to_the_document_settings(self, settings_fixture):
document_part, settings_ = settings_fixture
settings = document_part.settings
assert settings is settings_
document_part.save("foobar.docx")
def it_provides_access_to_the_document_styles(self, styles_fixture):
document_part, styles_ = styles_fixture
styles = document_part.styles
assert styles is styles_
package_.save.assert_called_once_with("foobar.docx")
def it_provides_access_to_its_core_properties(self, core_props_fixture):
document_part, core_properties_ = core_props_fixture
core_properties = document_part.core_properties
assert core_properties is core_properties_
def it_provides_access_to_the_comments_added_to_the_document(
self, _comments_part_prop_: Mock, comments_part_: Mock, comments_: Mock, package_: Mock
):
comments_part_.comments = comments_
_comments_part_prop_.return_value = comments_part_
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)
assert document_part.comments is comments_
def it_provides_access_to_the_document_settings(
self, _settings_part_prop_: Mock, settings_part_: Mock, settings_: Mock, package_: Mock
):
settings_part_.settings = settings_
_settings_part_prop_.return_value = settings_part_
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)
assert document_part.settings is settings_
def it_provides_access_to_the_document_styles(
self, _styles_part_prop_: Mock, styles_part_: Mock, styles_: Mock, package_: Mock
):
styles_part_.styles = styles_
_styles_part_prop_.return_value = styles_part_
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)
assert document_part.styles is styles_
def it_provides_access_to_its_core_properties(self, package_: Mock, core_properties_: Mock):
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)
package_.core_properties = core_properties_
assert document_part.core_properties is core_properties_
def it_provides_access_to_the_inline_shapes_in_the_document(
self, inline_shapes_fixture
self, InlineShapes_: Mock, package_: Mock
):
document, InlineShapes_, body_elm = inline_shapes_fixture
inline_shapes = document.inline_shapes
InlineShapes_.assert_called_once_with(body_elm, document)
document_elm = element("w:document/w:body")
body_elm = document_elm[0]
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, document_elm, package_
)
inline_shapes = document_part.inline_shapes
InlineShapes_.assert_called_once_with(body_elm, document_part)
assert inline_shapes is InlineShapes_.return_value
def it_provides_access_to_the_numbering_part(
self, part_related_by_, numbering_part_
self, part_related_by_: Mock, numbering_part_: Mock, package_: Mock
):
part_related_by_.return_value = numbering_part_
document_part = DocumentPart(None, None, None, None)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -125,7 +190,14 @@ numbering_part = document_part.numbering_part

def and_it_creates_a_numbering_part_if_not_present(
self, part_related_by_, relate_to_, NumberingPart_, numbering_part_
self,
part_related_by_: Mock,
relate_to_: Mock,
NumberingPart_: Mock,
numbering_part_: Mock,
package_: Mock,
):
part_related_by_.side_effect = KeyError
NumberingPart_.new.return_value = numbering_part_
document_part = DocumentPart(None, None, None, None)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -138,6 +210,10 @@ numbering_part = document_part.numbering_part

def it_can_get_a_style_by_id(self, styles_prop_, styles_, style_):
def it_can_get_a_style_by_id(
self, styles_prop_: Mock, styles_: Mock, style_: Mock, package_: Mock
):
styles_prop_.return_value = styles_
styles_.get_by_id.return_value = style_
document_part = DocumentPart(None, None, None, None)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -149,6 +225,10 @@ style = document_part.get_style("BodyText", WD_STYLE_TYPE.PARAGRAPH)

def it_can_get_the_id_of_a_style(self, style_, styles_prop_, styles_):
def it_can_get_the_id_of_a_style(
self, style_: Mock, styles_prop_: Mock, styles_: Mock, package_: Mock
):
styles_prop_.return_value = styles_
styles_.get_style_id.return_value = "BodyCharacter"
document_part = DocumentPart(None, None, None, None)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -160,7 +240,42 @@ style_id = document_part.get_style_id(style_, WD_STYLE_TYPE.CHARACTER)

def it_provides_access_to_its_comments_part_to_help(
self, package_: Mock, part_related_by_: Mock, comments_part_: Mock
):
part_related_by_.return_value = comments_part_
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)
comments_part = document_part._comments_part
part_related_by_.assert_called_once_with(document_part, RT.COMMENTS)
assert comments_part is comments_part_
def and_it_creates_a_default_comments_part_if_not_present(
self,
package_: Mock,
part_related_by_: Mock,
CommentsPart_: Mock,
comments_part_: Mock,
relate_to_: Mock,
):
part_related_by_.side_effect = KeyError
CommentsPart_.default.return_value = comments_part_
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)
comments_part = document_part._comments_part
CommentsPart_.default.assert_called_once_with(package_)
relate_to_.assert_called_once_with(document_part, comments_part_, RT.COMMENTS)
assert comments_part is comments_part_
def it_provides_access_to_its_settings_part_to_help(
self, part_related_by_, settings_part_
self, part_related_by_: Mock, settings_part_: Mock, package_: Mock
):
part_related_by_.return_value = settings_part_
document_part = DocumentPart(None, None, None, None)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -173,7 +288,14 @@ settings_part = document_part._settings_part

def and_it_creates_a_default_settings_part_if_not_present(
self, package_, part_related_by_, SettingsPart_, settings_part_, relate_to_
self,
package_: Mock,
part_related_by_: Mock,
SettingsPart_: Mock,
settings_part_: Mock,
relate_to_: Mock,
):
part_related_by_.side_effect = KeyError
SettingsPart_.default.return_value = settings_part_
document_part = DocumentPart(None, None, None, package_)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -187,6 +309,8 @@ settings_part = document_part._settings_part

def it_provides_access_to_its_styles_part_to_help(
self, part_related_by_, styles_part_
self, part_related_by_: Mock, styles_part_: Mock, package_: Mock
):
part_related_by_.return_value = styles_part_
document_part = DocumentPart(None, None, None, None)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -199,7 +323,14 @@ styles_part = document_part._styles_part

def and_it_creates_a_default_styles_part_if_not_present(
self, package_, part_related_by_, StylesPart_, styles_part_, relate_to_
self,
package_: Mock,
part_related_by_: Mock,
StylesPart_: Mock,
styles_part_: Mock,
relate_to_: Mock,
):
part_related_by_.side_effect = KeyError
StylesPart_.default.return_value = styles_part_
document_part = DocumentPart(None, None, None, package_)
document_part = DocumentPart(
PackURI("/word/document.xml"), CT.WML_DOCUMENT, element("w:document"), package_
)

@@ -212,133 +343,114 @@ styles_part = document_part._styles_part

# fixtures -------------------------------------------------------
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def core_props_fixture(self, package_, core_properties_):
document_part = DocumentPart(None, None, None, package_)
package_.core_properties = core_properties_
return document_part, core_properties_
def comments_(self, request: FixtureRequest) -> Mock:
return instance_mock(request, Comments)
@pytest.fixture
def inline_shapes_fixture(self, request, InlineShapes_):
document_elm = (a_document().with_nsdecls().with_child(a_body())).element
body_elm = document_elm[0]
document = DocumentPart(None, None, document_elm, None)
return document, InlineShapes_, body_elm
def CommentsPart_(self, request: FixtureRequest) -> Mock:
return class_mock(request, "docx.parts.document.CommentsPart")
@pytest.fixture
def save_fixture(self, package_):
document_part = DocumentPart(None, None, None, package_)
file_ = "foobar.docx"
return document_part, file_
def comments_part_(self, request: FixtureRequest) -> Mock:
return instance_mock(request, CommentsPart)
@pytest.fixture
def settings_fixture(self, _settings_part_prop_, settings_part_, settings_):
document_part = DocumentPart(None, None, None, None)
_settings_part_prop_.return_value = settings_part_
settings_part_.settings = settings_
return document_part, settings_
def _comments_part_prop_(self, request: FixtureRequest) -> Mock:
return property_mock(request, DocumentPart, "_comments_part")
@pytest.fixture
def styles_fixture(self, _styles_part_prop_, styles_part_, styles_):
document_part = DocumentPart(None, None, None, None)
_styles_part_prop_.return_value = styles_part_
styles_part_.styles = styles_
return document_part, styles_
# fixture components ---------------------------------------------
@pytest.fixture
def core_properties_(self, request):
def core_properties_(self, request: FixtureRequest):
return instance_mock(request, CoreProperties)
@pytest.fixture
def drop_rel_(self, request):
def drop_rel_(self, request: FixtureRequest):
return method_mock(request, DocumentPart, "drop_rel", autospec=True)
@pytest.fixture
def FooterPart_(self, request):
def FooterPart_(self, request: FixtureRequest):
return class_mock(request, "docx.parts.document.FooterPart")
@pytest.fixture
def footer_part_(self, request):
def footer_part_(self, request: FixtureRequest):
return instance_mock(request, FooterPart)
@pytest.fixture
def HeaderPart_(self, request):
def HeaderPart_(self, request: FixtureRequest):
return class_mock(request, "docx.parts.document.HeaderPart")
@pytest.fixture
def header_part_(self, request):
def header_part_(self, request: FixtureRequest):
return instance_mock(request, HeaderPart)
@pytest.fixture
def InlineShapes_(self, request):
def InlineShapes_(self, request: FixtureRequest):
return class_mock(request, "docx.parts.document.InlineShapes")
@pytest.fixture
def NumberingPart_(self, request):
def NumberingPart_(self, request: FixtureRequest):
return class_mock(request, "docx.parts.document.NumberingPart")
@pytest.fixture
def numbering_part_(self, request):
def numbering_part_(self, request: FixtureRequest):
return instance_mock(request, NumberingPart)
@pytest.fixture
def package_(self, request):
def package_(self, request: FixtureRequest):
return instance_mock(request, Package)
@pytest.fixture
def part_related_by_(self, request):
def part_related_by_(self, request: FixtureRequest):
return method_mock(request, DocumentPart, "part_related_by")
@pytest.fixture
def relate_to_(self, request):
def relate_to_(self, request: FixtureRequest):
return method_mock(request, DocumentPart, "relate_to")
@pytest.fixture
def related_parts_(self, request):
def related_parts_(self, request: FixtureRequest):
return instance_mock(request, dict)
@pytest.fixture
def related_parts_prop_(self, request):
def related_parts_prop_(self, request: FixtureRequest):
return property_mock(request, DocumentPart, "related_parts")
@pytest.fixture
def SettingsPart_(self, request):
def SettingsPart_(self, request: FixtureRequest):
return class_mock(request, "docx.parts.document.SettingsPart")
@pytest.fixture
def settings_(self, request):
def settings_(self, request: FixtureRequest):
return instance_mock(request, Settings)
@pytest.fixture
def settings_part_(self, request):
def settings_part_(self, request: FixtureRequest):
return instance_mock(request, SettingsPart)
@pytest.fixture
def _settings_part_prop_(self, request):
def _settings_part_prop_(self, request: FixtureRequest):
return property_mock(request, DocumentPart, "_settings_part")
@pytest.fixture
def style_(self, request):
def style_(self, request: FixtureRequest):
return instance_mock(request, BaseStyle)
@pytest.fixture
def styles_(self, request):
def styles_(self, request: FixtureRequest):
return instance_mock(request, Styles)
@pytest.fixture
def StylesPart_(self, request):
def StylesPart_(self, request: FixtureRequest):
return class_mock(request, "docx.parts.document.StylesPart")
@pytest.fixture
def styles_part_(self, request):
def styles_part_(self, request: FixtureRequest):
return instance_mock(request, StylesPart)
@pytest.fixture
def styles_prop_(self, request):
def styles_prop_(self, request: FixtureRequest):
return property_mock(request, DocumentPart, "styles")
@pytest.fixture
def _styles_part_prop_(self, request):
def _styles_part_prop_(self, request: FixtureRequest):
return property_mock(request, DocumentPart, "_styles_part")

@@ -30,5 +30,3 @@ """Unit test suite for the docx.parts.hdrftr module."""

def it_can_create_a_new_footer_part(
self, package_, _default_footer_xml_, parse_xml_, _init_
):
def it_can_create_a_new_footer_part(self, package_, _default_footer_xml_, parse_xml_, _init_):
ftr = element("w:ftr")

@@ -99,5 +97,3 @@ package_.next_partname.return_value = "/word/footer24.xml"

def it_can_create_a_new_header_part(
self, package_, _default_header_xml_, parse_xml_, _init_
):
def it_can_create_a_new_header_part(self, package_, _default_header_xml_, parse_xml_, _init_):
hdr = element("w:hdr")

@@ -104,0 +100,0 @@ package_.next_partname.return_value = "/word/header42.xml"

@@ -27,5 +27,3 @@ """Unit test suite for docx.parts.image module."""

image_part_load_.assert_called_once_with(
partname_, content_type, blob_, package_
)
image_part_load_.assert_called_once_with(partname_, content_type, blob_, package_)
assert part is image_part_

@@ -36,5 +34,3 @@

_init_.assert_called_once_with(
ANY, partname_, image_.content_type, image_.blob, image_
)
_init_.assert_called_once_with(ANY, partname_, image_.content_type, image_.blob, image_)
assert isinstance(image_part, ImagePart)

@@ -41,0 +37,0 @@

@@ -27,5 +27,3 @@ """Test suite for the docx.parts.numbering module."""

@pytest.fixture
def num_defs_fixture(
self, _NumberingDefinitions_, numbering_elm_, numbering_definitions_
):
def num_defs_fixture(self, _NumberingDefinitions_, numbering_elm_, numbering_definitions_):
numbering_part = NumberingPart(None, None, numbering_elm_, None)

@@ -32,0 +30,0 @@ return (

@@ -17,5 +17,3 @@ """Unit test suite for the docx.parts.settings module"""

class DescribeSettingsPart:
def it_is_used_by_loader_to_construct_settings_part(
self, load_, package_, settings_part_
):
def it_is_used_by_loader_to_construct_settings_part(self, load_, package_, settings_part_):
partname, blob = "partname", "blob"

@@ -65,5 +63,3 @@ content_type = CT.WML_SETTINGS

def Settings_(self, request, settings_):
return class_mock(
request, "docx.parts.settings.Settings", return_value=settings_
)
return class_mock(request, "docx.parts.settings.Settings", return_value=settings_)

@@ -70,0 +66,0 @@ @pytest.fixture

@@ -33,5 +33,3 @@ """Unit test suite for the docx.parts.story module."""

def it_can_get_a_style_by_id_and_type(
self, _document_part_prop_, document_part_, style_
):
def it_can_get_a_style_by_id_and_type(self, _document_part_prop_, document_part_, style_):
style_id = "BodyText"

@@ -38,0 +36,0 @@ style_type = WD_STYLE_TYPE.PARAGRAPH

@@ -78,5 +78,3 @@ """Test suite for the docx.styles.style module."""

def _TableStyle_(self, request, table_style_):
return class_mock(
request, "docx.styles.style._TableStyle", return_value=table_style_
)
return class_mock(request, "docx.styles.style._TableStyle", return_value=table_style_)

@@ -533,13 +531,7 @@ @pytest.fixture

styles = element(
"w:styles/("
"w:style{w:type=paragraph,w:styleId=H},"
"w:style{w:type=paragraph,w:styleId=B})"
"w:styles/(w:style{w:type=paragraph,w:styleId=H},w:style{w:type=paragraph,w:styleId=B})"
)
style_elms = {"H": styles[0], "B": styles[1]}
style = ParagraphStyle(style_elms[style_name])
next_style = (
None
if next_style_name is None
else ParagraphStyle(style_elms[next_style_name])
)
next_style = ParagraphStyle(style_elms[next_style_name]) if next_style_name else None
expected_xml = xml(style_cxml)

@@ -546,0 +538,0 @@ return style, next_style, expected_xml

@@ -55,5 +55,3 @@ """Unit test suite for the docx.styles.styles module."""

styles._element.add_style_of_type.assert_called_once_with(
name_, style_type, builtin
)
styles._element.add_style_of_type.assert_called_once_with(name_, style_type, builtin)
StyleFactory_.assert_called_once_with(style_elm_)

@@ -114,5 +112,3 @@ assert style is style_

_get_style_id_from_name_.assert_called_once_with(
styles, "Style Name", style_type
)
_get_style_id_from_name_.assert_called_once_with(styles, "Style Name", style_type)
assert style_id == "StyleId"

@@ -137,5 +133,3 @@

def it_gets_a_style_id_from_a_name_to_help(
self, _getitem_, _get_style_id_from_style_, style_
):
def it_gets_a_style_id_from_a_name_to_help(self, _getitem_, _get_style_id_from_style_, style_):
style_name, style_type, style_id_ = "Foo Bar", 1, "FooBar"

@@ -179,5 +173,3 @@ _getitem_.return_value = style_

)
def add_fixture(
self, request, styles_elm_, _getitem_, style_elm_, StyleFactory_, style_
):
def add_fixture(self, request, styles_elm_, _getitem_, style_elm_, StyleFactory_, style_):
name, name_, style_type, builtin = request.param

@@ -214,4 +206,3 @@ styles = Styles(styles_elm_)

(
"w:styles/(w:style{w:type=table,w:default=1},w:style{w:type=table,w"
":default=1})",
"w:styles/(w:style{w:type=table,w:default=1},w:style{w:type=table,w:default=1})",
True,

@@ -395,5 +386,3 @@ WD_STYLE_TYPE.TABLE,

def LatentStyles_(self, request, latent_styles_):
return class_mock(
request, "docx.styles.styles.LatentStyles", return_value=latent_styles_
)
return class_mock(request, "docx.styles.styles.LatentStyles", return_value=latent_styles_)

@@ -400,0 +389,0 @@ @pytest.fixture

@@ -5,64 +5,53 @@ """Test suite for the docx.api module."""

import docx
from docx.api import Document
from docx.api import Document as DocumentFactoryFn
from docx.document import Document as DocumentCls
from docx.opc.constants import CONTENT_TYPE as CT
from .unitutil.mock import class_mock, function_mock, instance_mock
from .unitutil.mock import FixtureRequest, Mock, class_mock, function_mock, instance_mock
class DescribeDocument:
def it_opens_a_docx_file(self, open_fixture):
docx, Package_, document_ = open_fixture
document = Document(docx)
Package_.open.assert_called_once_with(docx)
assert document is document_
"""Unit-test suite for `docx.api.Document` factory function."""
def it_opens_the_default_docx_if_none_specified(self, default_fixture):
docx, Package_, document_ = default_fixture
document = Document()
Package_.open.assert_called_once_with(docx)
assert document is document_
def it_raises_on_not_a_Word_file(self, raise_fixture):
not_a_docx = raise_fixture
with pytest.raises(ValueError, match="file 'foobar.xlsx' is not a Word file,"):
Document(not_a_docx)
# fixtures -------------------------------------------------------
@pytest.fixture
def default_fixture(self, _default_docx_path_, Package_, document_):
docx = "barfoo.docx"
_default_docx_path_.return_value = docx
def it_opens_a_docx_file(self, Package_: Mock, document_: Mock):
document_part = Package_.open.return_value.main_document_part
document_part.document = document_
document_part.content_type = CT.WML_DOCUMENT_MAIN
return docx, Package_, document_
@pytest.fixture
def open_fixture(self, Package_, document_):
docx = "foobar.docx"
document = DocumentFactoryFn("foobar.docx")
Package_.open.assert_called_once_with("foobar.docx")
assert document is document_
def it_opens_the_default_docx_if_none_specified(
self, _default_docx_path_: Mock, Package_: Mock, document_: Mock
):
_default_docx_path_.return_value = "default-document.docx"
document_part = Package_.open.return_value.main_document_part
document_part.document = document_
document_part.content_type = CT.WML_DOCUMENT_MAIN
return docx, Package_, document_
@pytest.fixture
def raise_fixture(self, Package_):
not_a_docx = "foobar.xlsx"
document = DocumentFactoryFn()
Package_.open.assert_called_once_with("default-document.docx")
assert document is document_
def it_raises_on_not_a_Word_file(self, Package_: Mock):
Package_.open.return_value.main_document_part.content_type = "BOGUS"
return not_a_docx
# fixture components ---------------------------------------------
with pytest.raises(ValueError, match="file 'foobar.xlsx' is not a Word file,"):
DocumentFactoryFn("foobar.xlsx")
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def _default_docx_path_(self, request):
def _default_docx_path_(self, request: FixtureRequest):
return function_mock(request, "docx.api._default_docx_path")
@pytest.fixture
def document_(self, request):
return instance_mock(request, docx.document.Document)
def document_(self, request: FixtureRequest):
return instance_mock(request, DocumentCls)
@pytest.fixture
def Package_(self, request):
def Package_(self, request: FixtureRequest):
return class_mock(request, "docx.api.Package")

@@ -0,7 +1,15 @@

# pyright: reportPrivateUsage=false
"""Test suite for the docx.blkcntnr (block item container) module."""
from __future__ import annotations
from typing import cast
import pytest
from docx import Document
import docx
from docx.blkcntnr import BlockItemContainer
from docx.document import Document
from docx.oxml.document import CT_Body
from docx.shared import Inches

@@ -13,3 +21,3 @@ from docx.table import Table

from .unitutil.file import snippet_seq, test_file
from .unitutil.mock import call, instance_mock, method_mock
from .unitutil.mock import FixtureRequest, Mock, call, instance_mock, method_mock

@@ -20,6 +28,15 @@

def it_can_add_a_paragraph(self, add_paragraph_fixture, _add_paragraph_):
text, style, paragraph_, add_run_calls = add_paragraph_fixture
@pytest.mark.parametrize(
("text", "style"), [("", None), ("Foo", None), ("", "Bar"), ("Foo", "Bar")]
)
def it_can_add_a_paragraph(
self,
text: str,
style: str | None,
blkcntnr: BlockItemContainer,
_add_paragraph_: Mock,
paragraph_: Mock,
):
paragraph_.style = None
_add_paragraph_.return_value = paragraph_
blkcntnr = BlockItemContainer(None, None)

@@ -29,15 +46,17 @@ paragraph = blkcntnr.add_paragraph(text, style)

_add_paragraph_.assert_called_once_with(blkcntnr)
assert paragraph.add_run.call_args_list == add_run_calls
assert paragraph_.add_run.call_args_list == ([call(text)] if text else [])
assert paragraph.style == style
assert paragraph is paragraph_
def it_can_add_a_table(self, add_table_fixture):
blkcntnr, rows, cols, width, expected_xml = add_table_fixture
def it_can_add_a_table(self, blkcntnr: BlockItemContainer):
rows, cols, width = 2, 2, Inches(2)
table = blkcntnr.add_table(rows, cols, width)
assert isinstance(table, Table)
assert table._element.xml == expected_xml
assert table._element.xml == snippet_seq("new-tbl")[0]
assert table._parent is blkcntnr
def it_can_iterate_its_inner_content(self):
document = Document(test_file("blk-inner-content.docx"))
document = docx.Document(test_file("blk-inner-content.docx"))

@@ -61,65 +80,5 @@ inner_content = document.iter_inner_content()

def it_provides_access_to_the_paragraphs_it_contains(self, paragraphs_fixture):
# test len(), iterable, and indexed access
blkcntnr, expected_count = paragraphs_fixture
paragraphs = blkcntnr.paragraphs
assert len(paragraphs) == expected_count
count = 0
for idx, paragraph in enumerate(paragraphs):
assert isinstance(paragraph, Paragraph)
assert paragraphs[idx] is paragraph
count += 1
assert count == expected_count
def it_provides_access_to_the_tables_it_contains(self, tables_fixture):
# test len(), iterable, and indexed access
blkcntnr, expected_count = tables_fixture
tables = blkcntnr.tables
assert len(tables) == expected_count
count = 0
for idx, table in enumerate(tables):
assert isinstance(table, Table)
assert tables[idx] is table
count += 1
assert count == expected_count
def it_adds_a_paragraph_to_help(self, _add_paragraph_fixture):
blkcntnr, expected_xml = _add_paragraph_fixture
new_paragraph = blkcntnr._add_paragraph()
assert isinstance(new_paragraph, Paragraph)
assert new_paragraph._parent == blkcntnr
assert blkcntnr._element.xml == expected_xml
# fixtures -------------------------------------------------------
@pytest.fixture(
params=[
("", None),
("Foo", None),
("", "Bar"),
("Foo", "Bar"),
]
)
def add_paragraph_fixture(self, request, paragraph_):
text, style = request.param
paragraph_.style = None
add_run_calls = [call(text)] if text else []
return text, style, paragraph_, add_run_calls
@pytest.fixture
def _add_paragraph_fixture(self, request):
blkcntnr_cxml, after_cxml = "w:body", "w:body/w:p"
blkcntnr = BlockItemContainer(element(blkcntnr_cxml), None)
expected_xml = xml(after_cxml)
return blkcntnr, expected_xml
@pytest.fixture
def add_table_fixture(self):
blkcntnr = BlockItemContainer(element("w:body"), None)
rows, cols, width = 2, 2, Inches(2)
expected_xml = snippet_seq("new-tbl")[0]
return blkcntnr, rows, cols, width, expected_xml
@pytest.fixture(
params=[
@pytest.mark.parametrize(
("blkcntnr_cxml", "expected_count"),
[
("w:body", 0),

@@ -130,11 +89,21 @@ ("w:body/w:p", 1),

("w:body/(w:p,w:tbl,w:p)", 2),
]
],
)
def paragraphs_fixture(self, request):
blkcntnr_cxml, expected_count = request.param
blkcntnr = BlockItemContainer(element(blkcntnr_cxml), None)
return blkcntnr, expected_count
def it_provides_access_to_the_paragraphs_it_contains(
self, blkcntnr_cxml: str, expected_count: int, document_: Mock
):
blkcntnr = BlockItemContainer(cast(CT_Body, element(blkcntnr_cxml)), document_)
@pytest.fixture(
params=[
paragraphs = blkcntnr.paragraphs
# -- supports len() --
assert len(paragraphs) == expected_count
# -- is iterable --
assert all(isinstance(p, Paragraph) for p in paragraphs)
# -- is indexable --
assert all(p is paragraphs[idx] for idx, p in enumerate(paragraphs))
@pytest.mark.parametrize(
("blkcntnr_cxml", "expected_count"),
[
("w:body", 0),

@@ -145,17 +114,44 @@ ("w:body/w:tbl", 1),

("w:body/(w:tbl,w:tbl,w:p)", 2),
]
],
)
def tables_fixture(self, request):
blkcntnr_cxml, expected_count = request.param
blkcntnr = BlockItemContainer(element(blkcntnr_cxml), None)
return blkcntnr, expected_count
def it_provides_access_to_the_tables_it_contains(
self, blkcntnr_cxml: str, expected_count: int, document_: Mock
):
blkcntnr = BlockItemContainer(cast(CT_Body, element(blkcntnr_cxml)), document_)
# fixture components ---------------------------------------------
tables = blkcntnr.tables
# -- supports len() --
assert len(tables) == expected_count
# -- is iterable --
assert all(isinstance(t, Table) for t in tables)
# -- is indexable --
assert all(t is tables[idx] for idx, t in enumerate(tables))
def it_adds_a_paragraph_to_help(self, document_: Mock):
blkcntnr = BlockItemContainer(cast(CT_Body, element("w:body")), document_)
new_paragraph = blkcntnr._add_paragraph()
assert isinstance(new_paragraph, Paragraph)
assert new_paragraph._parent == blkcntnr
assert blkcntnr._element.xml == xml("w:body/w:p")
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def _add_paragraph_(self, request):
def _add_paragraph_(self, request: FixtureRequest):
return method_mock(request, BlockItemContainer, "_add_paragraph")
@pytest.fixture
def paragraph_(self, request):
def blkcntnr(self, document_: Mock):
blkcntnr_elm = cast(CT_Body, element("w:body"))
return BlockItemContainer(blkcntnr_elm, document_)
@pytest.fixture
def document_(self, request: FixtureRequest):
return instance_mock(request, Document)
@pytest.fixture
def paragraph_(self, request: FixtureRequest):
return instance_mock(request, Paragraph)

@@ -12,2 +12,3 @@ # pyright: reportPrivateUsage=false

from docx.comments import Comment, Comments
from docx.document import Document, _Body

@@ -17,3 +18,3 @@ from docx.enum.section import WD_SECTION

from docx.opc.coreprops import CoreProperties
from docx.oxml.document import CT_Document
from docx.oxml.document import CT_Body, CT_Document
from docx.parts.document import DocumentPart

@@ -30,12 +31,42 @@ from docx.section import Section, Sections

from .unitutil.cxml import element, xml
from .unitutil.mock import Mock, class_mock, instance_mock, method_mock, property_mock
from .unitutil.mock import (
FixtureRequest,
Mock,
class_mock,
instance_mock,
method_mock,
property_mock,
)
class DescribeDocument:
"""Unit-test suite for `docx.Document`."""
"""Unit-test suite for `docx.document.Document`."""
def it_can_add_a_heading(self, add_heading_fixture, add_paragraph_, paragraph_):
level, style = add_heading_fixture
def it_can_add_a_comment(
self,
document_part_: Mock,
comments_prop_: Mock,
comments_: Mock,
comment_: Mock,
run_mark_comment_range_: Mock,
):
comment_.comment_id = 42
comments_.add_comment.return_value = comment_
comments_prop_.return_value = comments_
document = Document(cast(CT_Document, element("w:document/w:body/w:p/w:r")), document_part_)
run = document.paragraphs[0].runs[0]
comment = document.add_comment(run, "Comment text.")
comments_.add_comment.assert_called_once_with("Comment text.", "", "")
run_mark_comment_range_.assert_called_once_with(run, run, 42)
assert comment is comment_
@pytest.mark.parametrize(
("level", "style"), [(0, "Title"), (1, "Heading 1"), (2, "Heading 2"), (9, "Heading 9")]
)
def it_can_add_a_heading(
self, level: int, style: str, document: Document, add_paragraph_: Mock, paragraph_: Mock
):
add_paragraph_.return_value = paragraph_
document = Document(None, None)

@@ -47,4 +78,3 @@ paragraph = document.add_heading("Spam vs. Bacon", level)

def it_raises_on_heading_level_out_of_range(self):
document = Document(None, None)
def it_raises_on_heading_level_out_of_range(self, document: Document):
with pytest.raises(ValueError, match="level must be in range 0-9, got -1"):

@@ -55,6 +85,7 @@ document.add_heading(level=-1)

def it_can_add_a_page_break(self, add_paragraph_, paragraph_, run_):
def it_can_add_a_page_break(
self, document: Document, add_paragraph_: Mock, paragraph_: Mock, run_: Mock
):
add_paragraph_.return_value = paragraph_
paragraph_.add_run.return_value = run_
document = Document(None, None)

@@ -68,24 +99,66 @@ paragraph = document.add_page_break()

def it_can_add_a_paragraph(self, add_paragraph_fixture):
document, text, style, paragraph_ = add_paragraph_fixture
@pytest.mark.parametrize(
("text", "style"), [("", None), ("", "Heading 1"), ("foo\rbar", "Body Text")]
)
def it_can_add_a_paragraph(
self,
text: str,
style: str | None,
document: Document,
body_: Mock,
body_prop_: Mock,
paragraph_: Mock,
):
body_prop_.return_value = body_
body_.add_paragraph.return_value = paragraph_
paragraph = document.add_paragraph(text, style)
document._body.add_paragraph.assert_called_once_with(text, style)
body_.add_paragraph.assert_called_once_with(text, style)
assert paragraph is paragraph_
def it_can_add_a_picture(self, add_picture_fixture):
document, path, width, height, run_, picture_ = add_picture_fixture
def it_can_add_a_picture(
self, document: Document, add_paragraph_: Mock, run_: Mock, picture_: Mock
):
path, width, height = "foobar.png", 100, 200
add_paragraph_.return_value.add_run.return_value = run_
run_.add_picture.return_value = picture_
picture = document.add_picture(path, width, height)
run_.add_picture.assert_called_once_with(path, width, height)
assert picture is picture_
@pytest.mark.parametrize(
("sentinel_cxml", "start_type", "new_sentinel_cxml"),
[
("w:sectPr", WD_SECTION.EVEN_PAGE, "w:sectPr/w:type{w:val=evenPage}"),
(
"w:sectPr/w:type{w:val=evenPage}",
WD_SECTION.ODD_PAGE,
"w:sectPr/w:type{w:val=oddPage}",
),
("w:sectPr/w:type{w:val=oddPage}", WD_SECTION.NEW_PAGE, "w:sectPr"),
],
)
def it_can_add_a_section(
self, add_section_fixture, Section_, section_, document_part_
self,
sentinel_cxml: str,
start_type: WD_SECTION,
new_sentinel_cxml: str,
Section_: Mock,
section_: Mock,
document_part_: Mock,
):
document_elm, start_type, expected_xml = add_section_fixture
Section_.return_value = section_
document = Document(document_elm, document_part_)
document = Document(
cast(CT_Document, element("w:document/w:body/(w:p,%s)" % sentinel_cxml)),
document_part_,
)
section = document.add_section(start_type)
assert document.element.xml == expected_xml
assert document.element.xml == xml(
"w:document/w:body/(w:p,w:p/w:pPr/%s,%s)" % (sentinel_cxml, new_sentinel_cxml)
)
sectPr = document.element.xpath("w:body/w:sectPr")[0]

@@ -95,21 +168,48 @@ Section_.assert_called_once_with(sectPr, document_part_)

def it_can_add_a_table(self, add_table_fixture):
document, rows, cols, style, width, table_ = add_table_fixture
def it_can_add_a_table(
self,
document: Document,
_block_width_prop_: Mock,
body_prop_: Mock,
body_: Mock,
table_: Mock,
):
rows, cols, style = 4, 2, "Light Shading Accent 1"
body_prop_.return_value = body_
body_.add_table.return_value = table_
_block_width_prop_.return_value = width = 42
table = document.add_table(rows, cols, style)
document._body.add_table.assert_called_once_with(rows, cols, width)
body_.add_table.assert_called_once_with(rows, cols, width)
assert table == table_
assert table.style == style
def it_can_save_the_document_to_a_file(self, save_fixture):
document, file_ = save_fixture
document.save(file_)
document._part.save.assert_called_once_with(file_)
def it_can_save_the_document_to_a_file(self, document_part_: Mock):
document = Document(cast(CT_Document, element("w:document")), document_part_)
def it_provides_access_to_its_core_properties(self, core_props_fixture):
document, core_properties_ = core_props_fixture
document.save("foobar.docx")
document_part_.save.assert_called_once_with("foobar.docx")
def it_provides_access_to_the_comments(self, document_part_: Mock, comments_: Mock):
document_part_.comments = comments_
document = Document(cast(CT_Document, element("w:document")), document_part_)
assert document.comments is comments_
def it_provides_access_to_its_core_properties(
self, document_part_: Mock, core_properties_: Mock
):
document_part_.core_properties = core_properties_
document = Document(cast(CT_Document, element("w:document")), document_part_)
core_properties = document.core_properties
assert core_properties is core_properties_
def it_provides_access_to_its_inline_shapes(self, inline_shapes_fixture):
document, inline_shapes_ = inline_shapes_fixture
def it_provides_access_to_its_inline_shapes(self, document_part_: Mock, inline_shapes_: Mock):
document_part_.inline_shapes = inline_shapes_
document = Document(cast(CT_Document, element("w:document")), document_part_)
assert document.inline_shapes is inline_shapes_

@@ -120,16 +220,20 @@

):
document_elm = cast(CT_Document, element("w:document"))
body_prop_.return_value = body_
body_.iter_inner_content.return_value = iter((1, 2, 3))
document = Document(document_elm, document_part_)
document = Document(cast(CT_Document, element("w:document")), document_part_)
assert list(document.iter_inner_content()) == [1, 2, 3]
def it_provides_access_to_its_paragraphs(self, paragraphs_fixture):
document, paragraphs_ = paragraphs_fixture
def it_provides_access_to_its_paragraphs(
self, document: Document, body_prop_: Mock, body_: Mock, paragraphs_: Mock
):
body_prop_.return_value = body_
body_.paragraphs = paragraphs_
paragraphs = document.paragraphs
assert paragraphs is paragraphs_
def it_provides_access_to_its_sections(self, document_part_, Sections_, sections_):
document_elm = element("w:document")
def it_provides_access_to_its_sections(
self, document_part_: Mock, Sections_: Mock, sections_: Mock
):
document_elm = cast(CT_Document, element("w:document"))
Sections_.return_value = sections_

@@ -143,96 +247,42 @@ document = Document(document_elm, document_part_)

def it_provides_access_to_its_settings(self, settings_fixture):
document, settings_ = settings_fixture
def it_provides_access_to_its_settings(self, document_part_: Mock, settings_: Mock):
document_part_.settings = settings_
document = Document(cast(CT_Document, element("w:document")), document_part_)
assert document.settings is settings_
def it_provides_access_to_its_styles(self, styles_fixture):
document, styles_ = styles_fixture
def it_provides_access_to_its_styles(self, document_part_: Mock, styles_: Mock):
document_part_.styles = styles_
document = Document(cast(CT_Document, element("w:document")), document_part_)
assert document.styles is styles_
def it_provides_access_to_its_tables(self, tables_fixture):
document, tables_ = tables_fixture
tables = document.tables
assert tables is tables_
def it_provides_access_to_its_tables(
self, document: Document, body_prop_: Mock, body_: Mock, tables_: Mock
):
body_prop_.return_value = body_
body_.tables = tables_
def it_provides_access_to_the_document_part(self, part_fixture):
document, part_ = part_fixture
assert document.part is part_
assert document.tables is tables_
def it_provides_access_to_the_document_body(self, body_fixture):
document, body_elm, _Body_, body_ = body_fixture
def it_provides_access_to_the_document_part(self, document_part_: Mock):
document = Document(cast(CT_Document, element("w:document")), document_part_)
assert document.part is document_part_
def it_provides_access_to_the_document_body(
self, _Body_: Mock, body_: Mock, document_part_: Mock
):
_Body_.return_value = body_
document_elm = cast(CT_Document, element("w:document/w:body"))
body_elm = document_elm[0]
document = Document(document_elm, document_part_)
body = document._body
_Body_.assert_called_once_with(body_elm, document)
assert body is body_
def it_determines_block_width_to_help(self, block_width_fixture):
document, expected_value = block_width_fixture
width = document._block_width
assert isinstance(width, Length)
assert width == expected_value
# fixtures -------------------------------------------------------
@pytest.fixture(
params=[
(0, "Title"),
(1, "Heading 1"),
(2, "Heading 2"),
(9, "Heading 9"),
]
)
def add_heading_fixture(self, request):
level, style = request.param
return level, style
@pytest.fixture(
params=[
("", None),
("", "Heading 1"),
("foo\rbar", "Body Text"),
]
)
def add_paragraph_fixture(self, request, body_prop_, paragraph_):
text, style = request.param
document = Document(None, None)
body_prop_.return_value.add_paragraph.return_value = paragraph_
return document, text, style, paragraph_
@pytest.fixture
def add_picture_fixture(self, request, add_paragraph_, run_, picture_):
document = Document(None, None)
path, width, height = "foobar.png", 100, 200
add_paragraph_.return_value.add_run.return_value = run_
run_.add_picture.return_value = picture_
return document, path, width, height, run_, picture_
@pytest.fixture(
params=[
("w:sectPr", WD_SECTION.EVEN_PAGE, "w:sectPr/w:type{w:val=evenPage}"),
(
"w:sectPr/w:type{w:val=evenPage}",
WD_SECTION.ODD_PAGE,
"w:sectPr/w:type{w:val=oddPage}",
),
("w:sectPr/w:type{w:val=oddPage}", WD_SECTION.NEW_PAGE, "w:sectPr"),
]
)
def add_section_fixture(self, request):
sentinel, start_type, new_sentinel = request.param
document_elm = element("w:document/w:body/(w:p,%s)" % sentinel)
expected_xml = xml(
"w:document/w:body/(w:p,w:p/w:pPr/%s,%s)" % (sentinel, new_sentinel)
)
return document_elm, start_type, expected_xml
@pytest.fixture
def add_table_fixture(self, _block_width_prop_, body_prop_, table_):
document = Document(None, None)
rows, cols, style = 4, 2, "Light Shading Accent 1"
body_prop_.return_value.add_table.return_value = table_
_block_width_prop_.return_value = width = 42
return document, rows, cols, style, width, table_
@pytest.fixture
def block_width_fixture(self, sections_prop_, section_):
document = Document(None, None)
def it_determines_block_width_to_help(
self, document: Document, sections_prop_: Mock, section_: Mock
):
sections_prop_.return_value = [None, section_]

@@ -242,143 +292,113 @@ section_.page_width = 6000

section_.right_margin = 1000
expected_value = 3500
return document, expected_value
@pytest.fixture
def body_fixture(self, _Body_, body_):
document_elm = element("w:document/w:body")
body_elm = document_elm[0]
document = Document(document_elm, None)
return document, body_elm, _Body_, body_
width = document._block_width
@pytest.fixture
def core_props_fixture(self, document_part_, core_properties_):
document = Document(None, document_part_)
document_part_.core_properties = core_properties_
return document, core_properties_
assert isinstance(width, Length)
assert width == 3500
@pytest.fixture
def inline_shapes_fixture(self, document_part_, inline_shapes_):
document = Document(None, document_part_)
document_part_.inline_shapes = inline_shapes_
return document, inline_shapes_
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def paragraphs_fixture(self, body_prop_, paragraphs_):
document = Document(None, None)
body_prop_.return_value.paragraphs = paragraphs_
return document, paragraphs_
def add_paragraph_(self, request: FixtureRequest):
return method_mock(request, Document, "add_paragraph")
@pytest.fixture
def part_fixture(self, document_part_):
document = Document(None, document_part_)
return document, document_part_
def _Body_(self, request: FixtureRequest):
return class_mock(request, "docx.document._Body")
@pytest.fixture
def save_fixture(self, document_part_):
document = Document(None, document_part_)
file_ = "foobar.docx"
return document, file_
def body_(self, request: FixtureRequest):
return instance_mock(request, _Body)
@pytest.fixture
def settings_fixture(self, document_part_, settings_):
document = Document(None, document_part_)
document_part_.settings = settings_
return document, settings_
def _block_width_prop_(self, request: FixtureRequest):
return property_mock(request, Document, "_block_width")
@pytest.fixture
def styles_fixture(self, document_part_, styles_):
document = Document(None, document_part_)
document_part_.styles = styles_
return document, styles_
def body_prop_(self, request: FixtureRequest):
return property_mock(request, Document, "_body")
@pytest.fixture
def tables_fixture(self, body_prop_, tables_):
document = Document(None, None)
body_prop_.return_value.tables = tables_
return document, tables_
def comment_(self, request: FixtureRequest):
return instance_mock(request, Comment)
# fixture components ---------------------------------------------
@pytest.fixture
def add_paragraph_(self, request):
return method_mock(request, Document, "add_paragraph")
def comments_(self, request: FixtureRequest):
return instance_mock(request, Comments)
@pytest.fixture
def _Body_(self, request, body_):
return class_mock(request, "docx.document._Body", return_value=body_)
def comments_prop_(self, request: FixtureRequest):
return property_mock(request, Document, "comments")
@pytest.fixture
def body_(self, request):
return instance_mock(request, _Body)
def core_properties_(self, request: FixtureRequest):
return instance_mock(request, CoreProperties)
@pytest.fixture
def _block_width_prop_(self, request):
return property_mock(request, Document, "_block_width")
def document(self, document_part_: Mock) -> Document:
document_elm = cast(CT_Document, element("w:document"))
return Document(document_elm, document_part_)
@pytest.fixture
def body_prop_(self, request, body_):
return property_mock(request, Document, "_body", return_value=body_)
@pytest.fixture
def core_properties_(self, request):
return instance_mock(request, CoreProperties)
@pytest.fixture
def document_part_(self, request):
def document_part_(self, request: FixtureRequest):
return instance_mock(request, DocumentPart)
@pytest.fixture
def inline_shapes_(self, request):
def inline_shapes_(self, request: FixtureRequest):
return instance_mock(request, InlineShapes)
@pytest.fixture
def paragraph_(self, request):
def paragraph_(self, request: FixtureRequest):
return instance_mock(request, Paragraph)
@pytest.fixture
def paragraphs_(self, request):
def paragraphs_(self, request: FixtureRequest):
return instance_mock(request, list)
@pytest.fixture
def picture_(self, request):
def picture_(self, request: FixtureRequest):
return instance_mock(request, InlineShape)
@pytest.fixture
def run_(self, request):
def run_(self, request: FixtureRequest):
return instance_mock(request, Run)
@pytest.fixture
def Section_(self, request):
def run_mark_comment_range_(self, request: FixtureRequest):
return method_mock(request, Run, "mark_comment_range")
@pytest.fixture
def Section_(self, request: FixtureRequest):
return class_mock(request, "docx.document.Section")
@pytest.fixture
def section_(self, request):
def section_(self, request: FixtureRequest):
return instance_mock(request, Section)
@pytest.fixture
def Sections_(self, request):
def Sections_(self, request: FixtureRequest):
return class_mock(request, "docx.document.Sections")
@pytest.fixture
def sections_(self, request):
def sections_(self, request: FixtureRequest):
return instance_mock(request, Sections)
@pytest.fixture
def sections_prop_(self, request):
def sections_prop_(self, request: FixtureRequest):
return property_mock(request, Document, "sections")
@pytest.fixture
def settings_(self, request):
def settings_(self, request: FixtureRequest):
return instance_mock(request, Settings)
@pytest.fixture
def styles_(self, request):
def styles_(self, request: FixtureRequest):
return instance_mock(request, Styles)
@pytest.fixture
def table_(self, request):
return instance_mock(request, Table, style="UNASSIGNED")
def table_(self, request: FixtureRequest):
return instance_mock(request, Table)
@pytest.fixture
def tables_(self, request):
def tables_(self, request: FixtureRequest):
return instance_mock(request, list)

@@ -388,12 +408,7 @@

class Describe_Body:
def it_can_clear_itself_of_all_content_it_holds(self, clear_fixture):
body, expected_xml = clear_fixture
_body = body.clear_content()
assert body._body.xml == expected_xml
assert _body is body
"""Unit-test suite for `docx.document._Body`."""
# fixtures -------------------------------------------------------
@pytest.fixture(
params=[
@pytest.mark.parametrize(
("cxml", "expected_cxml"),
[
("w:body", "w:body"),

@@ -403,8 +418,18 @@ ("w:body/w:p", "w:body"),

("w:body/(w:p, w:sectPr)", "w:body/w:sectPr"),
]
],
)
def clear_fixture(self, request):
before_cxml, after_cxml = request.param
body = _Body(element(before_cxml), None)
expected_xml = xml(after_cxml)
return body, expected_xml
def it_can_clear_itself_of_all_content_it_holds(
self, cxml: str, expected_cxml: str, document_: Mock
):
body = _Body(cast(CT_Body, element(cxml)), document_)
_body = body.clear_content()
assert body._body.xml == xml(expected_cxml)
assert _body is body
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def document_(self, request: FixtureRequest):
return instance_mock(request, Document)

@@ -63,5 +63,3 @@ """Test suite for docx.enum module, focused on base classes.

def but_it_raises_when_there_is_no_such_mapped_XML_value(self):
with pytest.raises(
ValueError, match="SomeXmlAttr has no XML mapping for 'baz'"
):
with pytest.raises(ValueError, match="SomeXmlAttr has no XML mapping for 'baz'"):
SomeXmlAttr.from_xml("baz")

@@ -68,0 +66,0 @@

@@ -0,3 +1,7 @@

# pyright: reportPrivateUsage=false
"""Unit test suite for docx.package module."""
from __future__ import annotations
import pytest

@@ -11,8 +15,17 @@

from .unitutil.file import docx_path
from .unitutil.mock import class_mock, instance_mock, method_mock, property_mock
from .unitutil.mock import (
FixtureRequest,
Mock,
class_mock,
instance_mock,
method_mock,
property_mock,
)
class DescribePackage:
"""Unit-test suite for `docx.package.Package`."""
def it_can_get_or_add_an_image_part_containing_a_specified_image(
self, image_parts_prop_, image_parts_, image_part_
self, image_parts_prop_: Mock, image_parts_: Mock, image_part_: Mock
):

@@ -30,6 +43,7 @@ image_parts_prop_.return_value = image_parts_

package = Package.open(docx_path("having-images"))
image_parts = package.image_parts
assert len(image_parts) == 3
for image_part in image_parts:
assert isinstance(image_part, ImagePart)
assert all(isinstance(p, ImagePart) for p in image_parts)

@@ -39,11 +53,11 @@ # fixture components ---------------------------------------------

@pytest.fixture
def image_part_(self, request):
def image_part_(self, request: FixtureRequest):
return instance_mock(request, ImagePart)
@pytest.fixture
def image_parts_(self, request):
def image_parts_(self, request: FixtureRequest):
return instance_mock(request, ImageParts)
@pytest.fixture
def image_parts_prop_(self, request):
def image_parts_prop_(self, request: FixtureRequest):
return property_mock(request, Package, "image_parts")

@@ -53,4 +67,10 @@

class DescribeImageParts:
"""Unit-test suite for `docx.package.Package`."""
def it_can_get_a_matching_image_part(
self, Image_, image_, _get_by_sha1_, image_part_
self,
Image_: Mock,
image_: Mock,
_get_by_sha1_: Mock,
image_part_: Mock,
):

@@ -69,3 +89,8 @@ Image_.from_file.return_value = image_

def but_it_adds_a_new_image_part_when_match_fails(
self, Image_, image_, _get_by_sha1_, _add_image_part_, image_part_
self,
Image_: Mock,
image_: Mock,
_get_by_sha1_: Mock,
_add_image_part_: Mock,
image_part_: Mock,
):

@@ -85,10 +110,35 @@ Image_.from_file.return_value = image_

def it_knows_the_next_available_image_partname(self, next_partname_fixture):
image_parts, ext, expected_partname = next_partname_fixture
assert image_parts._next_image_partname(ext) == expected_partname
@pytest.mark.parametrize(
("existing_partname_numbers", "expected_partname_number"),
[
((2, 3), 1),
((1, 3), 2),
((1, 2), 3),
],
)
def it_knows_the_next_available_image_partname(
self,
request: FixtureRequest,
existing_partname_numbers: tuple[int, int],
expected_partname_number: int,
):
image_parts = ImageParts()
for n in existing_partname_numbers:
image_parts.append(
instance_mock(request, ImagePart, partname=PackURI(f"/word/media/image{n}.png"))
)
def it_can_really_add_a_new_image_part(
self, _next_image_partname_, partname_, image_, ImagePart_, image_part_
next_partname = image_parts._next_image_partname("png")
assert next_partname == PackURI("/word/media/image%d.png" % expected_partname_number)
def it_can_add_a_new_image_part(
self,
_next_image_partname_: Mock,
image_: Mock,
ImagePart_: Mock,
image_part_: Mock,
):
_next_image_partname_.return_value = partname_
partname = PackURI("/word/media/image7.png")
_next_image_partname_.return_value = partname
ImagePart_.from_image.return_value = image_part_

@@ -99,3 +149,3 @@ image_parts = ImageParts()

ImagePart_.from_image.assert_called_once_with(image_, partname_)
ImagePart_.from_image.assert_called_once_with(image_, partname)
assert image_part in image_parts

@@ -106,52 +156,28 @@ assert image_part is image_part_

@pytest.fixture(params=[((2, 3), 1), ((1, 3), 2), ((1, 2), 3)])
def next_partname_fixture(self, request):
def image_part_with_partname_(n):
partname = image_partname(n)
return instance_mock(request, ImagePart, partname=partname)
def image_partname(n):
return PackURI("/word/media/image%d.png" % n)
existing_partname_numbers, expected_partname_number = request.param
image_parts = ImageParts()
for n in existing_partname_numbers:
image_part_ = image_part_with_partname_(n)
image_parts.append(image_part_)
ext = "png"
expected_image_partname = image_partname(expected_partname_number)
return image_parts, ext, expected_image_partname
# fixture components ---------------------------------------------
@pytest.fixture
def _add_image_part_(self, request):
def _add_image_part_(self, request: FixtureRequest):
return method_mock(request, ImageParts, "_add_image_part")
@pytest.fixture
def _get_by_sha1_(self, request):
def _get_by_sha1_(self, request: FixtureRequest):
return method_mock(request, ImageParts, "_get_by_sha1")
@pytest.fixture
def Image_(self, request):
def Image_(self, request: FixtureRequest):
return class_mock(request, "docx.package.Image")
@pytest.fixture
def image_(self, request):
def image_(self, request: FixtureRequest):
return instance_mock(request, Image)
@pytest.fixture
def ImagePart_(self, request):
def ImagePart_(self, request: FixtureRequest):
return class_mock(request, "docx.package.ImagePart")
@pytest.fixture
def image_part_(self, request):
def image_part_(self, request: FixtureRequest):
return instance_mock(request, ImagePart)
@pytest.fixture
def _next_image_partname_(self, request):
def _next_image_partname_(self, request: FixtureRequest):
return method_mock(request, ImageParts, "_next_image_partname")
@pytest.fixture
def partname_(self, request):
return instance_mock(request, PackURI)

@@ -68,5 +68,3 @@ # pyright: reportPrivateUsage=false

CT_Document,
element(
"w:document/w:body/(w:p/w:pPr/w:sectPr,w:p/w:pPr/w:sectPr,w:sectPr)"
),
element("w:document/w:body/(w:p/w:pPr/w:sectPr,w:p/w:pPr/w:sectPr,w:sectPr)"),
)

@@ -91,5 +89,3 @@ sectPrs = document_elm.xpath("//w:sectPr")

CT_Document,
element(
"w:document/w:body/(w:p/w:pPr/w:sectPr,w:p/w:pPr/w:sectPr,w:sectPr)"
),
element("w:document/w:body/(w:p/w:pPr/w:sectPr,w:p/w:pPr/w:sectPr,w:sectPr)"),
)

@@ -108,3 +104,3 @@ sectPrs = document_elm.xpath("//w:sectPr")

# fixture components ---------------------------------------------
# -- fixtures---------------------------------------------------------------------------------

@@ -176,5 +172,3 @@ @pytest.fixture

_Footer_.assert_called_once_with(
sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE
)
_Footer_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE)
assert footer is footer_

@@ -191,5 +185,3 @@

_Header_.assert_called_once_with(
sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE
)
_Header_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE)
assert header is header_

@@ -206,5 +198,3 @@

_Footer_.assert_called_once_with(
sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE
)
_Footer_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE)
assert footer is footer_

@@ -221,5 +211,3 @@

_Header_.assert_called_once_with(
sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE
)
_Header_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE)
assert header is header_

@@ -236,5 +224,3 @@

_Footer_.assert_called_once_with(
sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY
)
_Footer_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY)
assert footer is footer_

@@ -251,5 +237,3 @@

_Header_.assert_called_once_with(
sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY
)
_Header_.assert_called_once_with(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY)
assert header is header_

@@ -574,17 +558,13 @@

@pytest.mark.parametrize(
("has_definition", "expected_value"), [(False, True), (True, False)]
)
@pytest.mark.parametrize(("has_definition", "expected_value"), [(False, True), (True, False)])
def it_knows_when_its_linked_to_the_previous_header_or_footer(
self, has_definition: bool, expected_value: bool, _has_definition_prop_: Mock
self,
has_definition: bool,
expected_value: bool,
header: _BaseHeaderFooter,
_has_definition_prop_: Mock,
):
_has_definition_prop_.return_value = has_definition
header = _BaseHeaderFooter(
None, None, None # pyright: ignore[reportGeneralTypeIssues]
)
assert header.is_linked_to_previous is expected_value
is_linked = header.is_linked_to_previous
assert is_linked is expected_value
@pytest.mark.parametrize(

@@ -605,2 +585,3 @@ ("has_definition", "value", "drop_calls", "add_calls"),

add_calls: int,
header: _BaseHeaderFooter,
_has_definition_prop_: Mock,

@@ -611,5 +592,2 @@ _drop_definition_: Mock,

_has_definition_prop_.return_value = has_definition
header = _BaseHeaderFooter(
None, None, None # pyright: ignore[reportGeneralTypeIssues]
)

@@ -622,9 +600,6 @@ header.is_linked_to_previous = value

def it_provides_access_to_the_header_or_footer_part_for_BlockItemContainer(
self, _get_or_add_definition_: Mock, header_part_: Mock
self, header: _BaseHeaderFooter, _get_or_add_definition_: Mock, header_part_: Mock
):
# ---this override fulfills part of the BlockItemContainer subclass interface---
_get_or_add_definition_.return_value = header_part_
header = _BaseHeaderFooter(
None, None, None # pyright: ignore[reportGeneralTypeIssues]
)

@@ -637,3 +612,3 @@ header_part = header.part

def it_provides_access_to_the_hdr_or_ftr_element_to_help(
self, _get_or_add_definition_: Mock, header_part_: Mock
self, header: _BaseHeaderFooter, _get_or_add_definition_: Mock, header_part_: Mock
):

@@ -643,5 +618,2 @@ hdr = element("w:hdr")

header_part_.element = hdr
header = _BaseHeaderFooter(
None, None, None # pyright: ignore[reportGeneralTypeIssues]
)

@@ -654,9 +626,10 @@ hdr_elm = header._element

def it_gets_the_definition_when_it_has_one(
self, _has_definition_prop_: Mock, _definition_prop_: Mock, header_part_: Mock
self,
header: _BaseHeaderFooter,
_has_definition_prop_: Mock,
_definition_prop_: Mock,
header_part_: Mock,
):
_has_definition_prop_.return_value = True
_definition_prop_.return_value = header_part_
header = _BaseHeaderFooter(
None, None, None # pyright: ignore[reportGeneralTypeIssues]
)

@@ -669,2 +642,3 @@ header_part = header._get_or_add_definition()

self,
header: _BaseHeaderFooter,
_has_definition_prop_: Mock,

@@ -678,5 +652,2 @@ _prior_headerfooter_prop_: Mock,

prior_headerfooter_._get_or_add_definition.return_value = header_part_
header = _BaseHeaderFooter(
None, None, None # pyright: ignore[reportGeneralTypeIssues]
)

@@ -690,2 +661,3 @@ header_part = header._get_or_add_definition()

self,
header: _BaseHeaderFooter,
_has_definition_prop_: Mock,

@@ -699,5 +671,2 @@ _prior_headerfooter_prop_: Mock,

_add_definition_.return_value = header_part_
header = _BaseHeaderFooter(
None, None, None # pyright: ignore[reportGeneralTypeIssues]
)

@@ -720,2 +689,6 @@ header_part = header._get_or_add_definition()

@pytest.fixture
def document_part_(self, request: FixtureRequest):
return instance_mock(request, DocumentPart)
@pytest.fixture
def _drop_definition_(self, request: FixtureRequest):

@@ -733,2 +706,7 @@ return method_mock(request, _BaseHeaderFooter, "_drop_definition")

@pytest.fixture
def header(self, document_part_: Mock) -> _BaseHeaderFooter:
sectPr = cast(CT_SectPr, element("w:sectPr"))
return _BaseHeaderFooter(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY)
@pytest.fixture
def header_part_(self, request: FixtureRequest):

@@ -749,6 +727,4 @@ return instance_mock(request, HeaderPart)

def it_can_add_a_footer_part_to_help(
self, document_part_: Mock, footer_part_: Mock
):
sectPr = element("w:sectPr{r:a=b}")
def it_can_add_a_footer_part_to_help(self, document_part_: Mock, footer_part_: Mock):
sectPr = cast(CT_SectPr, element("w:sectPr{r:a=b}"))
document_part_.add_footer_part.return_value = footer_part_, "rId3"

@@ -760,5 +736,3 @@ footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY)

document_part_.add_footer_part.assert_called_once_with()
assert sectPr.xml == xml(
"w:sectPr{r:a=b}/w:footerReference{w:type=default,r:id=rId3}"
)
assert sectPr.xml == xml("w:sectPr{r:a=b}/w:footerReference{w:type=default,r:id=rId3}")
assert footer_part is footer_part_

@@ -769,3 +743,3 @@

):
sectPr = element("w:sectPr/w:footerReference{w:type=even,r:id=rId3}")
sectPr = cast(CT_SectPr, element("w:sectPr/w:footerReference{w:type=even,r:id=rId3}"))
document_part_.footer_part.return_value = footer_part_

@@ -780,3 +754,5 @@ footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE)

def it_can_drop_the_related_footer_part_to_help(self, document_part_: Mock):
sectPr = element("w:sectPr{r:a=b}/w:footerReference{w:type=first,r:id=rId42}")
sectPr = cast(
CT_SectPr, element("w:sectPr{r:a=b}/w:footerReference{w:type=first,r:id=rId42}")
)
footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE)

@@ -807,3 +783,3 @@

doc_elm = element("w:document/(w:sectPr,w:sectPr)")
prior_sectPr, sectPr = doc_elm[0], doc_elm[1]
prior_sectPr, sectPr = cast(CT_SectPr, doc_elm[0]), cast(CT_SectPr, doc_elm[1])
footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE)

@@ -815,11 +791,9 @@ # ---mock must occur after construction of "real" footer---

_Footer_.assert_called_once_with(
prior_sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE
)
_Footer_.assert_called_once_with(prior_sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE)
assert prior_footer is footer_
def but_it_returns_None_when_its_the_first_footer(self):
def but_it_returns_None_when_its_the_first_footer(self, document_part_: Mock):
doc_elm = cast(CT_Document, element("w:document/w:sectPr"))
sectPr = doc_elm[0]
footer = _Footer(sectPr, None, None)
sectPr = cast(CT_SectPr, doc_elm[0])
footer = _Footer(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY)

@@ -830,3 +804,3 @@ prior_footer = footer._prior_headerfooter

# -- fixtures ----------------------------------------------------
# -- fixtures---------------------------------------------------------------------------------

@@ -847,6 +821,6 @@ @pytest.fixture

class Describe_Header:
def it_can_add_a_header_part_to_help(
self, document_part_: Mock, header_part_: Mock
):
sectPr = element("w:sectPr{r:a=b}")
"""Unit-test suite for `docx.section._Header`."""
def it_can_add_a_header_part_to_help(self, document_part_: Mock, header_part_: Mock):
sectPr = cast(CT_SectPr, element("w:sectPr{r:a=b}"))
document_part_.add_header_part.return_value = header_part_, "rId3"

@@ -858,5 +832,3 @@ header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.FIRST_PAGE)

document_part_.add_header_part.assert_called_once_with()
assert sectPr.xml == xml(
"w:sectPr{r:a=b}/w:headerReference{w:type=first,r:id=rId3}"
)
assert sectPr.xml == xml("w:sectPr{r:a=b}/w:headerReference{w:type=first,r:id=rId3}")
assert header_part is header_part_

@@ -867,3 +839,3 @@

):
sectPr = element("w:sectPr/w:headerReference{w:type=default,r:id=rId8}")
sectPr = cast(CT_SectPr, element("w:sectPr/w:headerReference{w:type=default,r:id=rId8}"))
document_part_.header_part.return_value = header_part_

@@ -878,3 +850,5 @@ header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY)

def it_can_drop_the_related_header_part_to_help(self, document_part_: Mock):
sectPr = element("w:sectPr{r:a=b}/w:headerReference{w:type=even,r:id=rId42}")
sectPr = cast(
CT_SectPr, element("w:sectPr{r:a=b}/w:headerReference{w:type=even,r:id=rId42}")
)
header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.EVEN_PAGE)

@@ -902,6 +876,6 @@

def it_provides_access_to_the_prior_Header_to_help(
self, request, document_part_: Mock, header_: Mock
self, request: FixtureRequest, document_part_: Mock, header_: Mock
):
doc_elm = element("w:document/(w:sectPr,w:sectPr)")
prior_sectPr, sectPr = doc_elm[0], doc_elm[1]
prior_sectPr, sectPr = cast(CT_SectPr, doc_elm[0]), cast(CT_SectPr, doc_elm[1])
header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY)

@@ -913,11 +887,9 @@ # ---mock must occur after construction of "real" header---

_Header_.assert_called_once_with(
prior_sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY
)
_Header_.assert_called_once_with(prior_sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY)
assert prior_header is header_
def but_it_returns_None_when_its_the_first_header(self):
def but_it_returns_None_when_its_the_first_header(self, document_part_: Mock):
doc_elm = element("w:document/w:sectPr")
sectPr = doc_elm[0]
header = _Header(sectPr, None, None)
sectPr = cast(CT_SectPr, doc_elm[0])
header = _Header(sectPr, document_part_, WD_HEADER_FOOTER.PRIMARY)

@@ -928,3 +900,3 @@ prior_header = header._prior_headerfooter

# -- fixtures-----------------------------------------------------
# -- fixtures---------------------------------------------------------------------------------

@@ -931,0 +903,0 @@ @pytest.fixture

@@ -0,3 +1,7 @@

# pyright: reportPrivateUsage=false
"""Unit test suite for the docx.settings module."""
from __future__ import annotations
import pytest

@@ -11,26 +15,7 @@

class DescribeSettings:
def it_knows_when_the_document_has_distinct_odd_and_even_headers(
self, odd_and_even_get_fixture
):
settings_elm, expected_value = odd_and_even_get_fixture
settings = Settings(settings_elm)
"""Unit-test suite for the `docx.settings.Settings` objects."""
odd_and_even_pages_header_footer = settings.odd_and_even_pages_header_footer
assert odd_and_even_pages_header_footer is expected_value
def it_can_change_whether_the_document_has_distinct_odd_and_even_headers(
self, odd_and_even_set_fixture
):
settings_elm, value, expected_xml = odd_and_even_set_fixture
settings = Settings(settings_elm)
settings.odd_and_even_pages_header_footer = value
assert settings_elm.xml == expected_xml
# fixtures -------------------------------------------------------
@pytest.fixture(
params=[
@pytest.mark.parametrize(
("cxml", "expected_value"),
[
("w:settings", False),

@@ -41,25 +26,25 @@ ("w:settings/w:evenAndOddHeaders", True),

("w:settings/w:evenAndOddHeaders{w:val=true}", True),
]
],
)
def odd_and_even_get_fixture(self, request):
settings_cxml, expected_value = request.param
settings_elm = element(settings_cxml)
return settings_elm, expected_value
def it_knows_when_the_document_has_distinct_odd_and_even_headers(
self, cxml: str, expected_value: bool
):
assert Settings(element(cxml)).odd_and_even_pages_header_footer is expected_value
@pytest.fixture(
params=[
@pytest.mark.parametrize(
("cxml", "new_value", "expected_cxml"),
[
("w:settings", True, "w:settings/w:evenAndOddHeaders"),
("w:settings/w:evenAndOddHeaders", False, "w:settings"),
(
"w:settings/w:evenAndOddHeaders{w:val=1}",
True,
"w:settings/w:evenAndOddHeaders",
),
("w:settings/w:evenAndOddHeaders{w:val=1}", True, "w:settings/w:evenAndOddHeaders"),
("w:settings/w:evenAndOddHeaders{w:val=off}", False, "w:settings"),
]
],
)
def odd_and_even_set_fixture(self, request):
settings_cxml, value, expected_cxml = request.param
settings_elm = element(settings_cxml)
expected_xml = xml(expected_cxml)
return settings_elm, value, expected_xml
def it_can_change_whether_the_document_has_distinct_odd_and_even_headers(
self, cxml: str, new_value: bool, expected_cxml: str
):
settings = Settings(element(cxml))
settings.odd_and_even_pages_header_footer = new_value
assert settings._settings.xml == xml(expected_cxml)

@@ -0,194 +1,129 @@

# pyright: reportPrivateUsage=false
"""Test suite for the docx.shape module."""
from __future__ import annotations
from typing import cast
import pytest
from docx.document import Document
from docx.enum.shape import WD_INLINE_SHAPE
from docx.oxml.document import CT_Body
from docx.oxml.ns import nsmap
from docx.oxml.shape import CT_Inline
from docx.shape import InlineShape, InlineShapes
from docx.shared import Length
from docx.shared import Emu, Length
from .oxml.unitdata.dml import (
a_blip,
a_blipFill,
a_graphic,
a_graphicData,
a_pic,
an_inline,
)
from .unitutil.cxml import element, xml
from .unitutil.mock import loose_mock
from .unitutil.mock import FixtureRequest, Mock, instance_mock
class DescribeInlineShapes:
def it_knows_how_many_inline_shapes_it_contains(self, inline_shapes_fixture):
inline_shapes, expected_count = inline_shapes_fixture
assert len(inline_shapes) == expected_count
"""Unit-test suite for `docx.shape.InlineShapes` objects."""
def it_can_iterate_over_its_InlineShape_instances(self, inline_shapes_fixture):
inline_shapes, inline_shape_count = inline_shapes_fixture
actual_count = 0
for inline_shape in inline_shapes:
assert isinstance(inline_shape, InlineShape)
actual_count += 1
assert actual_count == inline_shape_count
def it_knows_how_many_inline_shapes_it_contains(self, body: CT_Body, document_: Mock):
inline_shapes = InlineShapes(body, document_)
assert len(inline_shapes) == 2
def it_provides_indexed_access_to_inline_shapes(self, inline_shapes_fixture):
inline_shapes, inline_shape_count = inline_shapes_fixture
for idx in range(-inline_shape_count, inline_shape_count):
inline_shape = inline_shapes[idx]
assert isinstance(inline_shape, InlineShape)
def it_can_iterate_over_its_InlineShape_instances(self, body: CT_Body, document_: Mock):
inline_shapes = InlineShapes(body, document_)
assert all(isinstance(s, InlineShape) for s in inline_shapes)
assert len(list(inline_shapes)) == 2
def it_raises_on_indexed_access_out_of_range(self, inline_shapes_fixture):
inline_shapes, inline_shape_count = inline_shapes_fixture
too_low = -1 - inline_shape_count
with pytest.raises(IndexError, match=r"inline shape index \[-3\] out of rang"):
inline_shapes[too_low]
too_high = inline_shape_count
def it_provides_indexed_access_to_inline_shapes(self, body: CT_Body, document_: Mock):
inline_shapes = InlineShapes(body, document_)
for idx in range(-2, 2):
assert isinstance(inline_shapes[idx], InlineShape)
def it_raises_on_indexed_access_out_of_range(self, body: CT_Body, document_: Mock):
inline_shapes = InlineShapes(body, document_)
with pytest.raises(IndexError, match=r"inline shape index \[-3\] out of range"):
inline_shapes[-3]
with pytest.raises(IndexError, match=r"inline shape index \[2\] out of range"):
inline_shapes[too_high]
inline_shapes[2]
def it_knows_the_part_it_belongs_to(self, inline_shapes_with_parent_):
inline_shapes, parent_ = inline_shapes_with_parent_
part = inline_shapes.part
assert part is parent_.part
def it_knows_the_part_it_belongs_to(self, body: CT_Body, document_: Mock):
inline_shapes = InlineShapes(body, document_)
assert inline_shapes.part is document_.part
# fixtures -------------------------------------------------------
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def inline_shapes_fixture(self):
body = element("w:body/w:p/(w:r/w:drawing/wp:inline, w:r/w:drawing/wp:inline)")
inline_shapes = InlineShapes(body, None)
expected_count = 2
return inline_shapes, expected_count
def body(self) -> CT_Body:
return cast(
CT_Body, element("w:body/w:p/(w:r/w:drawing/wp:inline, w:r/w:drawing/wp:inline)")
)
# fixture components ---------------------------------------------
@pytest.fixture
def inline_shapes_with_parent_(self, request):
parent_ = loose_mock(request, name="parent_")
inline_shapes = InlineShapes(None, parent_)
return inline_shapes, parent_
def document_(self, request: FixtureRequest):
return instance_mock(request, Document)
class DescribeInlineShape:
def it_knows_what_type_of_shape_it_is(self, shape_type_fixture):
inline_shape, inline_shape_type = shape_type_fixture
assert inline_shape.type == inline_shape_type
"""Unit-test suite for `docx.shape.InlineShape` objects."""
def it_knows_its_display_dimensions(self, dimensions_get_fixture):
inline_shape, cx, cy = dimensions_get_fixture
width = inline_shape.width
height = inline_shape.height
assert isinstance(width, Length)
assert width == cx
assert isinstance(height, Length)
assert height == cy
@pytest.mark.parametrize(
("uri", "content_cxml", "expected_value"),
[
# -- embedded picture --
(nsmap["pic"], "/pic:pic/pic:blipFill/a:blip{r:embed=rId1}", WD_INLINE_SHAPE.PICTURE),
# -- linked picture --
(
nsmap["pic"],
"/pic:pic/pic:blipFill/a:blip{r:link=rId2}",
WD_INLINE_SHAPE.LINKED_PICTURE,
),
# -- linked and embedded picture (not expected) --
(
nsmap["pic"],
"/pic:pic/pic:blipFill/a:blip{r:embed=rId1,r:link=rId2}",
WD_INLINE_SHAPE.LINKED_PICTURE,
),
# -- chart --
(nsmap["c"], "", WD_INLINE_SHAPE.CHART),
# -- SmartArt --
(nsmap["dgm"], "", WD_INLINE_SHAPE.SMART_ART),
# -- something else we don't know about --
("foobar", "", WD_INLINE_SHAPE.NOT_IMPLEMENTED),
],
)
def it_knows_what_type_of_shape_it_is(
self, uri: str, content_cxml: str, expected_value: WD_INLINE_SHAPE
):
cxml = "wp:inline/a:graphic/a:graphicData{uri=%s}%s" % (uri, content_cxml)
inline = cast(CT_Inline, element(cxml))
inline_shape = InlineShape(inline)
assert inline_shape.type == expected_value
def it_can_change_its_display_dimensions(self, dimensions_set_fixture):
inline_shape, cx, cy, expected_xml = dimensions_set_fixture
inline_shape.width = cx
inline_shape.height = cy
assert inline_shape._inline.xml == expected_xml
def it_knows_its_display_dimensions(self):
inline = cast(CT_Inline, element("wp:inline/wp:extent{cx=333, cy=666}"))
inline_shape = InlineShape(inline)
# fixtures -------------------------------------------------------
width, height = inline_shape.width, inline_shape.height
@pytest.fixture
def dimensions_get_fixture(self):
inline_cxml, expected_cx, expected_cy = (
"wp:inline/wp:extent{cx=333, cy=666}",
333,
666,
)
inline_shape = InlineShape(element(inline_cxml))
return inline_shape, expected_cx, expected_cy
assert isinstance(width, Length)
assert width == 333
assert isinstance(height, Length)
assert height == 666
@pytest.fixture
def dimensions_set_fixture(self):
inline_cxml, new_cx, new_cy, expected_cxml = (
"wp:inline/(wp:extent{cx=333,cy=666},a:graphic/a:graphicData/"
"pic:pic/pic:spPr/a:xfrm/a:ext{cx=333,cy=666})",
444,
888,
"wp:inline/(wp:extent{cx=444,cy=888},a:graphic/a:graphicData/"
"pic:pic/pic:spPr/a:xfrm/a:ext{cx=444,cy=888})",
def it_can_change_its_display_dimensions(self):
inline_shape = InlineShape(
cast(
CT_Inline,
element(
"wp:inline/(wp:extent{cx=333,cy=666},a:graphic/a:graphicData/pic:pic/"
"pic:spPr/a:xfrm/a:ext{cx=333,cy=666})"
),
)
)
inline_shape = InlineShape(element(inline_cxml))
expected_xml = xml(expected_cxml)
return inline_shape, new_cx, new_cy, expected_xml
@pytest.fixture(
params=[
"embed pic",
"link pic",
"link+embed pic",
"chart",
"smart art",
"not implemented",
]
)
def shape_type_fixture(self, request):
if request.param == "embed pic":
inline = self._inline_with_picture(embed=True)
shape_type = WD_INLINE_SHAPE.PICTURE
inline_shape.width = Emu(444)
inline_shape.height = Emu(888)
elif request.param == "link pic":
inline = self._inline_with_picture(link=True)
shape_type = WD_INLINE_SHAPE.LINKED_PICTURE
elif request.param == "link+embed pic":
inline = self._inline_with_picture(embed=True, link=True)
shape_type = WD_INLINE_SHAPE.LINKED_PICTURE
elif request.param == "chart":
inline = self._inline_with_uri(nsmap["c"])
shape_type = WD_INLINE_SHAPE.CHART
elif request.param == "smart art":
inline = self._inline_with_uri(nsmap["dgm"])
shape_type = WD_INLINE_SHAPE.SMART_ART
elif request.param == "not implemented":
inline = self._inline_with_uri("foobar")
shape_type = WD_INLINE_SHAPE.NOT_IMPLEMENTED
return InlineShape(inline), shape_type
# fixture components ---------------------------------------------
def _inline_with_picture(self, embed=False, link=False):
picture_ns = nsmap["pic"]
blip_bldr = a_blip()
if embed:
blip_bldr.with_embed("rId1")
if link:
blip_bldr.with_link("rId2")
inline = (
an_inline()
.with_nsdecls("wp", "r")
.with_child(
a_graphic()
.with_nsdecls()
.with_child(
a_graphicData()
.with_uri(picture_ns)
.with_child(
a_pic()
.with_nsdecls()
.with_child(a_blipFill().with_child(blip_bldr))
)
)
)
).element
return inline
def _inline_with_uri(self, uri):
inline = (
an_inline()
.with_nsdecls("wp")
.with_child(
a_graphic().with_nsdecls().with_child(a_graphicData().with_uri(uri))
)
).element
return inline
assert inline_shape._inline.xml == xml(
"wp:inline/(wp:extent{cx=444,cy=888},a:graphic/a:graphicData/pic:pic/pic:spPr/"
"a:xfrm/a:ext{cx=444,cy=888})"
)
"""Test suite for the docx.shared module."""
from __future__ import annotations
import pytest

@@ -9,9 +11,15 @@

from .unitutil.cxml import element
from .unitutil.mock import instance_mock
from .unitutil.mock import FixtureRequest, Mock, instance_mock
class DescribeElementProxy:
def it_knows_when_its_equal_to_another_proxy_object(self, eq_fixture):
proxy, proxy_2, proxy_3, not_a_proxy = eq_fixture
"""Unit-test suite for `docx.shared.ElementProxy` objects."""
def it_knows_when_its_equal_to_another_proxy_object(self):
p, q = element("w:p"), element("w:p")
proxy = ElementProxy(p)
proxy_2 = ElementProxy(p)
proxy_3 = ElementProxy(q)
not_a_proxy = "Foobar"
assert (proxy == proxy_2) is True

@@ -25,41 +33,20 @@ assert (proxy == proxy_3) is False

def it_knows_its_element(self, element_fixture):
proxy, element = element_fixture
assert proxy.element is element
def it_knows_its_part(self, part_fixture):
proxy, part_ = part_fixture
assert proxy.part is part_
# fixture --------------------------------------------------------
@pytest.fixture
def element_fixture(self):
def it_knows_its_element(self):
p = element("w:p")
proxy = ElementProxy(p)
return proxy, p
assert proxy.element is p
@pytest.fixture
def eq_fixture(self):
p, q = element("w:p"), element("w:p")
proxy = ElementProxy(p)
proxy_2 = ElementProxy(p)
proxy_3 = ElementProxy(q)
not_a_proxy = "Foobar"
return proxy, proxy_2, proxy_3, not_a_proxy
@pytest.fixture
def part_fixture(self, other_proxy_, part_):
def it_knows_its_part(self, other_proxy_: Mock, part_: Mock):
other_proxy_.part = part_
proxy = ElementProxy(None, other_proxy_)
return proxy, part_
proxy = ElementProxy(element("w:p"), other_proxy_)
assert proxy.part is part_
# fixture components ---------------------------------------------
# -- fixture ---------------------------------------------------------------------------------
@pytest.fixture
def other_proxy_(self, request):
def other_proxy_(self, request: FixtureRequest):
return instance_mock(request, ElementProxy)
@pytest.fixture
def part_(self, request):
def part_(self, request: FixtureRequest):
return instance_mock(request, XmlPart)

@@ -69,19 +56,7 @@

class DescribeLength:
def it_can_construct_from_convenient_units(self, construct_fixture):
UnitCls, units_val, emu = construct_fixture
length = UnitCls(units_val)
assert isinstance(length, Length)
assert length == emu
"""Unit-test suite for `docx.shared.Length` objects."""
def it_can_self_convert_to_convenient_units(self, units_fixture):
emu, units_prop_name, expected_length_in_units, type_ = units_fixture
length = Length(emu)
length_in_units = getattr(length, units_prop_name)
assert length_in_units == expected_length_in_units
assert isinstance(length_in_units, type_)
# fixtures -------------------------------------------------------
@pytest.fixture(
params=[
@pytest.mark.parametrize(
("UnitCls", "units_val", "emu"),
[
(Length, 914400, 914400),

@@ -94,28 +69,43 @@ (Inches, 1.1, 1005840),

(Twips, 360, 228600),
]
],
)
def construct_fixture(self, request):
UnitCls, units_val, emu = request.param
return UnitCls, units_val, emu
def it_can_construct_from_convenient_units(self, UnitCls: type, units_val: float, emu: int):
length = UnitCls(units_val)
assert isinstance(length, Length)
assert length == emu
@pytest.fixture(
params=[
(914400, "inches", 1.0, float),
(914400, "cm", 2.54, float),
(914400, "emu", 914400, int),
(914400, "mm", 25.4, float),
(914400, "pt", 72.0, float),
(914400, "twips", 1440, int),
]
@pytest.mark.parametrize(
("prop_name", "expected_value", "expected_type"),
[
("inches", 1.0, float),
("cm", 2.54, float),
("emu", 914400, int),
("mm", 25.4, float),
("pt", 72.0, float),
("twips", 1440, int),
],
)
def units_fixture(self, request):
emu, units_prop_name, expected_length_in_units, type_ = request.param
return emu, units_prop_name, expected_length_in_units, type_
def it_can_self_convert_to_convenient_units(
self, prop_name: str, expected_value: float, expected_type: type
):
# -- use an inch for the initial value --
length = Length(914400)
length_in_units = getattr(length, prop_name)
assert length_in_units == expected_value
assert isinstance(length_in_units, expected_type)
class DescribeRGBColor:
"""Unit-test suite for `docx.shared.RGBColor` objects."""
def it_is_natively_constructed_using_three_ints_0_to_255(self):
RGBColor(0x12, 0x34, 0x56)
with pytest.raises(ValueError, match=r"RGBColor\(\) takes three integer valu"):
RGBColor("12", "34", "56")
rgb_color = RGBColor(0x12, 0x34, 0x56)
assert isinstance(rgb_color, RGBColor)
# -- it is comparable to a tuple[int, int, int] --
assert rgb_color == (18, 52, 86)
def it_raises_with_helpful_error_message_on_wrong_types(self):
with pytest.raises(TypeError, match=r"RGBColor\(\) takes three integer valu"):
RGBColor("12", "34", "56") # pyright: ignore
with pytest.raises(ValueError, match=r"\(\) takes three integer values 0-255"):

@@ -131,3 +121,3 @@ RGBColor(-1, 34, 56)

def it_can_provide_a_hex_string_rgb_value(self):
assert str(RGBColor(0x12, 0x34, 0x56)) == "123456"
assert str(RGBColor(0xF3, 0x8A, 0x56)) == "F38A56"

@@ -134,0 +124,0 @@ def it_has_a_custom_repr(self):

@@ -65,5 +65,3 @@ # pyright: reportPrivateUsage=false

)
def it_can_change_its_typeface_name(
self, r_cxml: str, value: str, expected_r_cxml: str
):
def it_can_change_its_typeface_name(self, r_cxml: str, value: str, expected_r_cxml: str):
r = cast(CT_R, element(r_cxml))

@@ -99,5 +97,3 @@ font = Font(r)

)
def it_can_change_its_size(
self, r_cxml: str, value: Length | None, expected_r_cxml: str
):
def it_can_change_its_size(self, r_cxml: str, value: Length | None, expected_r_cxml: str):
r = cast(CT_R, element(r_cxml))

@@ -229,5 +225,3 @@ font = Font(r)

)
def it_knows_whether_it_is_subscript(
self, r_cxml: str, expected_value: bool | None
):
def it_knows_whether_it_is_subscript(self, r_cxml: str, expected_value: bool | None):
r = cast(CT_R, element(r_cxml))

@@ -289,5 +283,3 @@ font = Font(r)

)
def it_knows_whether_it_is_superscript(
self, r_cxml: str, expected_value: bool | None
):
def it_knows_whether_it_is_superscript(self, r_cxml: str, expected_value: bool | None):
r = cast(CT_R, element(r_cxml))

@@ -350,5 +342,3 @@ font = Font(r)

)
def it_knows_its_underline_type(
self, r_cxml: str, expected_value: WD_UNDERLINE | bool | None
):
def it_knows_its_underline_type(self, r_cxml: str, expected_value: WD_UNDERLINE | bool | None):
r = cast(CT_R, element(r_cxml))

@@ -401,5 +391,3 @@ font = Font(r)

)
def it_knows_its_highlight_color(
self, r_cxml: str, expected_value: WD_COLOR | None
):
def it_knows_its_highlight_color(self, r_cxml: str, expected_value: WD_COLOR | None):
r = cast(CT_R, element(r_cxml))

@@ -406,0 +394,0 @@ font = Font(r)

@@ -110,9 +110,3 @@ # pyright: reportPrivateUsage=false

):
p_cxml = (
"w:p/("
" w:pPr/w:ind"
' ,w:r/(w:t"foo",w:lastRenderedPageBreak,w:t"bar")'
' ,w:r/w:t"foo"'
")"
)
p_cxml = 'w:p/(w:pPr/w:ind,w:r/(w:t"foo",w:lastRenderedPageBreak,w:t"bar"),w:r/w:t"foo")'
p = cast(CT_P, element(p_cxml))

@@ -119,0 +113,0 @@ lrpb = p.lastRenderedPageBreaks[0]

@@ -88,5 +88,3 @@ """Unit test suite for the docx.text.paragraph module."""

style = paragraph.style
paragraph.part.get_style.assert_called_once_with(
style_id_, WD_STYLE_TYPE.PARAGRAPH
)
paragraph.part.get_style.assert_called_once_with(style_id_, WD_STYLE_TYPE.PARAGRAPH)
assert style is style_

@@ -99,5 +97,3 @@

paragraph.part.get_style_id.assert_called_once_with(
value, WD_STYLE_TYPE.PARAGRAPH
)
paragraph.part.get_style_id.assert_called_once_with(value, WD_STYLE_TYPE.PARAGRAPH)
assert paragraph._p.xml == expected_xml

@@ -113,4 +109,3 @@

(
"w:p/(w:r/w:lastRenderedPageBreak,"
"w:hyperlink/w:r/w:lastRenderedPageBreak)",
"w:p/(w:r/w:lastRenderedPageBreak,w:hyperlink/w:r/w:lastRenderedPageBreak)",
2,

@@ -150,4 +145,3 @@ ),

(
'w:p/(w:r/w:t"click ",w:hyperlink{r:id=rId6}/w:r/w:t"here",'
'w:r/w:t" for more")',
'w:p/(w:r/w:t"click ",w:hyperlink{r:id=rId6}/w:r/w:t"here",w:r/w:t" for more")',
"click here for more",

@@ -392,5 +386,3 @@ ),

run_, run_2_ = runs_
return class_mock(
request, "docx.text.paragraph.Run", side_effect=[run_, run_2_]
)
return class_mock(request, "docx.text.paragraph.Run", side_effect=[run_, run_2_])

@@ -397,0 +389,0 @@ @pytest.fixture

@@ -14,2 +14,3 @@ # pyright: reportPrivateUsage=false

from docx.enum.text import WD_BREAK, WD_UNDERLINE
from docx.oxml.text.paragraph import CT_P
from docx.oxml.text.run import CT_R

@@ -19,6 +20,7 @@ from docx.parts.document import DocumentPart

from docx.text.font import Font
from docx.text.paragraph import Paragraph
from docx.text.run import Run
from ..unitutil.cxml import element, xml
from ..unitutil.mock import class_mock, instance_mock, property_mock
from ..unitutil.mock import FixtureRequest, Mock, class_mock, instance_mock, property_mock

@@ -29,11 +31,54 @@

def it_knows_its_bool_prop_states(self, bool_prop_get_fixture):
run, prop_name, expected_state = bool_prop_get_fixture
assert getattr(run, prop_name) == expected_state
@pytest.mark.parametrize(
("r_cxml", "bool_prop_name", "expected_value"),
[
("w:r/w:rPr", "bold", None),
("w:r/w:rPr/w:b", "bold", True),
("w:r/w:rPr/w:b{w:val=on}", "bold", True),
("w:r/w:rPr/w:b{w:val=off}", "bold", False),
("w:r/w:rPr/w:b{w:val=1}", "bold", True),
("w:r/w:rPr/w:i{w:val=0}", "italic", False),
],
)
def it_knows_its_bool_prop_states(
self, r_cxml: str, bool_prop_name: str, expected_value: bool | None, paragraph_: Mock
):
run = Run(cast(CT_R, element(r_cxml)), paragraph_)
assert getattr(run, bool_prop_name) == expected_value
def it_can_change_its_bool_prop_settings(self, bool_prop_set_fixture):
run, prop_name, value, expected_xml = bool_prop_set_fixture
setattr(run, prop_name, value)
assert run._r.xml == expected_xml
@pytest.mark.parametrize(
("initial_r_cxml", "bool_prop_name", "value", "expected_cxml"),
[
# -- nothing to True, False, and None ---------------------------
("w:r", "bold", True, "w:r/w:rPr/w:b"),
("w:r", "bold", False, "w:r/w:rPr/w:b{w:val=0}"),
("w:r", "italic", None, "w:r/w:rPr"),
# -- default to True, False, and None ---------------------------
("w:r/w:rPr/w:b", "bold", True, "w:r/w:rPr/w:b"),
("w:r/w:rPr/w:b", "bold", False, "w:r/w:rPr/w:b{w:val=0}"),
("w:r/w:rPr/w:i", "italic", None, "w:r/w:rPr"),
# -- True to True, False, and None ------------------------------
("w:r/w:rPr/w:b{w:val=on}", "bold", True, "w:r/w:rPr/w:b"),
("w:r/w:rPr/w:b{w:val=1}", "bold", False, "w:r/w:rPr/w:b{w:val=0}"),
("w:r/w:rPr/w:b{w:val=1}", "bold", None, "w:r/w:rPr"),
# -- False to True, False, and None -----------------------------
("w:r/w:rPr/w:i{w:val=false}", "italic", True, "w:r/w:rPr/w:i"),
("w:r/w:rPr/w:i{w:val=0}", "italic", False, "w:r/w:rPr/w:i{w:val=0}"),
("w:r/w:rPr/w:i{w:val=off}", "italic", None, "w:r/w:rPr"),
],
)
def it_can_change_its_bool_prop_settings(
self,
initial_r_cxml: str,
bool_prop_name: str,
value: bool | None,
expected_cxml: str,
paragraph_: Mock,
):
run = Run(cast(CT_R, element(initial_r_cxml)), paragraph_)
setattr(run, bool_prop_name, value)
assert run._r.xml == xml(expected_cxml)
@pytest.mark.parametrize(

@@ -49,7 +94,5 @@ ("r_cxml", "expected_value"),

def it_knows_whether_it_contains_a_page_break(
self, r_cxml: str, expected_value: bool
self, r_cxml: str, expected_value: bool, paragraph_: Mock
):
r = cast(CT_R, element(r_cxml))
run = Run(r, None) # pyright: ignore[reportGeneralTypeIssues]
run = Run(cast(CT_R, element(r_cxml)), paragraph_)
assert run.contains_page_break == expected_value

@@ -87,44 +130,146 @@

def it_knows_its_character_style(self, style_get_fixture):
run, style_id_, style_ = style_get_fixture
def it_can_mark_a_comment_reference_range(self, paragraph_: Mock):
p = cast(CT_P, element('w:p/w:r/w:t"referenced text"'))
run = last_run = Run(p.r_lst[0], paragraph_)
run.mark_comment_range(last_run, comment_id=42)
assert p.xml == xml(
'w:p/(w:commentRangeStart{w:id=42},w:r/w:t"referenced text"'
",w:commentRangeEnd{w:id=42}"
",w:r/(w:rPr/w:rStyle{w:val=CommentReference},w:commentReference{w:id=42}))"
)
def it_knows_its_character_style(
self, part_prop_: Mock, document_part_: Mock, paragraph_: Mock
):
style_ = document_part_.get_style.return_value
part_prop_.return_value = document_part_
style_id = "Barfoo"
run = Run(cast(CT_R, element(f"w:r/w:rPr/w:rStyle{{w:val={style_id}}}")), paragraph_)
style = run.style
run.part.get_style.assert_called_once_with(style_id_, WD_STYLE_TYPE.CHARACTER)
document_part_.get_style.assert_called_once_with(style_id, WD_STYLE_TYPE.CHARACTER)
assert style is style_
def it_can_change_its_character_style(self, style_set_fixture):
run, value, expected_xml = style_set_fixture
@pytest.mark.parametrize(
("r_cxml", "value", "style_id", "expected_cxml"),
[
("w:r", "Foo Font", "FooFont", "w:r/w:rPr/w:rStyle{w:val=FooFont}"),
("w:r/w:rPr", "Foo Font", "FooFont", "w:r/w:rPr/w:rStyle{w:val=FooFont}"),
(
"w:r/w:rPr/w:rStyle{w:val=FooFont}",
"Bar Font",
"BarFont",
"w:r/w:rPr/w:rStyle{w:val=BarFont}",
),
("w:r/w:rPr/w:rStyle{w:val=FooFont}", None, None, "w:r/w:rPr"),
("w:r", None, None, "w:r/w:rPr"),
],
)
def it_can_change_its_character_style(
self,
r_cxml: str,
value: str | None,
style_id: str | None,
expected_cxml: str,
part_prop_: Mock,
paragraph_: Mock,
):
part_ = part_prop_.return_value
part_.get_style_id.return_value = style_id
run = Run(cast(CT_R, element(r_cxml)), paragraph_)
run.style = value
run.part.get_style_id.assert_called_once_with(value, WD_STYLE_TYPE.CHARACTER)
assert run._r.xml == expected_xml
def it_knows_its_underline_type(self, underline_get_fixture):
run, expected_value = underline_get_fixture
part_.get_style_id.assert_called_once_with(value, WD_STYLE_TYPE.CHARACTER)
assert run._r.xml == xml(expected_cxml)
@pytest.mark.parametrize(
("r_cxml", "expected_value"),
[
("w:r", None),
("w:r/w:rPr/w:u", None),
("w:r/w:rPr/w:u{w:val=single}", True),
("w:r/w:rPr/w:u{w:val=none}", False),
("w:r/w:rPr/w:u{w:val=double}", WD_UNDERLINE.DOUBLE),
("w:r/w:rPr/w:u{w:val=wave}", WD_UNDERLINE.WAVY),
],
)
def it_knows_its_underline_type(
self, r_cxml: str, expected_value: bool | WD_UNDERLINE | None, paragraph_: Mock
):
run = Run(cast(CT_R, element(r_cxml)), paragraph_)
assert run.underline is expected_value
def it_can_change_its_underline_type(self, underline_set_fixture):
run, underline, expected_xml = underline_set_fixture
run.underline = underline
assert run._r.xml == expected_xml
@pytest.mark.parametrize(
("initial_r_cxml", "new_underline", "expected_cxml"),
[
("w:r", True, "w:r/w:rPr/w:u{w:val=single}"),
("w:r", False, "w:r/w:rPr/w:u{w:val=none}"),
("w:r", None, "w:r/w:rPr"),
("w:r", WD_UNDERLINE.SINGLE, "w:r/w:rPr/w:u{w:val=single}"),
("w:r", WD_UNDERLINE.THICK, "w:r/w:rPr/w:u{w:val=thick}"),
("w:r/w:rPr/w:u{w:val=single}", True, "w:r/w:rPr/w:u{w:val=single}"),
("w:r/w:rPr/w:u{w:val=single}", False, "w:r/w:rPr/w:u{w:val=none}"),
("w:r/w:rPr/w:u{w:val=single}", None, "w:r/w:rPr"),
(
"w:r/w:rPr/w:u{w:val=single}",
WD_UNDERLINE.SINGLE,
"w:r/w:rPr/w:u{w:val=single}",
),
(
"w:r/w:rPr/w:u{w:val=single}",
WD_UNDERLINE.DOTTED,
"w:r/w:rPr/w:u{w:val=dotted}",
),
],
)
def it_can_change_its_underline_type(
self,
initial_r_cxml: str,
new_underline: bool | WD_UNDERLINE | None,
expected_cxml: str,
paragraph_: Mock,
):
run = Run(cast(CT_R, element(initial_r_cxml)), paragraph_)
run.underline = new_underline
assert run._r.xml == xml(expected_cxml)
@pytest.mark.parametrize("invalid_value", ["foobar", 42, "single"])
def it_raises_on_assign_invalid_underline_value(self, invalid_value: Any):
r = cast(CT_R, element("w:r/w:rPr"))
run = Run(r, None)
def it_raises_on_assign_invalid_underline_value(self, invalid_value: Any, paragraph_: Mock):
run = Run(cast(CT_R, element("w:r/w:rPr")), paragraph_)
with pytest.raises(ValueError, match=" is not a valid WD_UNDERLINE"):
run.underline = invalid_value
def it_provides_access_to_its_font(self, font_fixture):
run, Font_, font_ = font_fixture
def it_provides_access_to_its_font(self, Font_: Mock, font_: Mock, paragraph_: Mock):
Font_.return_value = font_
run = Run(cast(CT_R, element("w:r")), paragraph_)
font = run.font
Font_.assert_called_once_with(run._element)
assert font is font_
def it_can_add_text(self, add_text_fixture, Text_):
r, text_str, expected_xml = add_text_fixture
run = Run(r, None)
@pytest.mark.parametrize(
("r_cxml", "new_text", "expected_cxml"),
[
("w:r", "foo", 'w:r/w:t"foo"'),
('w:r/w:t"foo"', "bar", 'w:r/(w:t"foo", w:t"bar")'),
("w:r", "fo ", 'w:r/w:t{xml:space=preserve}"fo "'),
("w:r", "f o", 'w:r/w:t"f o"'),
],
)
def it_can_add_text(
self, r_cxml: str, new_text: str, expected_cxml: str, Text_: Mock, paragraph_: Mock
):
run = Run(cast(CT_R, element(r_cxml)), paragraph_)
_text = run.add_text(text_str)
text = run.add_text(new_text)
assert run._r.xml == expected_xml
assert _text is Text_.return_value
assert run._r.xml == xml(expected_cxml)
assert text is Text_.return_value

@@ -142,24 +287,38 @@ @pytest.mark.parametrize(

)
def it_can_add_a_break(self, break_type: WD_BREAK, expected_cxml: str):
r = cast(CT_R, element("w:r"))
run = Run(r, None) # pyright:ignore[reportGeneralTypeIssues]
expected_xml = xml(expected_cxml)
def it_can_add_a_break(self, break_type: WD_BREAK, expected_cxml: str, paragraph_: Mock):
run = Run(cast(CT_R, element("w:r")), paragraph_)
run.add_break(break_type)
assert run._r.xml == expected_xml
assert run._r.xml == xml(expected_cxml)
def it_can_add_a_tab(self, add_tab_fixture):
run, expected_xml = add_tab_fixture
@pytest.mark.parametrize(
("r_cxml", "expected_cxml"), [('w:r/w:t"foo"', 'w:r/(w:t"foo", w:tab)')]
)
def it_can_add_a_tab(self, r_cxml: str, expected_cxml: str, paragraph_: Mock):
run = Run(cast(CT_R, element(r_cxml)), paragraph_)
run.add_tab()
assert run._r.xml == expected_xml
def it_can_add_a_picture(self, add_picture_fixture):
run, image, width, height, inline = add_picture_fixture[:5]
expected_xml, InlineShape_, picture_ = add_picture_fixture[5:]
assert run._r.xml == xml(expected_cxml)
def it_can_add_a_picture(
self,
part_prop_: Mock,
document_part_: Mock,
InlineShape_: Mock,
picture_: Mock,
paragraph_: Mock,
):
part_prop_.return_value = document_part_
run = Run(cast(CT_R, element("w:r/wp:x")), paragraph_)
image = "foobar.png"
width, height, inline = 1111, 2222, element("wp:inline{id=42}")
document_part_.new_pic_inline.return_value = inline
InlineShape_.return_value = picture_
picture = run.add_picture(image, width, height)
run.part.new_pic_inline.assert_called_once_with(image, width, height)
assert run._r.xml == expected_xml
document_part_.new_pic_inline.assert_called_once_with(image, width, height)
assert run._r.xml == xml("w:r/(wp:x,w:drawing/wp:inline{id=42})")
InlineShape_.assert_called_once_with(inline)

@@ -183,11 +342,9 @@ assert picture is picture_

def it_can_remove_its_content_but_keep_formatting(
self, initial_r_cxml: str, expected_cxml: str
self, initial_r_cxml: str, expected_cxml: str, paragraph_: Mock
):
r = cast(CT_R, element(initial_r_cxml))
run = Run(r, None) # pyright: ignore[reportGeneralTypeIssues]
expected_xml = xml(expected_cxml)
run = Run(cast(CT_R, element(initial_r_cxml)), paragraph_)
cleared_run = run.clear()
assert run._r.xml == expected_xml
assert run._r.xml == xml(expected_cxml)
assert cleared_run is run

@@ -204,126 +361,9 @@

)
def it_knows_the_text_it_contains(self, r_cxml: str, expected_text: str):
r = cast(CT_R, element(r_cxml))
run = Run(r, None) # pyright: ignore[reportGeneralTypeIssues]
def it_knows_the_text_it_contains(self, r_cxml: str, expected_text: str, paragraph_: Mock):
run = Run(cast(CT_R, element(r_cxml)), paragraph_)
assert run.text == expected_text
def it_can_replace_the_text_it_contains(self, text_set_fixture):
run, text, expected_xml = text_set_fixture
run.text = text
assert run._r.xml == expected_xml
# fixtures -------------------------------------------------------
@pytest.fixture
def add_picture_fixture(self, part_prop_, document_part_, InlineShape_, picture_):
run = Run(element("w:r/wp:x"), None)
image = "foobar.png"
width, height, inline = 1111, 2222, element("wp:inline{id=42}")
expected_xml = xml("w:r/(wp:x,w:drawing/wp:inline{id=42})")
document_part_.new_pic_inline.return_value = inline
InlineShape_.return_value = picture_
return (run, image, width, height, inline, expected_xml, InlineShape_, picture_)
@pytest.fixture(
params=[
('w:r/w:t"foo"', 'w:r/(w:t"foo", w:tab)'),
]
)
def add_tab_fixture(self, request):
r_cxml, expected_cxml = request.param
run = Run(element(r_cxml), None)
expected_xml = xml(expected_cxml)
return run, expected_xml
@pytest.fixture(
params=[
("w:r", "foo", 'w:r/w:t"foo"'),
('w:r/w:t"foo"', "bar", 'w:r/(w:t"foo", w:t"bar")'),
("w:r", "fo ", 'w:r/w:t{xml:space=preserve}"fo "'),
("w:r", "f o", 'w:r/w:t"f o"'),
]
)
def add_text_fixture(self, request):
r_cxml, text, expected_cxml = request.param
r = element(r_cxml)
expected_xml = xml(expected_cxml)
return r, text, expected_xml
@pytest.fixture(
params=[
("w:r/w:rPr", "bold", None),
("w:r/w:rPr/w:b", "bold", True),
("w:r/w:rPr/w:b{w:val=on}", "bold", True),
("w:r/w:rPr/w:b{w:val=off}", "bold", False),
("w:r/w:rPr/w:b{w:val=1}", "bold", True),
("w:r/w:rPr/w:i{w:val=0}", "italic", False),
]
)
def bool_prop_get_fixture(self, request):
r_cxml, bool_prop_name, expected_value = request.param
run = Run(element(r_cxml), None)
return run, bool_prop_name, expected_value
@pytest.fixture(
params=[
# nothing to True, False, and None ---------------------------
("w:r", "bold", True, "w:r/w:rPr/w:b"),
("w:r", "bold", False, "w:r/w:rPr/w:b{w:val=0}"),
("w:r", "italic", None, "w:r/w:rPr"),
# default to True, False, and None ---------------------------
("w:r/w:rPr/w:b", "bold", True, "w:r/w:rPr/w:b"),
("w:r/w:rPr/w:b", "bold", False, "w:r/w:rPr/w:b{w:val=0}"),
("w:r/w:rPr/w:i", "italic", None, "w:r/w:rPr"),
# True to True, False, and None ------------------------------
("w:r/w:rPr/w:b{w:val=on}", "bold", True, "w:r/w:rPr/w:b"),
("w:r/w:rPr/w:b{w:val=1}", "bold", False, "w:r/w:rPr/w:b{w:val=0}"),
("w:r/w:rPr/w:b{w:val=1}", "bold", None, "w:r/w:rPr"),
# False to True, False, and None -----------------------------
("w:r/w:rPr/w:i{w:val=false}", "italic", True, "w:r/w:rPr/w:i"),
("w:r/w:rPr/w:i{w:val=0}", "italic", False, "w:r/w:rPr/w:i{w:val=0}"),
("w:r/w:rPr/w:i{w:val=off}", "italic", None, "w:r/w:rPr"),
]
)
def bool_prop_set_fixture(self, request):
initial_r_cxml, bool_prop_name, value, expected_cxml = request.param
run = Run(element(initial_r_cxml), None)
expected_xml = xml(expected_cxml)
return run, bool_prop_name, value, expected_xml
@pytest.fixture
def font_fixture(self, Font_, font_):
run = Run(element("w:r"), None)
return run, Font_, font_
@pytest.fixture
def style_get_fixture(self, part_prop_):
style_id = "Barfoo"
r_cxml = "w:r/w:rPr/w:rStyle{w:val=%s}" % style_id
run = Run(element(r_cxml), None)
style_ = part_prop_.return_value.get_style.return_value
return run, style_id, style_
@pytest.fixture(
params=[
("w:r", "Foo Font", "FooFont", "w:r/w:rPr/w:rStyle{w:val=FooFont}"),
("w:r/w:rPr", "Foo Font", "FooFont", "w:r/w:rPr/w:rStyle{w:val=FooFont}"),
(
"w:r/w:rPr/w:rStyle{w:val=FooFont}",
"Bar Font",
"BarFont",
"w:r/w:rPr/w:rStyle{w:val=BarFont}",
),
("w:r/w:rPr/w:rStyle{w:val=FooFont}", None, None, "w:r/w:rPr"),
("w:r", None, None, "w:r/w:rPr"),
]
)
def style_set_fixture(self, request, part_prop_):
r_cxml, value, style_id, expected_cxml = request.param
run = Run(element(r_cxml), None)
part_prop_.return_value.get_style_id.return_value = style_id
expected_xml = xml(expected_cxml)
return run, value, expected_xml
@pytest.fixture(
params=[
@pytest.mark.parametrize(
("new_text", "expected_cxml"),
[
("abc def", 'w:r/w:t"abc def"'),

@@ -333,82 +373,45 @@ ("abc\tdef", 'w:r/(w:t"abc", w:tab, w:t"def")'),

("abc\rdef", 'w:r/(w:t"abc", w:br, w:t"def")'),
]
],
)
def text_set_fixture(self, request):
new_text, expected_cxml = request.param
initial_r_cxml = 'w:r/w:t"should get deleted"'
run = Run(element(initial_r_cxml), None)
expected_xml = xml(expected_cxml)
return run, new_text, expected_xml
def it_can_replace_the_text_it_contains(
self, new_text: str, expected_cxml: str, paragraph_: Mock
):
run = Run(cast(CT_R, element('w:r/w:t"should get deleted"')), paragraph_)
@pytest.fixture(
params=[
("w:r", None),
("w:r/w:rPr/w:u", None),
("w:r/w:rPr/w:u{w:val=single}", True),
("w:r/w:rPr/w:u{w:val=none}", False),
("w:r/w:rPr/w:u{w:val=double}", WD_UNDERLINE.DOUBLE),
("w:r/w:rPr/w:u{w:val=wave}", WD_UNDERLINE.WAVY),
]
)
def underline_get_fixture(self, request):
r_cxml, expected_underline = request.param
run = Run(element(r_cxml), None)
return run, expected_underline
run.text = new_text
@pytest.fixture(
params=[
("w:r", True, "w:r/w:rPr/w:u{w:val=single}"),
("w:r", False, "w:r/w:rPr/w:u{w:val=none}"),
("w:r", None, "w:r/w:rPr"),
("w:r", WD_UNDERLINE.SINGLE, "w:r/w:rPr/w:u{w:val=single}"),
("w:r", WD_UNDERLINE.THICK, "w:r/w:rPr/w:u{w:val=thick}"),
("w:r/w:rPr/w:u{w:val=single}", True, "w:r/w:rPr/w:u{w:val=single}"),
("w:r/w:rPr/w:u{w:val=single}", False, "w:r/w:rPr/w:u{w:val=none}"),
("w:r/w:rPr/w:u{w:val=single}", None, "w:r/w:rPr"),
(
"w:r/w:rPr/w:u{w:val=single}",
WD_UNDERLINE.SINGLE,
"w:r/w:rPr/w:u{w:val=single}",
),
(
"w:r/w:rPr/w:u{w:val=single}",
WD_UNDERLINE.DOTTED,
"w:r/w:rPr/w:u{w:val=dotted}",
),
]
)
def underline_set_fixture(self, request):
initial_r_cxml, new_underline, expected_cxml = request.param
run = Run(element(initial_r_cxml), None)
expected_xml = xml(expected_cxml)
return run, new_underline, expected_xml
assert run._r.xml == xml(expected_cxml)
# fixture components ---------------------------------------------
# -- fixtures --------------------------------------------------------------------------------
@pytest.fixture
def document_part_(self, request):
def document_part_(self, request: FixtureRequest):
return instance_mock(request, DocumentPart)
@pytest.fixture
def Font_(self, request, font_):
return class_mock(request, "docx.text.run.Font", return_value=font_)
def Font_(self, request: FixtureRequest):
return class_mock(request, "docx.text.run.Font")
@pytest.fixture
def font_(self, request):
def font_(self, request: FixtureRequest):
return instance_mock(request, Font)
@pytest.fixture
def InlineShape_(self, request):
def InlineShape_(self, request: FixtureRequest):
return class_mock(request, "docx.text.run.InlineShape")
@pytest.fixture
def part_prop_(self, request, document_part_):
return property_mock(request, Run, "part", return_value=document_part_)
def paragraph_(self, request: FixtureRequest):
return instance_mock(request, Paragraph)
@pytest.fixture
def picture_(self, request):
def part_prop_(self, request: FixtureRequest):
return property_mock(request, Run, "part")
@pytest.fixture
def picture_(self, request: FixtureRequest):
return instance_mock(request, InlineShape)
@pytest.fixture
def Text_(self, request):
def Text_(self, request: FixtureRequest):
return class_mock(request, "docx.text.run._Text")

@@ -46,5 +46,3 @@ """Utility functions for loading files for unit testing."""

"""
snippet_file_path = os.path.join(
test_file_dir, "snippets", "%s.txt" % snippet_file_name
)
snippet_file_path = os.path.join(test_file_dir, "snippets", "%s.txt" % snippet_file_name)
with open(snippet_file_path, "rb") as f:

@@ -51,0 +49,0 @@ snippet_bytes = f.read()

@@ -78,5 +78,3 @@ """Utility functions wrapping the excellent `mock` library."""

def initializer_mock(
request: FixtureRequest, cls: type, autospec: bool = True, **kwargs: Any
):
def initializer_mock(request: FixtureRequest, cls: type, autospec: bool = True, **kwargs: Any):
"""Return mock for __init__() method on `cls`.

@@ -86,5 +84,3 @@

"""
_patch = patch.object(
cls, "__init__", autospec=autospec, return_value=None, **kwargs
)
_patch = patch.object(cls, "__init__", autospec=autospec, return_value=None, **kwargs)
request.addfinalizer(_patch.stop)

@@ -91,0 +87,0 @@ return _patch.start()

[tox]
envlist = py38, py39, py310, py311, py312
envlist = py39, py310, py311, py312, py313

@@ -4,0 +4,0 @@ [testenv]

"""Test data builders for DrawingML XML elements."""
from ...unitdata import BaseBuilder
class CT_BlipBuilder(BaseBuilder):
__tag__ = "a:blip"
__nspfxs__ = ("a",)
__attrs__ = ("r:embed", "r:link", "cstate")
class CT_BlipFillPropertiesBuilder(BaseBuilder):
__tag__ = "pic:blipFill"
__nspfxs__ = ("pic",)
__attrs__ = ()
class CT_GraphicalObjectBuilder(BaseBuilder):
__tag__ = "a:graphic"
__nspfxs__ = ("a",)
__attrs__ = ()
class CT_GraphicalObjectDataBuilder(BaseBuilder):
__tag__ = "a:graphicData"
__nspfxs__ = ("a",)
__attrs__ = ("uri",)
class CT_InlineBuilder(BaseBuilder):
__tag__ = "wp:inline"
__nspfxs__ = ("wp",)
__attrs__ = ("distT", "distB", "distL", "distR")
class CT_PictureBuilder(BaseBuilder):
__tag__ = "pic:pic"
__nspfxs__ = ("pic",)
__attrs__ = ()
def a_blip():
return CT_BlipBuilder()
def a_blipFill():
return CT_BlipFillPropertiesBuilder()
def a_graphic():
return CT_GraphicalObjectBuilder()
def a_graphicData():
return CT_GraphicalObjectDataBuilder()
def a_pic():
return CT_PictureBuilder()
def an_inline():
return CT_InlineBuilder()

Sorry, the diff of this file is not supported yet