python-docx
Advanced tools
Sorry, the diff of this file is not supported yet
| .. _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 |
+2
-0
@@ -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 @@ |
+8
-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 @@ ++++++++++++++++++ |
+4
-3
@@ -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 |
+22
-8
@@ -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 @@ |
+49
-37
| """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 @@ |
+63
-5
@@ -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 @@ |
+122
-348
@@ -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 @@ |
+12
-13
@@ -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 |
+13
-1
@@ -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 |
+69
-80
@@ -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 |
+212
-100
@@ -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 |
+28
-39
@@ -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") |
+84
-88
@@ -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) |
+235
-210
@@ -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 @@ |
+73
-47
@@ -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) |
+64
-92
@@ -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 |
+26
-41
@@ -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) |
+96
-161
@@ -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})" | ||
| ) |
+58
-68
| """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 |
+237
-234
@@ -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() |
+1
-1
| [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
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
8888489
1.43%514
3.42%28678
1.43%