Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit f246fde

Browse files
committedMay 1, 2024
rfctr: improve typing
1 parent e493474 commit f246fde

32 files changed

+501
-461
lines changed
 

‎features/steps/coreprops.py‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from datetime import datetime, timedelta
44

55
from behave import given, then, when
6+
from behave.runner import Context
67

78
from docx import Document
89
from docx.opc.coreprops import CoreProperties
@@ -13,25 +14,25 @@
1314

1415

1516
@given("a document having known core properties")
16-
def given_a_document_having_known_core_properties(context):
17+
def given_a_document_having_known_core_properties(context: Context):
1718
context.document = Document(test_docx("doc-coreprops"))
1819

1920

2021
@given("a document having no core properties part")
21-
def given_a_document_having_no_core_properties_part(context):
22+
def given_a_document_having_no_core_properties_part(context: Context):
2223
context.document = Document(test_docx("doc-no-coreprops"))
2324

2425

2526
# when ====================================================
2627

2728

2829
@when("I access the core properties object")
29-
def when_I_access_the_core_properties_object(context):
30+
def when_I_access_the_core_properties_object(context: Context):
3031
context.document.core_properties
3132

3233

3334
@when("I assign new values to the properties")
34-
def when_I_assign_new_values_to_the_properties(context):
35+
def when_I_assign_new_values_to_the_properties(context: Context):
3536
context.propvals = (
3637
("author", "Creator"),
3738
("category", "Category"),
@@ -58,7 +59,7 @@ def when_I_assign_new_values_to_the_properties(context):
5859

5960

6061
@then("a core properties part with default values is added")
61-
def then_a_core_properties_part_with_default_values_is_added(context):
62+
def then_a_core_properties_part_with_default_values_is_added(context: Context):
6263
core_properties = context.document.core_properties
6364
assert core_properties.title == "Word Document"
6465
assert core_properties.last_modified_by == "python-docx"
@@ -71,14 +72,14 @@ def then_a_core_properties_part_with_default_values_is_added(context):
7172

7273

7374
@then("I can access the core properties object")
74-
def then_I_can_access_the_core_properties_object(context):
75+
def then_I_can_access_the_core_properties_object(context: Context):
7576
document = context.document
7677
core_properties = document.core_properties
7778
assert isinstance(core_properties, CoreProperties)
7879

7980

8081
@then("the core property values match the known values")
81-
def then_the_core_property_values_match_the_known_values(context):
82+
def then_the_core_property_values_match_the_known_values(context: Context):
8283
known_propvals = (
8384
("author", "Steve Canny"),
8485
("category", "Category"),
@@ -106,7 +107,7 @@ def then_the_core_property_values_match_the_known_values(context):
106107

107108

108109
@then("the core property values match the new values")
109-
def then_the_core_property_values_match_the_new_values(context):
110+
def then_the_core_property_values_match_the_new_values(context: Context):
110111
core_properties = context.document.core_properties
111112
for name, expected_value in context.propvals:
112113
value = getattr(core_properties, name)

‎src/docx/enum/__init__.py‎

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +0,0 @@
1-
"""Enumerations used in python-docx."""
2-
3-
4-
class Enumeration:
5-
@classmethod
6-
def from_xml(cls, xml_val):
7-
return cls._xml_to_idx[xml_val]
8-
9-
@classmethod
10-
def to_xml(cls, enum_val):
11-
return cls._idx_to_xml[enum_val]

‎src/docx/image/image.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def height(self) -> Inches:
114114
return Inches(self.px_height / self.vert_dpi)
115115

116116
def scaled_dimensions(
117-
self, width: int | None = None, height: int | None = None
117+
self, width: int | Length | None = None, height: int | Length | None = None
118118
) -> Tuple[Length, Length]:
119119
"""(cx, cy) pair representing scaled dimensions of this image.
120120

‎src/docx/opc/coreprops.py‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,53 @@
33
These are broadly-standardized attributes like author, last-modified, etc.
44
"""
55

6+
from __future__ import annotations
7+
8+
from typing import TYPE_CHECKING
9+
10+
from docx.oxml.coreprops import CT_CoreProperties
11+
12+
if TYPE_CHECKING:
13+
from docx.oxml.coreprops import CT_CoreProperties
14+
615

716
class CoreProperties:
817
"""Corresponds to part named ``/docProps/core.xml``, containing the core document
918
properties for this document package."""
1019

11-
def __init__(self, element):
20+
def __init__(self, element: CT_CoreProperties):
1221
self._element = element
1322

1423
@property
1524
def author(self):
1625
return self._element.author_text
1726

1827
@author.setter
19-
def author(self, value):
28+
def author(self, value: str):
2029
self._element.author_text = value
2130

2231
@property
2332
def category(self):
2433
return self._element.category_text
2534

2635
@category.setter
27-
def category(self, value):
36+
def category(self, value: str):
2837
self._element.category_text = value
2938

3039
@property
3140
def comments(self):
3241
return self._element.comments_text
3342

3443
@comments.setter
35-
def comments(self, value):
44+
def comments(self, value: str):
3645
self._element.comments_text = value
3746

3847
@property
3948
def content_status(self):
4049
return self._element.contentStatus_text
4150

4251
@content_status.setter
43-
def content_status(self, value):
52+
def content_status(self, value: str):
4453
self._element.contentStatus_text = value
4554

4655
@property
@@ -56,31 +65,31 @@ def identifier(self):
5665
return self._element.identifier_text
5766

5867
@identifier.setter
59-
def identifier(self, value):
68+
def identifier(self, value: str):
6069
self._element.identifier_text = value
6170

6271
@property
6372
def keywords(self):
6473
return self._element.keywords_text
6574

6675
@keywords.setter
67-
def keywords(self, value):
76+
def keywords(self, value: str):
6877
self._element.keywords_text = value
6978

7079
@property
7180
def language(self):
7281
return self._element.language_text
7382

7483
@language.setter
75-
def language(self, value):
84+
def language(self, value: str):
7685
self._element.language_text = value
7786

7887
@property
7988
def last_modified_by(self):
8089
return self._element.lastModifiedBy_text
8190

8291
@last_modified_by.setter
83-
def last_modified_by(self, value):
92+
def last_modified_by(self, value: str):
8493
self._element.lastModifiedBy_text = value
8594

8695
@property
@@ -112,21 +121,21 @@ def subject(self):
112121
return self._element.subject_text
113122

114123
@subject.setter
115-
def subject(self, value):
124+
def subject(self, value: str):
116125
self._element.subject_text = value
117126

118127
@property
119128
def title(self):
120129
return self._element.title_text
121130

122131
@title.setter
123-
def title(self, value):
132+
def title(self, value: str):
124133
self._element.title_text = value
125134

126135
@property
127136
def version(self):
128137
return self._element.version_text
129138

130139
@version.setter
131-
def version(self, value):
140+
def version(self, value: str):
132141
self._element.version_text = value

‎src/docx/opc/oxml.py‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
deleted or only hold the package related custom element classes.
88
"""
99

10+
from __future__ import annotations
11+
12+
from typing import cast
13+
1014
from lxml import etree
1115

1216
from docx.opc.constants import NAMESPACE as NS
@@ -138,7 +142,7 @@ class CT_Relationship(BaseOxmlElement):
138142
target part."""
139143

140144
@staticmethod
141-
def new(rId, reltype, target, target_mode=RTM.INTERNAL):
145+
def new(rId: str, reltype: str, target: str, target_mode: str = RTM.INTERNAL):
142146
"""Return a new ``<Relationship>`` element."""
143147
xml = '<Relationship xmlns="%s"/>' % nsmap["pr"]
144148
relationship = parse_xml(xml)
@@ -178,19 +182,18 @@ def target_mode(self):
178182
class CT_Relationships(BaseOxmlElement):
179183
"""``<Relationships>`` element, the root element in a .rels file."""
180184

181-
def add_rel(self, rId, reltype, target, is_external=False):
185+
def add_rel(self, rId: str, reltype: str, target: str, is_external: bool = False):
182186
"""Add a child ``<Relationship>`` element with attributes set according to
183187
parameter values."""
184188
target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL
185189
relationship = CT_Relationship.new(rId, reltype, target, target_mode)
186190
self.append(relationship)
187191

188192
@staticmethod
189-
def new():
193+
def new() -> CT_Relationships:
190194
"""Return a new ``<Relationships>`` element."""
191195
xml = '<Relationships xmlns="%s"/>' % nsmap["pr"]
192-
relationships = parse_xml(xml)
193-
return relationships
196+
return cast(CT_Relationships, parse_xml(xml))
194197

195198
@property
196199
def Relationship_lst(self):

‎src/docx/opc/package.py‎

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

33
from __future__ import annotations
44

5-
from typing import IO, TYPE_CHECKING, Iterator
5+
from typing import IO, TYPE_CHECKING, Iterator, cast
66

77
from docx.opc.constants import RELATIONSHIP_TYPE as RT
88
from docx.opc.packuri import PACKAGE_URI, PackURI
@@ -14,7 +14,9 @@
1414
from docx.shared import lazyproperty
1515

1616
if TYPE_CHECKING:
17+
from docx.opc.coreprops import CoreProperties
1718
from docx.opc.part import Part
19+
from docx.opc.rel import _Relationship # pyright: ignore[reportPrivateUsage]
1820

1921

2022
class OpcPackage:
@@ -37,16 +39,18 @@ def after_unmarshal(self):
3739
pass
3840

3941
@property
40-
def core_properties(self):
42+
def core_properties(self) -> CoreProperties:
4143
"""|CoreProperties| object providing read/write access to the Dublin Core
4244
properties for this document."""
4345
return self._core_properties_part.core_properties
4446

45-
def iter_rels(self):
47+
def iter_rels(self) -> Iterator[_Relationship]:
4648
"""Generate exactly one reference to each relationship in the package by
4749
performing a depth-first traversal of the rels graph."""
4850

49-
def walk_rels(source, visited=None):
51+
def walk_rels(
52+
source: OpcPackage | Part, visited: list[Part] | None = None
53+
) -> Iterator[_Relationship]:
5054
visited = [] if visited is None else visited
5155
for rel in source.rels.values():
5256
yield rel
@@ -103,7 +107,7 @@ def main_document_part(self):
103107
"""
104108
return self.part_related_by(RT.OFFICE_DOCUMENT)
105109

106-
def next_partname(self, template):
110+
def next_partname(self, template: str) -> PackURI:
107111
"""Return a |PackURI| instance representing partname matching `template`.
108112
109113
The returned part-name has the next available numeric suffix to distinguish it
@@ -163,13 +167,13 @@ def save(self, pkg_file: str | IO[bytes]):
163167
PackageWriter.write(pkg_file, self.rels, self.parts)
164168

165169
@property
166-
def _core_properties_part(self):
170+
def _core_properties_part(self) -> CorePropertiesPart:
167171
"""|CorePropertiesPart| object related to this package.
168172
169173
Creates a default core properties part if one is not present (not common).
170174
"""
171175
try:
172-
return self.part_related_by(RT.CORE_PROPERTIES)
176+
return cast(CorePropertiesPart, self.part_related_by(RT.CORE_PROPERTIES))
173177
except KeyError:
174178
core_properties_part = CorePropertiesPart.default(self)
175179
self.relate_to(core_properties_part, RT.CORE_PROPERTIES)

‎src/docx/opc/packuri.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
Also some useful known pack URI strings such as PACKAGE_URI.
44
"""
55

6+
from __future__ import annotations
7+
68
import posixpath
79
import re
810

@@ -16,22 +18,21 @@ class PackURI(str):
1618

1719
_filename_re = re.compile("([a-zA-Z]+)([1-9][0-9]*)?")
1820

19-
def __new__(cls, pack_uri_str):
21+
def __new__(cls, pack_uri_str: str):
2022
if pack_uri_str[0] != "/":
2123
tmpl = "PackURI must begin with slash, got '%s'"
2224
raise ValueError(tmpl % pack_uri_str)
2325
return str.__new__(cls, pack_uri_str)
2426

2527
@staticmethod
26-
def from_rel_ref(baseURI, relative_ref):
27-
"""Return a |PackURI| instance containing the absolute pack URI formed by
28-
translating `relative_ref` onto `baseURI`."""
28+
def from_rel_ref(baseURI: str, relative_ref: str) -> PackURI:
29+
"""The absolute PackURI formed by translating `relative_ref` onto `baseURI`."""
2930
joined_uri = posixpath.join(baseURI, relative_ref)
3031
abs_uri = posixpath.abspath(joined_uri)
3132
return PackURI(abs_uri)
3233

3334
@property
34-
def baseURI(self):
35+
def baseURI(self) -> str:
3536
"""The base URI of this pack URI, the directory portion, roughly speaking.
3637
3738
E.g. ``'/ppt/slides'`` for ``'/ppt/slides/slide1.xml'``. For the package pseudo-
@@ -40,9 +41,8 @@ def baseURI(self):
4041
return posixpath.split(self)[0]
4142

4243
@property
43-
def ext(self):
44-
"""The extension portion of this pack URI, e.g. ``'xml'`` for
45-
``'/word/document.xml'``.
44+
def ext(self) -> str:
45+
"""The extension portion of this pack URI, e.g. ``'xml'`` for ``'/word/document.xml'``.
4646
4747
Note the period is not included.
4848
"""
@@ -84,7 +84,7 @@ def membername(self):
8484
"""
8585
return self[1:]
8686

87-
def relative_ref(self, baseURI):
87+
def relative_ref(self, baseURI: str):
8888
"""Return string containing relative reference to package item from `baseURI`.
8989
9090
E.g. PackURI('/ppt/slideLayouts/slideLayout1.xml') would return

‎src/docx/opc/part.py‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
# pyright: reportImportCycles=false
2+
13
"""Open Packaging Convention (OPC) objects related to package parts."""
24

35
from __future__ import annotations
46

5-
from typing import TYPE_CHECKING, Callable, Dict, Type, cast
7+
from typing import TYPE_CHECKING, Callable, Type, cast
68

79
from docx.opc.oxml import serialize_part_xml
810
from docx.opc.packuri import PackURI
@@ -12,6 +14,7 @@
1214
from docx.shared import lazyproperty
1315

1416
if TYPE_CHECKING:
17+
from docx.oxml.xmlchemy import BaseOxmlElement
1518
from docx.package import Package
1619

1720

@@ -24,7 +27,7 @@ class Part:
2427

2528
def __init__(
2629
self,
27-
partname: str,
30+
partname: PackURI,
2831
content_type: str,
2932
blob: bytes | None = None,
3033
package: Package | None = None,
@@ -56,13 +59,13 @@ def before_marshal(self):
5659
pass
5760

5861
@property
59-
def blob(self):
62+
def blob(self) -> bytes:
6063
"""Contents of this package part as a sequence of bytes.
6164
6265
May be text or binary. Intended to be overridden by subclasses. Default behavior
6366
is to return load blob.
6467
"""
65-
return self._blob
68+
return self._blob or b""
6669

6770
@property
6871
def content_type(self):
@@ -79,7 +82,7 @@ def drop_rel(self, rId: str):
7982
del self.rels[rId]
8083

8184
@classmethod
82-
def load(cls, partname: str, content_type: str, blob: bytes, package: Package):
85+
def load(cls, partname: PackURI, content_type: str, blob: bytes, package: Package):
8386
return cls(partname, content_type, blob, package)
8487

8588
def load_rel(self, reltype: str, target: Part | str, rId: str, is_external: bool = False):
@@ -105,7 +108,7 @@ def partname(self):
105108
return self._partname
106109

107110
@partname.setter
108-
def partname(self, partname):
111+
def partname(self, partname: str):
109112
if not isinstance(partname, PackURI):
110113
tmpl = "partname must be instance of PackURI, got '%s'"
111114
raise TypeError(tmpl % type(partname).__name__)
@@ -127,9 +130,9 @@ def relate_to(self, target: Part | str, reltype: str, is_external: bool = False)
127130
new relationship is created.
128131
"""
129132
if is_external:
130-
return self.rels.get_or_add_ext_rel(reltype, target)
133+
return self.rels.get_or_add_ext_rel(reltype, cast(str, target))
131134
else:
132-
rel = self.rels.get_or_add(reltype, target)
135+
rel = self.rels.get_or_add(reltype, cast(Part, target))
133136
return rel.rId
134137

135138
@property
@@ -171,12 +174,12 @@ class PartFactory:
171174
"""
172175

173176
part_class_selector: Callable[[str, str], Type[Part] | None] | None
174-
part_type_for: Dict[str, Type[Part]] = {}
177+
part_type_for: dict[str, Type[Part]] = {}
175178
default_part_type = Part
176179

177180
def __new__(
178181
cls,
179-
partname: str,
182+
partname: PackURI,
180183
content_type: str,
181184
reltype: str,
182185
blob: bytes,
@@ -206,7 +209,9 @@ class XmlPart(Part):
206209
reserializing the XML payload and managing relationships to other parts.
207210
"""
208211

209-
def __init__(self, partname, content_type, element, package):
212+
def __init__(
213+
self, partname: PackURI, content_type: str, element: BaseOxmlElement, package: Package
214+
):
210215
super(XmlPart, self).__init__(partname, content_type, package=package)
211216
self._element = element
212217

@@ -220,7 +225,7 @@ def element(self):
220225
return self._element
221226

222227
@classmethod
223-
def load(cls, partname, content_type, blob, package):
228+
def load(cls, partname: PackURI, content_type: str, blob: bytes, package: Package):
224229
element = parse_xml(blob)
225230
return cls(partname, content_type, element, package)
226231

‎src/docx/opc/parts/coreprops.py‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,37 @@
11
"""Core properties part, corresponds to ``/docProps/core.xml`` part in package."""
22

3-
from datetime import datetime
3+
from __future__ import annotations
4+
5+
import datetime as dt
6+
from typing import TYPE_CHECKING
47

58
from docx.opc.constants import CONTENT_TYPE as CT
69
from docx.opc.coreprops import CoreProperties
710
from docx.opc.packuri import PackURI
811
from docx.opc.part import XmlPart
912
from docx.oxml.coreprops import CT_CoreProperties
1013

14+
if TYPE_CHECKING:
15+
from docx.opc.package import OpcPackage
16+
1117

1218
class CorePropertiesPart(XmlPart):
13-
"""Corresponds to part named ``/docProps/core.xml``, containing the core document
14-
properties for this document package."""
19+
"""Corresponds to part named ``/docProps/core.xml``.
20+
21+
The "core" is short for "Dublin Core" and contains document metadata relatively common across
22+
documents of all types, not just DOCX.
23+
"""
1524

1625
@classmethod
17-
def default(cls, package):
26+
def default(cls, package: OpcPackage):
1827
"""Return a new |CorePropertiesPart| object initialized with default values for
1928
its base properties."""
2029
core_properties_part = cls._new(package)
2130
core_properties = core_properties_part.core_properties
2231
core_properties.title = "Word Document"
2332
core_properties.last_modified_by = "python-docx"
2433
core_properties.revision = 1
25-
core_properties.modified = datetime.utcnow()
34+
core_properties.modified = dt.datetime.utcnow()
2635
return core_properties_part
2736

2837
@property
@@ -32,7 +41,7 @@ def core_properties(self):
3241
return CoreProperties(self.element)
3342

3443
@classmethod
35-
def _new(cls, package):
44+
def _new(cls, package: OpcPackage) -> CorePropertiesPart:
3645
partname = PackURI("/docProps/core.xml")
3746
content_type = CT.OPC_CORE_PROPERTIES
3847
coreProperties = CT_CoreProperties.new()

‎src/docx/opc/rel.py‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, Any, Dict
5+
from typing import TYPE_CHECKING, Any, Dict, cast
66

77
from docx.opc.oxml import CT_Relationships
88

@@ -16,7 +16,7 @@ class Relationships(Dict[str, "_Relationship"]):
1616
def __init__(self, baseURI: str):
1717
super(Relationships, self).__init__()
1818
self._baseURI = baseURI
19-
self._target_parts_by_rId: Dict[str, Any] = {}
19+
self._target_parts_by_rId: dict[str, Any] = {}
2020

2121
def add_relationship(
2222
self, reltype: str, target: Part | str, rId: str, is_external: bool = False
@@ -37,7 +37,7 @@ def get_or_add(self, reltype: str, target_part: Part) -> _Relationship:
3737
rel = self.add_relationship(reltype, target_part, rId)
3838
return rel
3939

40-
def get_or_add_ext_rel(self, reltype, target_ref):
40+
def get_or_add_ext_rel(self, reltype: str, target_ref: str) -> str:
4141
"""Return rId of external relationship of `reltype` to `target_ref`, newly added
4242
if not already present in collection."""
4343
rel = self._get_matching(reltype, target_ref, is_external=True)
@@ -46,7 +46,7 @@ def get_or_add_ext_rel(self, reltype, target_ref):
4646
rel = self.add_relationship(reltype, target_ref, rId, is_external=True)
4747
return rel.rId
4848

49-
def part_with_reltype(self, reltype):
49+
def part_with_reltype(self, reltype: str) -> Part:
5050
"""Return target part of rel with matching `reltype`, raising |KeyError| if not
5151
found and |ValueError| if more than one matching relationship is found."""
5252
rel = self._get_rel_of_type(reltype)
@@ -59,7 +59,7 @@ def related_parts(self):
5959
return self._target_parts_by_rId
6060

6161
@property
62-
def xml(self):
62+
def xml(self) -> str:
6363
"""Serialize this relationship collection into XML suitable for storage as a
6464
.rels file in an OPC package."""
6565
rels_elm = CT_Relationships.new()
@@ -73,7 +73,7 @@ def _get_matching(
7373
"""Return relationship of matching `reltype`, `target`, and `is_external` from
7474
collection, or None if not found."""
7575

76-
def matches(rel, reltype, target, is_external):
76+
def matches(rel: _Relationship, reltype: str, target: Part | str, is_external: bool):
7777
if rel.reltype != reltype:
7878
return False
7979
if rel.is_external != is_external:
@@ -88,7 +88,7 @@ def matches(rel, reltype, target, is_external):
8888
return rel
8989
return None
9090

91-
def _get_rel_of_type(self, reltype):
91+
def _get_rel_of_type(self, reltype: str):
9292
"""Return single relationship of type `reltype` from the collection.
9393
9494
Raises |KeyError| if no matching relationship is found. Raises |ValueError| if
@@ -104,7 +104,7 @@ def _get_rel_of_type(self, reltype):
104104
return matching[0]
105105

106106
@property
107-
def _next_rId(self) -> str:
107+
def _next_rId(self) -> str: # pyright: ignore[reportReturnType]
108108
"""Next available rId in collection, starting from 'rId1' and making use of any
109109
gaps in numbering, e.g. 'rId2' for rIds ['rId1', 'rId3']."""
110110
for n in range(1, len(self) + 2):
@@ -116,7 +116,9 @@ def _next_rId(self) -> str:
116116
class _Relationship:
117117
"""Value object for relationship to part."""
118118

119-
def __init__(self, rId: str, reltype, target, baseURI, external=False):
119+
def __init__(
120+
self, rId: str, reltype: str, target: Part | str, baseURI: str, external: bool = False
121+
):
120122
super(_Relationship, self).__init__()
121123
self._rId = rId
122124
self._reltype = reltype
@@ -125,28 +127,29 @@ def __init__(self, rId: str, reltype, target, baseURI, external=False):
125127
self._is_external = bool(external)
126128

127129
@property
128-
def is_external(self):
130+
def is_external(self) -> bool:
129131
return self._is_external
130132

131133
@property
132-
def reltype(self):
134+
def reltype(self) -> str:
133135
return self._reltype
134136

135137
@property
136-
def rId(self):
138+
def rId(self) -> str:
137139
return self._rId
138140

139141
@property
140-
def target_part(self):
142+
def target_part(self) -> Part:
141143
if self._is_external:
142144
raise ValueError(
143145
"target_part property on _Relationship is undef" "ined when target mode is External"
144146
)
145-
return self._target
147+
return cast("Part", self._target)
146148

147149
@property
148150
def target_ref(self) -> str:
149151
if self._is_external:
150-
return self._target
152+
return cast(str, self._target)
151153
else:
152-
return self._target.partname.relative_ref(self._baseURI)
154+
target = cast("Part", self._target)
155+
return target.partname.relative_ref(self._baseURI)

‎src/docx/oxml/coreprops.py‎

Lines changed: 40 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
"""Custom element classes for core properties-related XML elements."""
22

3+
from __future__ import annotations
4+
5+
import datetime as dt
36
import re
4-
from datetime import datetime, timedelta
5-
from typing import Any
7+
from typing import TYPE_CHECKING, Any, Callable
68

79
from docx.oxml.ns import nsdecls, qn
810
from docx.oxml.parser import parse_xml
911
from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne
1012

13+
if TYPE_CHECKING:
14+
from lxml.etree import _Element as etree_Element # pyright: ignore[reportPrivateUsage]
15+
1116

1217
class CT_CoreProperties(BaseOxmlElement):
1318
"""`<cp:coreProperties>` element, the root element of the Core Properties part.
@@ -17,6 +22,8 @@ class CT_CoreProperties(BaseOxmlElement):
1722
present in the XML. String elements are limited in length to 255 unicode characters.
1823
"""
1924

25+
get_or_add_revision: Callable[[], etree_Element]
26+
2027
category = ZeroOrOne("cp:category", successors=())
2128
contentStatus = ZeroOrOne("cp:contentStatus", successors=())
2229
created = ZeroOrOne("dcterms:created", successors=())
@@ -28,7 +35,9 @@ class CT_CoreProperties(BaseOxmlElement):
2835
lastModifiedBy = ZeroOrOne("cp:lastModifiedBy", successors=())
2936
lastPrinted = ZeroOrOne("cp:lastPrinted", successors=())
3037
modified = ZeroOrOne("dcterms:modified", successors=())
31-
revision = ZeroOrOne("cp:revision", successors=())
38+
revision: etree_Element | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
39+
"cp:revision", successors=()
40+
)
3241
subject = ZeroOrOne("dc:subject", successors=())
3342
title = ZeroOrOne("dc:title", successors=())
3443
version = ZeroOrOne("cp:version", successors=())
@@ -80,55 +89,55 @@ def created_datetime(self):
8089
return self._datetime_of_element("created")
8190

8291
@created_datetime.setter
83-
def created_datetime(self, value):
92+
def created_datetime(self, value: dt.datetime):
8493
self._set_element_datetime("created", value)
8594

8695
@property
8796
def identifier_text(self):
8897
return self._text_of_element("identifier")
8998

9099
@identifier_text.setter
91-
def identifier_text(self, value):
100+
def identifier_text(self, value: str):
92101
self._set_element_text("identifier", value)
93102

94103
@property
95104
def keywords_text(self):
96105
return self._text_of_element("keywords")
97106

98107
@keywords_text.setter
99-
def keywords_text(self, value):
108+
def keywords_text(self, value: str):
100109
self._set_element_text("keywords", value)
101110

102111
@property
103112
def language_text(self):
104113
return self._text_of_element("language")
105114

106115
@language_text.setter
107-
def language_text(self, value):
116+
def language_text(self, value: str):
108117
self._set_element_text("language", value)
109118

110119
@property
111120
def lastModifiedBy_text(self):
112121
return self._text_of_element("lastModifiedBy")
113122

114123
@lastModifiedBy_text.setter
115-
def lastModifiedBy_text(self, value):
124+
def lastModifiedBy_text(self, value: str):
116125
self._set_element_text("lastModifiedBy", value)
117126

118127
@property
119128
def lastPrinted_datetime(self):
120129
return self._datetime_of_element("lastPrinted")
121130

122131
@lastPrinted_datetime.setter
123-
def lastPrinted_datetime(self, value):
132+
def lastPrinted_datetime(self, value: dt.datetime):
124133
self._set_element_datetime("lastPrinted", value)
125134

126135
@property
127-
def modified_datetime(self):
136+
def modified_datetime(self) -> dt.datetime | None:
128137
return self._datetime_of_element("modified")
129138

130139
@modified_datetime.setter
131-
def modified_datetime(self, value):
140+
def modified_datetime(self, value: dt.datetime):
132141
self._set_element_datetime("modified", value)
133142

134143
@property
@@ -137,7 +146,7 @@ def revision_number(self):
137146
revision = self.revision
138147
if revision is None:
139148
return 0
140-
revision_str = revision.text
149+
revision_str = str(revision.text)
141150
try:
142151
revision = int(revision_str)
143152
except ValueError:
@@ -149,9 +158,9 @@ def revision_number(self):
149158
return revision
150159

151160
@revision_number.setter
152-
def revision_number(self, value):
161+
def revision_number(self, value: int):
153162
"""Set revision property to string value of integer `value`."""
154-
if not isinstance(value, int) or value < 1:
163+
if not isinstance(value, int) or value < 1: # pyright: ignore[reportUnnecessaryIsInstance]
155164
tmpl = "revision property requires positive int, got '%s'"
156165
raise ValueError(tmpl % value)
157166
revision = self.get_or_add_revision()
@@ -162,26 +171,26 @@ def subject_text(self):
162171
return self._text_of_element("subject")
163172

164173
@subject_text.setter
165-
def subject_text(self, value):
174+
def subject_text(self, value: str):
166175
self._set_element_text("subject", value)
167176

168177
@property
169178
def title_text(self):
170179
return self._text_of_element("title")
171180

172181
@title_text.setter
173-
def title_text(self, value):
182+
def title_text(self, value: str):
174183
self._set_element_text("title", value)
175184

176185
@property
177186
def version_text(self):
178187
return self._text_of_element("version")
179188

180189
@version_text.setter
181-
def version_text(self, value):
190+
def version_text(self, value: str):
182191
self._set_element_text("version", value)
183192

184-
def _datetime_of_element(self, property_name):
193+
def _datetime_of_element(self, property_name: str) -> dt.datetime | None:
185194
element = getattr(self, property_name)
186195
if element is None:
187196
return None
@@ -192,16 +201,16 @@ def _datetime_of_element(self, property_name):
192201
# invalid datetime strings are ignored
193202
return None
194203

195-
def _get_or_add(self, prop_name):
204+
def _get_or_add(self, prop_name: str) -> BaseOxmlElement:
196205
"""Return element returned by "get_or_add_" method for `prop_name`."""
197206
get_or_add_method_name = "get_or_add_%s" % prop_name
198207
get_or_add_method = getattr(self, get_or_add_method_name)
199208
element = get_or_add_method()
200209
return element
201210

202211
@classmethod
203-
def _offset_dt(cls, dt, offset_str):
204-
"""A |datetime| instance offset from `dt` by timezone offset in `offset_str`.
212+
def _offset_dt(cls, dt_: dt.datetime, offset_str: str) -> dt.datetime:
213+
"""A |datetime| instance offset from `dt_` by timezone offset in `offset_str`.
205214
206215
`offset_str` is like `"-07:00"`.
207216
"""
@@ -212,13 +221,13 @@ def _offset_dt(cls, dt, offset_str):
212221
sign_factor = -1 if sign == "+" else 1
213222
hours = int(hours_str) * sign_factor
214223
minutes = int(minutes_str) * sign_factor
215-
td = timedelta(hours=hours, minutes=minutes)
216-
return dt + td
224+
td = dt.timedelta(hours=hours, minutes=minutes)
225+
return dt_ + td
217226

218227
_offset_pattern = re.compile(r"([+-])(\d\d):(\d\d)")
219228

220229
@classmethod
221-
def _parse_W3CDTF_to_datetime(cls, w3cdtf_str):
230+
def _parse_W3CDTF_to_datetime(cls, w3cdtf_str: str) -> dt.datetime:
222231
# valid W3CDTF date cases:
223232
# yyyy e.g. "2003"
224233
# yyyy-mm e.g. "2003-12"
@@ -235,22 +244,22 @@ def _parse_W3CDTF_to_datetime(cls, w3cdtf_str):
235244
# "-07:30", so we have to do it ourselves
236245
parseable_part = w3cdtf_str[:19]
237246
offset_str = w3cdtf_str[19:]
238-
dt = None
247+
dt_ = None
239248
for tmpl in templates:
240249
try:
241-
dt = datetime.strptime(parseable_part, tmpl)
250+
dt_ = dt.datetime.strptime(parseable_part, tmpl)
242251
except ValueError:
243252
continue
244-
if dt is None:
253+
if dt_ is None:
245254
tmpl = "could not parse W3CDTF datetime string '%s'"
246255
raise ValueError(tmpl % w3cdtf_str)
247256
if len(offset_str) == 6:
248-
return cls._offset_dt(dt, offset_str)
249-
return dt
257+
return cls._offset_dt(dt_, offset_str)
258+
return dt_
250259

251-
def _set_element_datetime(self, prop_name, value):
260+
def _set_element_datetime(self, prop_name: str, value: dt.datetime):
252261
"""Set date/time value of child element having `prop_name` to `value`."""
253-
if not isinstance(value, datetime):
262+
if not isinstance(value, dt.datetime): # pyright: ignore[reportUnnecessaryIsInstance]
254263
tmpl = "property requires <type 'datetime.datetime'> object, got %s"
255264
raise ValueError(tmpl % type(value))
256265
element = self._get_or_add(prop_name)

‎src/docx/oxml/document.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
class CT_Document(BaseOxmlElement):
1616
"""``<w:document>`` element, the root element of a document.xml file."""
1717

18-
body = ZeroOrOne("w:body")
18+
body: CT_Body = ZeroOrOne("w:body") # pyright: ignore[reportAssignmentType]
1919

2020
@property
2121
def sectPr_lst(self) -> List[CT_SectPr]:

‎src/docx/oxml/parser.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
oxml_parser.set_element_class_lookup(element_class_lookup)
2121

2222

23-
def parse_xml(xml: str) -> "BaseOxmlElement":
23+
def parse_xml(xml: str | bytes) -> "BaseOxmlElement":
2424
"""Root lxml element obtained by parsing XML character string `xml`.
2525
2626
The custom parser is used, so custom element classes are produced for elements in

‎src/docx/oxml/settings.py‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
11
"""Custom element classes related to document settings."""
22

3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, Callable
6+
37
from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrOne
48

9+
if TYPE_CHECKING:
10+
from docx.oxml.shared import CT_OnOff
11+
512

613
class CT_Settings(BaseOxmlElement):
714
"""`w:settings` element, root element for the settings part."""
815

16+
get_or_add_evenAndOddHeaders: Callable[[], CT_OnOff]
17+
_remove_evenAndOddHeaders: Callable[[], None]
18+
919
_tag_seq = (
1020
"w:writeProtection",
1121
"w:view",
@@ -106,20 +116,23 @@ class CT_Settings(BaseOxmlElement):
106116
"w:decimalSymbol",
107117
"w:listSeparator",
108118
)
109-
evenAndOddHeaders = ZeroOrOne("w:evenAndOddHeaders", successors=_tag_seq[48:])
119+
evenAndOddHeaders: CT_OnOff | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
120+
"w:evenAndOddHeaders", successors=_tag_seq[48:]
121+
)
110122
del _tag_seq
111123

112124
@property
113-
def evenAndOddHeaders_val(self):
125+
def evenAndOddHeaders_val(self) -> bool:
114126
"""Value of `w:evenAndOddHeaders/@w:val` or |None| if not present."""
115127
evenAndOddHeaders = self.evenAndOddHeaders
116128
if evenAndOddHeaders is None:
117129
return False
118130
return evenAndOddHeaders.val
119131

120132
@evenAndOddHeaders_val.setter
121-
def evenAndOddHeaders_val(self, value):
122-
if value in [None, False]:
133+
def evenAndOddHeaders_val(self, value: bool | None):
134+
if value is None or value is False:
123135
self._remove_evenAndOddHeaders()
124-
else:
125-
self.get_or_add_evenAndOddHeaders().val = value
136+
return
137+
138+
self.get_or_add_evenAndOddHeaders().val = value

‎src/docx/oxml/shape.py‎

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING
5+
from typing import TYPE_CHECKING, cast
66

77
from docx.oxml.ns import nsdecls
88
from docx.oxml.parser import parse_xml
@@ -34,48 +34,58 @@ class CT_Blip(BaseOxmlElement):
3434
"""``<a:blip>`` element, specifies image source and adjustments such as alpha and
3535
tint."""
3636

37-
embed = OptionalAttribute("r:embed", ST_RelationshipId)
38-
link = OptionalAttribute("r:link", ST_RelationshipId)
37+
embed: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
38+
"r:embed", ST_RelationshipId
39+
)
40+
link: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
41+
"r:link", ST_RelationshipId
42+
)
3943

4044

4145
class CT_BlipFillProperties(BaseOxmlElement):
4246
"""``<pic:blipFill>`` element, specifies picture properties."""
4347

44-
blip = ZeroOrOne("a:blip", successors=("a:srcRect", "a:tile", "a:stretch"))
48+
blip: CT_Blip = ZeroOrOne( # pyright: ignore[reportAssignmentType]
49+
"a:blip", successors=("a:srcRect", "a:tile", "a:stretch")
50+
)
4551

4652

4753
class CT_GraphicalObject(BaseOxmlElement):
4854
"""``<a:graphic>`` element, container for a DrawingML object."""
4955

50-
graphicData = OneAndOnlyOne("a:graphicData")
56+
graphicData: CT_GraphicalObjectData = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
57+
"a:graphicData"
58+
)
5159

5260

5361
class CT_GraphicalObjectData(BaseOxmlElement):
5462
"""``<a:graphicData>`` element, container for the XML of a DrawingML object."""
5563

56-
pic = ZeroOrOne("pic:pic")
57-
uri = RequiredAttribute("uri", XsdToken)
64+
pic: CT_Picture = ZeroOrOne("pic:pic") # pyright: ignore[reportAssignmentType]
65+
uri: str = RequiredAttribute("uri", XsdToken) # pyright: ignore[reportAssignmentType]
5866

5967

6068
class CT_Inline(BaseOxmlElement):
6169
"""`<wp:inline>` element, container for an inline shape."""
6270

63-
extent = OneAndOnlyOne("wp:extent")
64-
docPr = OneAndOnlyOne("wp:docPr")
65-
graphic = OneAndOnlyOne("a:graphic")
71+
extent: CT_PositiveSize2D = OneAndOnlyOne("wp:extent") # pyright: ignore[reportAssignmentType]
72+
docPr: CT_NonVisualDrawingProps = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
73+
"wp:docPr"
74+
)
75+
graphic: CT_GraphicalObject = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
76+
"a:graphic"
77+
)
6678

6779
@classmethod
6880
def new(cls, cx: Length, cy: Length, shape_id: int, pic: CT_Picture) -> CT_Inline:
6981
"""Return a new ``<wp:inline>`` element populated with the values passed as
7082
parameters."""
71-
inline = parse_xml(cls._inline_xml())
83+
inline = cast(CT_Inline, parse_xml(cls._inline_xml()))
7284
inline.extent.cx = cx
7385
inline.extent.cy = cy
7486
inline.docPr.id = shape_id
7587
inline.docPr.name = "Picture %d" % shape_id
76-
inline.graphic.graphicData.uri = (
77-
"http://schemas.openxmlformats.org/drawingml/2006/picture"
78-
)
88+
inline.graphic.graphicData.uri = "http://schemas.openxmlformats.org/drawingml/2006/picture"
7989
inline.graphic.graphicData._insert_pic(pic)
8090
return inline
8191

@@ -126,9 +136,13 @@ class CT_NonVisualPictureProperties(BaseOxmlElement):
126136
class CT_Picture(BaseOxmlElement):
127137
"""``<pic:pic>`` element, a DrawingML picture."""
128138

129-
nvPicPr = OneAndOnlyOne("pic:nvPicPr")
130-
blipFill = OneAndOnlyOne("pic:blipFill")
131-
spPr = OneAndOnlyOne("pic:spPr")
139+
nvPicPr: CT_PictureNonVisual = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
140+
"pic:nvPicPr"
141+
)
142+
blipFill: CT_BlipFillProperties = OneAndOnlyOne( # pyright: ignore[reportAssignmentType]
143+
"pic:blipFill"
144+
)
145+
spPr: CT_ShapeProperties = OneAndOnlyOne("pic:spPr") # pyright: ignore[reportAssignmentType]
132146

133147
@classmethod
134148
def new(cls, pic_id, filename, rId, cx, cy):
@@ -190,8 +204,12 @@ class CT_PositiveSize2D(BaseOxmlElement):
190204
Specifies the size of a DrawingML drawing.
191205
"""
192206

193-
cx = RequiredAttribute("cx", ST_PositiveCoordinate)
194-
cy = RequiredAttribute("cy", ST_PositiveCoordinate)
207+
cx: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType]
208+
"cx", ST_PositiveCoordinate
209+
)
210+
cy: Length = RequiredAttribute( # pyright: ignore[reportAssignmentType]
211+
"cy", ST_PositiveCoordinate
212+
)
195213

196214

197215
class CT_PresetGeometry2D(BaseOxmlElement):

‎src/docx/oxml/shared.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class CT_DecimalNumber(BaseOxmlElement):
1818
val: int = RequiredAttribute("w:val", ST_DecimalNumber) # pyright: ignore[reportAssignmentType]
1919

2020
@classmethod
21-
def new(cls, nsptagname, val):
21+
def new(cls, nsptagname: str, val: int):
2222
"""Return a new ``CT_DecimalNumber`` element having tagname `nsptagname` and
2323
``val`` attribute set to `val`."""
2424
return OxmlElement(nsptagname, attrs={qn("w:val"): str(val)})
@@ -31,7 +31,7 @@ class CT_OnOff(BaseOxmlElement):
3131
"off". Defaults to `True`, so `<w:b>` for example means "bold is turned on".
3232
"""
3333

34-
val: bool = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues]
34+
val: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
3535
"w:val", ST_OnOff, default=True
3636
)
3737

@@ -42,7 +42,7 @@ class CT_String(BaseOxmlElement):
4242
In those cases, it containing a style name in its `val` attribute.
4343
"""
4444

45-
val: str = RequiredAttribute("w:val", ST_String) # pyright: ignore[reportGeneralTypeIssues]
45+
val: str = RequiredAttribute("w:val", ST_String) # pyright: ignore[reportAssignmentType]
4646

4747
@classmethod
4848
def new(cls, nsptagname: str, val: str):

‎src/docx/oxml/styles.py‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,12 +128,10 @@ class CT_Style(BaseOxmlElement):
128128
rPr = ZeroOrOne("w:rPr", successors=_tag_seq[18:])
129129
del _tag_seq
130130

131-
type: WD_STYLE_TYPE | None = (
132-
OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues]
133-
"w:type", WD_STYLE_TYPE
134-
)
131+
type: WD_STYLE_TYPE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
132+
"w:type", WD_STYLE_TYPE
135133
)
136-
styleId: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues]
134+
styleId: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
137135
"w:styleId", ST_String
138136
)
139137
default = OptionalAttribute("w:default", ST_OnOff)

‎src/docx/oxml/table.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -973,5 +973,5 @@ class CT_VMerge(BaseOxmlElement):
973973
"""``<w:vMerge>`` element, specifying vertical merging behavior of a cell."""
974974

975975
val: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
976-
"w:val", ST_Merge, default=ST_Merge.CONTINUE # pyright: ignore[reportArgumentType]
976+
"w:val", ST_Merge, default=ST_Merge.CONTINUE
977977
)

‎src/docx/oxml/text/font.py‎

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ class CT_Fonts(BaseOxmlElement):
3939
Specifies typeface name for the various language types.
4040
"""
4141

42-
ascii: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues]
42+
ascii: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
4343
"w:ascii", ST_String
4444
)
45-
hAnsi: str | None = OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues]
45+
hAnsi: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
4646
"w:hAnsi", ST_String
4747
)
4848

@@ -148,18 +148,14 @@ class CT_RPr(BaseOxmlElement):
148148
sz: CT_HpsMeasure | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
149149
"w:sz", successors=_tag_seq[24:]
150150
)
151-
highlight: CT_Highlight | None = (
152-
ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
153-
"w:highlight", successors=_tag_seq[26:]
154-
)
151+
highlight: CT_Highlight | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
152+
"w:highlight", successors=_tag_seq[26:]
155153
)
156154
u: CT_Underline | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
157155
"w:u", successors=_tag_seq[27:]
158156
)
159-
vertAlign: CT_VerticalAlignRun | None = (
160-
ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
161-
"w:vertAlign", successors=_tag_seq[32:]
162-
)
157+
vertAlign: CT_VerticalAlignRun | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
158+
"w:vertAlign", successors=_tag_seq[32:]
163159
)
164160
rtl = ZeroOrOne("w:rtl", successors=_tag_seq[33:])
165161
cs = ZeroOrOne("w:cs", successors=_tag_seq[34:])
@@ -268,10 +264,7 @@ def subscript(self, value: bool | None) -> None:
268264
elif bool(value) is True:
269265
self.get_or_add_vertAlign().val = ST_VerticalAlignRun.SUBSCRIPT
270266
# -- assert bool(value) is False --
271-
elif (
272-
self.vertAlign is not None
273-
and self.vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT
274-
):
267+
elif self.vertAlign is not None and self.vertAlign.val == ST_VerticalAlignRun.SUBSCRIPT:
275268
self._remove_vertAlign()
276269

277270
@property
@@ -295,10 +288,7 @@ def superscript(self, value: bool | None):
295288
elif bool(value) is True:
296289
self.get_or_add_vertAlign().val = ST_VerticalAlignRun.SUPERSCRIPT
297290
# -- assert bool(value) is False --
298-
elif (
299-
self.vertAlign is not None
300-
and self.vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT
301-
):
291+
elif self.vertAlign is not None and self.vertAlign.val == ST_VerticalAlignRun.SUPERSCRIPT:
302292
self._remove_vertAlign()
303293

304294
@property
@@ -353,10 +343,8 @@ def _set_bool_val(self, name: str, value: bool | None):
353343
class CT_Underline(BaseOxmlElement):
354344
"""`<w:u>` element, specifying the underlining style for a run."""
355345

356-
val: WD_UNDERLINE | None = (
357-
OptionalAttribute( # pyright: ignore[reportGeneralTypeIssues]
358-
"w:val", WD_UNDERLINE
359-
)
346+
val: WD_UNDERLINE | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
347+
"w:val", WD_UNDERLINE
360348
)
361349

362350

‎src/docx/package.py‎

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import IO
5+
from typing import IO, cast
66

77
from docx.image.image import Image
88
from docx.opc.constants import RELATIONSHIP_TYPE as RT
@@ -44,16 +44,16 @@ def _gather_image_parts(self):
4444
continue
4545
if rel.target_part in self.image_parts:
4646
continue
47-
self.image_parts.append(rel.target_part)
47+
self.image_parts.append(cast("ImagePart", rel.target_part))
4848

4949

5050
class ImageParts:
5151
"""Collection of |ImagePart| objects corresponding to images in the package."""
5252

5353
def __init__(self):
54-
self._image_parts = []
54+
self._image_parts: list[ImagePart] = []
5555

56-
def __contains__(self, item):
56+
def __contains__(self, item: object):
5757
return self._image_parts.__contains__(item)
5858

5959
def __iter__(self):
@@ -62,7 +62,7 @@ def __iter__(self):
6262
def __len__(self):
6363
return self._image_parts.__len__()
6464

65-
def append(self, item):
65+
def append(self, item: ImagePart):
6666
self._image_parts.append(item)
6767

6868
def get_or_add_image_part(self, image_descriptor: str | IO[bytes]) -> ImagePart:
@@ -77,31 +77,30 @@ def get_or_add_image_part(self, image_descriptor: str | IO[bytes]) -> ImagePart:
7777
return matching_image_part
7878
return self._add_image_part(image)
7979

80-
def _add_image_part(self, image):
81-
"""Return an |ImagePart| instance newly created from image and appended to the
82-
collection."""
80+
def _add_image_part(self, image: Image):
81+
"""Return |ImagePart| instance newly created from `image` and appended to the collection."""
8382
partname = self._next_image_partname(image.ext)
8483
image_part = ImagePart.from_image(image, partname)
8584
self.append(image_part)
8685
return image_part
8786

88-
def _get_by_sha1(self, sha1):
87+
def _get_by_sha1(self, sha1: str) -> ImagePart | None:
8988
"""Return the image part in this collection having a SHA1 hash matching `sha1`,
9089
or |None| if not found."""
9190
for image_part in self._image_parts:
9291
if image_part.sha1 == sha1:
9392
return image_part
9493
return None
9594

96-
def _next_image_partname(self, ext):
95+
def _next_image_partname(self, ext: str) -> PackURI:
9796
"""The next available image partname, starting from ``/word/media/image1.{ext}``
9897
where unused numbers are reused.
9998
10099
The partname is unique by number, without regard to the extension. `ext` does
101100
not include the leading period.
102101
"""
103102

104-
def image_partname(n):
103+
def image_partname(n: int) -> PackURI:
105104
return PackURI("/word/media/image%d.%s" % (n, ext))
106105

107106
used_numbers = [image_part.partname.idx for image_part in self]

‎src/docx/parts/document.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import TYPE_CHECKING, cast
5+
from typing import IO, TYPE_CHECKING, cast
66

77
from docx.document import Document
88
from docx.enum.style import WD_STYLE_TYPE
@@ -17,6 +17,7 @@
1717

1818
if TYPE_CHECKING:
1919
from docx.opc.coreprops import CoreProperties
20+
from docx.settings import Settings
2021
from docx.styles.style import BaseStyle
2122

2223

@@ -101,13 +102,13 @@ def numbering_part(self):
101102
self.relate_to(numbering_part, RT.NUMBERING)
102103
return numbering_part
103104

104-
def save(self, path_or_stream):
105+
def save(self, path_or_stream: str | IO[bytes]):
105106
"""Save this document to `path_or_stream`, which can be either a path to a
106107
filesystem location (a string) or a file-like object."""
107108
self.package.save(path_or_stream)
108109

109110
@property
110-
def settings(self):
111+
def settings(self) -> Settings:
111112
"""A |Settings| object providing access to the settings in the settings part of
112113
this document."""
113114
return self._settings_part.settings
@@ -119,14 +120,14 @@ def styles(self):
119120
return self._styles_part.styles
120121

121122
@property
122-
def _settings_part(self):
123+
def _settings_part(self) -> SettingsPart:
123124
"""A |SettingsPart| object providing access to the document-level settings for
124125
this document.
125126
126127
Creates a default settings part if one is not present.
127128
"""
128129
try:
129-
return self.part_related_by(RT.SETTINGS)
130+
return cast(SettingsPart, self.part_related_by(RT.SETTINGS))
130131
except KeyError:
131132
settings_part = SettingsPart.default(self.package)
132133
self.relate_to(settings_part, RT.SETTINGS)

‎src/docx/parts/hdrftr.py‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
"""Header and footer part objects."""
22

3+
from __future__ import annotations
4+
35
import os
6+
from typing import TYPE_CHECKING
47

58
from docx.opc.constants import CONTENT_TYPE as CT
69
from docx.oxml.parser import parse_xml
710
from docx.parts.story import StoryPart
811

12+
if TYPE_CHECKING:
13+
from docx.package import Package
14+
915

1016
class FooterPart(StoryPart):
1117
"""Definition of a section footer."""
1218

1319
@classmethod
14-
def new(cls, package):
20+
def new(cls, package: Package):
1521
"""Return newly created footer part."""
1622
partname = package.next_partname("/word/footer%d.xml")
1723
content_type = CT.WML_FOOTER
@@ -21,9 +27,7 @@ def new(cls, package):
2127
@classmethod
2228
def _default_footer_xml(cls):
2329
"""Return bytes containing XML for a default footer part."""
24-
path = os.path.join(
25-
os.path.split(__file__)[0], "..", "templates", "default-footer.xml"
26-
)
30+
path = os.path.join(os.path.split(__file__)[0], "..", "templates", "default-footer.xml")
2731
with open(path, "rb") as f:
2832
xml_bytes = f.read()
2933
return xml_bytes
@@ -33,7 +37,7 @@ class HeaderPart(StoryPart):
3337
"""Definition of a section header."""
3438

3539
@classmethod
36-
def new(cls, package):
40+
def new(cls, package: Package):
3741
"""Return newly created header part."""
3842
partname = package.next_partname("/word/header%d.xml")
3943
content_type = CT.WML_HEADER
@@ -43,9 +47,7 @@ def new(cls, package):
4347
@classmethod
4448
def _default_header_xml(cls):
4549
"""Return bytes containing XML for a default header part."""
46-
path = os.path.join(
47-
os.path.split(__file__)[0], "..", "templates", "default-header.xml"
48-
)
50+
path = os.path.join(os.path.split(__file__)[0], "..", "templates", "default-header.xml")
4951
with open(path, "rb") as f:
5052
xml_bytes = f.read()
5153
return xml_bytes

‎src/docx/parts/image.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
from __future__ import annotations
44

55
import hashlib
6+
from typing import TYPE_CHECKING
67

78
from docx.image.image import Image
89
from docx.opc.part import Part
910
from docx.shared import Emu, Inches
1011

12+
if TYPE_CHECKING:
13+
from docx.opc.package import OpcPackage
14+
from docx.opc.packuri import PackURI
15+
1116

1217
class ImagePart(Part):
1318
"""An image part.
@@ -16,7 +21,7 @@ class ImagePart(Part):
1621
"""
1722

1823
def __init__(
19-
self, partname: str, content_type: str, blob: bytes, image: Image | None = None
24+
self, partname: PackURI, content_type: str, blob: bytes, image: Image | None = None
2025
):
2126
super(ImagePart, self).__init__(partname, content_type, blob)
2227
self._image = image
@@ -36,7 +41,7 @@ def default_cy(self):
3641
vertical dots per inch (dpi)."""
3742
px_height = self.image.px_height
3843
horz_dpi = self.image.horz_dpi
39-
height_in_emu = 914400 * px_height / horz_dpi
44+
height_in_emu = int(round(914400 * px_height / horz_dpi))
4045
return Emu(height_in_emu)
4146

4247
@property
@@ -52,7 +57,7 @@ def filename(self):
5257
return "image.%s" % self.partname.ext
5358

5459
@classmethod
55-
def from_image(cls, image, partname):
60+
def from_image(cls, image: Image, partname: PackURI):
5661
"""Return an |ImagePart| instance newly created from `image` and assigned
5762
`partname`."""
5863
return ImagePart(partname, image.content_type, image.blob, image)
@@ -64,12 +69,12 @@ def image(self) -> Image:
6469
return self._image
6570

6671
@classmethod
67-
def load(cls, partname, content_type, blob, package):
72+
def load(cls, partname: PackURI, content_type: str, blob: bytes, package: OpcPackage):
6873
"""Called by ``docx.opc.package.PartFactory`` to load an image part from a
6974
package being opened by ``Document(...)`` call."""
7075
return cls(partname, content_type, blob)
7176

7277
@property
7378
def sha1(self):
7479
"""SHA1 hash digest of the blob of this image part."""
75-
return hashlib.sha1(self._blob).hexdigest()
80+
return hashlib.sha1(self.blob).hexdigest()

‎src/docx/parts/settings.py‎

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,51 @@
11
"""|SettingsPart| and closely related objects."""
22

3+
from __future__ import annotations
4+
35
import os
6+
from typing import TYPE_CHECKING, cast
47

58
from docx.opc.constants import CONTENT_TYPE as CT
69
from docx.opc.packuri import PackURI
710
from docx.opc.part import XmlPart
811
from docx.oxml.parser import parse_xml
912
from docx.settings import Settings
1013

14+
if TYPE_CHECKING:
15+
from docx.oxml.settings import CT_Settings
16+
from docx.package import Package
17+
1118

1219
class SettingsPart(XmlPart):
1320
"""Document-level settings part of a WordprocessingML (WML) package."""
1421

22+
def __init__(
23+
self, partname: PackURI, content_type: str, element: CT_Settings, package: Package
24+
):
25+
super().__init__(partname, content_type, element, package)
26+
self._settings = element
27+
1528
@classmethod
16-
def default(cls, package):
29+
def default(cls, package: Package):
1730
"""Return a newly created settings part, containing a default `w:settings`
1831
element tree."""
1932
partname = PackURI("/word/settings.xml")
2033
content_type = CT.WML_SETTINGS
21-
element = parse_xml(cls._default_settings_xml())
34+
element = cast("CT_Settings", parse_xml(cls._default_settings_xml()))
2235
return cls(partname, content_type, element, package)
2336

2437
@property
25-
def settings(self):
26-
"""A |Settings| proxy object for the `w:settings` element in this part,
27-
containing the document-level settings for this document."""
28-
return Settings(self.element)
38+
def settings(self) -> Settings:
39+
"""A |Settings| proxy object for the `w:settings` element in this part.
40+
41+
Contains the document-level settings for this document.
42+
"""
43+
return Settings(self._settings)
2944

3045
@classmethod
3146
def _default_settings_xml(cls):
3247
"""Return a bytestream containing XML for a default settings part."""
33-
path = os.path.join(
34-
os.path.split(__file__)[0], "..", "templates", "default-settings.xml"
35-
)
48+
path = os.path.join(os.path.split(__file__)[0], "..", "templates", "default-settings.xml")
3649
with open(path, "rb") as f:
3750
xml_bytes = f.read()
3851
return xml_bytes

‎src/docx/parts/story.py‎

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

33
from __future__ import annotations
44

5-
from typing import IO, TYPE_CHECKING, Tuple
5+
from typing import IO, TYPE_CHECKING, Tuple, cast
66

77
from docx.opc.constants import RELATIONSHIP_TYPE as RT
88
from docx.opc.part import XmlPart
@@ -60,8 +60,8 @@ def get_style_id(
6060
def new_pic_inline(
6161
self,
6262
image_descriptor: str | IO[bytes],
63-
width: Length | None = None,
64-
height: Length | None = None,
63+
width: int | Length | None = None,
64+
height: int | Length | None = None,
6565
) -> CT_Inline:
6666
"""Return a newly-created `w:inline` element.
6767
@@ -92,4 +92,4 @@ def _document_part(self) -> DocumentPart:
9292
"""|DocumentPart| object for this package."""
9393
package = self.package
9494
assert package is not None
95-
return package.main_document_part
95+
return cast("DocumentPart", package.main_document_part)

‎src/docx/section.py‎

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,11 +160,7 @@ def iter_inner_content(self) -> Iterator[Paragraph | Table]:
160160
Items appear in document order.
161161
"""
162162
for element in self._sectPr.iter_inner_content():
163-
yield (
164-
Paragraph(element, self) # pyright: ignore[reportGeneralTypeIssues]
165-
if isinstance(element, CT_P)
166-
else Table(element, self)
167-
)
163+
yield (Paragraph(element, self) if isinstance(element, CT_P) else Table(element, self))
168164

169165
@property
170166
def left_margin(self) -> Length | None:
@@ -269,12 +265,10 @@ def __init__(self, document_elm: CT_Document, document_part: DocumentPart):
269265
self._document_part = document_part
270266

271267
@overload
272-
def __getitem__(self, key: int) -> Section:
273-
...
268+
def __getitem__(self, key: int) -> Section: ...
274269

275270
@overload
276-
def __getitem__(self, key: slice) -> List[Section]:
277-
...
271+
def __getitem__(self, key: slice) -> List[Section]: ...
278272

279273
def __getitem__(self, key: int | slice) -> Section | List[Section]:
280274
if isinstance(key, slice):

‎src/docx/settings.py‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
"""Settings object, providing access to document-level settings."""
22

3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING, cast
6+
37
from docx.shared import ElementProxy
48

9+
if TYPE_CHECKING:
10+
import docx.types as t
11+
from docx.oxml.settings import CT_Settings
12+
from docx.oxml.xmlchemy import BaseOxmlElement
13+
514

615
class Settings(ElementProxy):
716
"""Provides access to document-level settings for a document.
817
918
Accessed using the :attr:`.Document.settings` property.
1019
"""
1120

21+
def __init__(self, element: BaseOxmlElement, parent: t.ProvidesXmlPart | None = None):
22+
super().__init__(element, parent)
23+
self._settings = cast("CT_Settings", element)
24+
1225
@property
13-
def odd_and_even_pages_header_footer(self):
26+
def odd_and_even_pages_header_footer(self) -> bool:
1427
"""True if this document has distinct odd and even page headers and footers.
1528
1629
Read/write.
1730
"""
18-
return self._element.evenAndOddHeaders_val
31+
return self._settings.evenAndOddHeaders_val
1932

2033
@odd_and_even_pages_header_footer.setter
21-
def odd_and_even_pages_header_footer(self, value):
22-
self._element.evenAndOddHeaders_val = value
34+
def odd_and_even_pages_header_footer(self, value: bool):
35+
self._settings.evenAndOddHeaders_val = value

‎src/docx/shape.py‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,36 @@
33
A shape is a visual object that appears on the drawing layer of a document.
44
"""
55

6+
from __future__ import annotations
7+
8+
from typing import TYPE_CHECKING
9+
610
from docx.enum.shape import WD_INLINE_SHAPE
711
from docx.oxml.ns import nsmap
812
from docx.shared import Parented
913

14+
if TYPE_CHECKING:
15+
from docx.oxml.document import CT_Body
16+
from docx.oxml.shape import CT_Inline
17+
from docx.parts.story import StoryPart
18+
from docx.shared import Length
19+
1020

1121
class InlineShapes(Parented):
12-
"""Sequence of |InlineShape| instances, supporting len(), iteration, and indexed
13-
access."""
22+
"""Sequence of |InlineShape| instances, supporting len(), iteration, and indexed access."""
1423

15-
def __init__(self, body_elm, parent):
24+
def __init__(self, body_elm: CT_Body, parent: StoryPart):
1625
super(InlineShapes, self).__init__(parent)
1726
self._body = body_elm
1827

19-
def __getitem__(self, idx):
28+
def __getitem__(self, idx: int):
2029
"""Provide indexed access, e.g. 'inline_shapes[idx]'."""
2130
try:
2231
inline = self._inline_lst[idx]
2332
except IndexError:
2433
msg = "inline shape index [%d] out of range" % idx
2534
raise IndexError(msg)
35+
2636
return InlineShape(inline)
2737

2838
def __iter__(self):
@@ -42,20 +52,20 @@ class InlineShape:
4252
"""Proxy for an ``<wp:inline>`` element, representing the container for an inline
4353
graphical object."""
4454

45-
def __init__(self, inline):
55+
def __init__(self, inline: CT_Inline):
4656
super(InlineShape, self).__init__()
4757
self._inline = inline
4858

4959
@property
50-
def height(self):
60+
def height(self) -> Length:
5161
"""Read/write.
5262
5363
The display height of this inline shape as an |Emu| instance.
5464
"""
5565
return self._inline.extent.cy
5666

5767
@height.setter
58-
def height(self, cy):
68+
def height(self, cy: Length):
5969
self._inline.extent.cy = cy
6070
self._inline.graphic.graphicData.pic.spPr.cy = cy
6171

@@ -88,6 +98,6 @@ def width(self):
8898
return self._inline.extent.cx
8999

90100
@width.setter
91-
def width(self, cx):
101+
def width(self, cx: Length):
92102
self._inline.extent.cx = cx
93103
self._inline.graphic.graphicData.pic.spPr.cx = cx

‎src/docx/text/run.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ def add_break(self, break_type: WD_BREAK = WD_BREAK.LINE):
5959
def add_picture(
6060
self,
6161
image_path_or_stream: str | IO[bytes],
62-
width: Length | None = None,
63-
height: Length | None = None,
62+
width: int | Length | None = None,
63+
height: int | Length | None = None,
6464
) -> InlineShape:
6565
"""Return |InlineShape| containing image identified by `image_path_or_stream`.
6666

‎tests/opc/parts/test_coreprops.py‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,32 @@
55
import pytest
66

77
from docx.opc.coreprops import CoreProperties
8+
from docx.opc.package import OpcPackage
9+
from docx.opc.packuri import PackURI
810
from docx.opc.parts.coreprops import CorePropertiesPart
9-
from docx.oxml.coreprops import CT_CoreProperties
1011

11-
from ...unitutil.mock import class_mock, instance_mock
12+
from ...unitutil.cxml import element
13+
from ...unitutil.mock import FixtureRequest, Mock, class_mock, instance_mock
1214

1315

1416
class DescribeCorePropertiesPart:
15-
def it_provides_access_to_its_core_props_object(self, coreprops_fixture):
16-
core_properties_part, CoreProperties_ = coreprops_fixture
17+
"""Unit-test suite for `docx.opc.parts.coreprops.CorePropertiesPart` objects."""
18+
19+
def it_provides_access_to_its_core_props_object(self, CoreProperties_: Mock, package_: Mock):
20+
core_properties_part = CorePropertiesPart(
21+
PackURI("/part/name"), "content/type", element("cp:coreProperties"), package_
22+
)
23+
1724
core_properties = core_properties_part.core_properties
25+
1826
CoreProperties_.assert_called_once_with(core_properties_part.element)
1927
assert isinstance(core_properties, CoreProperties)
2028

21-
def it_can_create_a_default_core_properties_part(self):
22-
core_properties_part = CorePropertiesPart.default(None)
29+
def it_can_create_a_default_core_properties_part(self, package_: Mock):
30+
core_properties_part = CorePropertiesPart.default(package_)
31+
2332
assert isinstance(core_properties_part, CorePropertiesPart)
33+
# --
2434
core_properties = core_properties_part.core_properties
2535
assert core_properties.title == "Word Document"
2636
assert core_properties.last_modified_by == "python-docx"
@@ -32,16 +42,9 @@ def it_can_create_a_default_core_properties_part(self):
3242
# fixtures ---------------------------------------------
3343

3444
@pytest.fixture
35-
def coreprops_fixture(self, element_, CoreProperties_):
36-
core_properties_part = CorePropertiesPart(None, None, element_, None)
37-
return core_properties_part, CoreProperties_
38-
39-
# fixture components -----------------------------------
40-
41-
@pytest.fixture
42-
def CoreProperties_(self, request):
45+
def CoreProperties_(self, request: FixtureRequest):
4346
return class_mock(request, "docx.opc.parts.coreprops.CoreProperties")
4447

4548
@pytest.fixture
46-
def element_(self, request):
47-
return instance_mock(request, CT_CoreProperties)
49+
def package_(self, request: FixtureRequest):
50+
return instance_mock(request, OpcPackage)

‎tests/opc/test_coreprops.py‎

Lines changed: 127 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,153 @@
1+
# pyright: reportPrivateUsage=false
2+
13
"""Unit test suite for the docx.opc.coreprops module."""
24

3-
from datetime import datetime
5+
from __future__ import annotations
6+
7+
import datetime as dt
8+
from typing import TYPE_CHECKING, cast
49

510
import pytest
611

712
from docx.opc.coreprops import CoreProperties
813
from docx.oxml.parser import parse_xml
914

15+
if TYPE_CHECKING:
16+
from docx.oxml.coreprops import CT_CoreProperties
17+
1018

1119
class DescribeCoreProperties:
12-
def it_knows_the_string_property_values(self, text_prop_get_fixture):
13-
core_properties, prop_name, expected_value = text_prop_get_fixture
20+
"""Unit-test suite for `docx.opc.coreprops.CoreProperties` objects."""
21+
22+
@pytest.mark.parametrize(
23+
("prop_name", "expected_value"),
24+
[
25+
("author", "python-docx"),
26+
("category", ""),
27+
("comments", ""),
28+
("content_status", "DRAFT"),
29+
("identifier", "GXS 10.2.1ab"),
30+
("keywords", "foo bar baz"),
31+
("language", "US-EN"),
32+
("last_modified_by", "Steve Canny"),
33+
("subject", "Spam"),
34+
("title", "Word Document"),
35+
("version", "1.2.88"),
36+
],
37+
)
38+
def it_knows_the_string_property_values(
39+
self, prop_name: str, expected_value: str, core_properties: CoreProperties
40+
):
1441
actual_value = getattr(core_properties, prop_name)
1542
assert actual_value == expected_value
1643

17-
def it_can_change_the_string_property_values(self, text_prop_set_fixture):
18-
core_properties, prop_name, value, expected_xml = text_prop_set_fixture
19-
setattr(core_properties, prop_name, value)
20-
assert core_properties._element.xml == expected_xml
21-
22-
def it_knows_the_date_property_values(self, date_prop_get_fixture):
23-
core_properties, prop_name, expected_datetime = date_prop_get_fixture
24-
actual_datetime = getattr(core_properties, prop_name)
25-
assert actual_datetime == expected_datetime
44+
@pytest.mark.parametrize(
45+
("prop_name", "tagname", "value"),
46+
[
47+
("author", "dc:creator", "scanny"),
48+
("category", "cp:category", "silly stories"),
49+
("comments", "dc:description", "Bar foo to you"),
50+
("content_status", "cp:contentStatus", "FINAL"),
51+
("identifier", "dc:identifier", "GT 5.2.xab"),
52+
("keywords", "cp:keywords", "dog cat moo"),
53+
("language", "dc:language", "GB-EN"),
54+
("last_modified_by", "cp:lastModifiedBy", "Billy Bob"),
55+
("subject", "dc:subject", "Eggs"),
56+
("title", "dc:title", "Dissertation"),
57+
("version", "cp:version", "81.2.8"),
58+
],
59+
)
60+
def it_can_change_the_string_property_values(self, prop_name: str, tagname: str, value: str):
61+
coreProperties = self.coreProperties(tagname="", str_val="")
62+
core_properties = CoreProperties(cast("CT_CoreProperties", parse_xml(coreProperties)))
2663

27-
def it_can_change_the_date_property_values(self, date_prop_set_fixture):
28-
core_properties, prop_name, value, expected_xml = date_prop_set_fixture
2964
setattr(core_properties, prop_name, value)
30-
assert core_properties._element.xml == expected_xml
31-
32-
def it_knows_the_revision_number(self, revision_get_fixture):
33-
core_properties, expected_revision = revision_get_fixture
34-
assert core_properties.revision == expected_revision
35-
36-
def it_can_change_the_revision_number(self, revision_set_fixture):
37-
core_properties, revision, expected_xml = revision_set_fixture
38-
core_properties.revision = revision
39-
assert core_properties._element.xml == expected_xml
4065

41-
# fixtures -------------------------------------------------------
66+
assert core_properties._element.xml == self.coreProperties(tagname, value)
4267

43-
@pytest.fixture(
44-
params=[
45-
("created", datetime(2012, 11, 17, 16, 37, 40)),
46-
("last_printed", datetime(2014, 6, 4, 4, 28)),
68+
@pytest.mark.parametrize(
69+
("prop_name", "expected_datetime"),
70+
[
71+
("created", dt.datetime(2012, 11, 17, 16, 37, 40)),
72+
("last_printed", dt.datetime(2014, 6, 4, 4, 28)),
4773
("modified", None),
48-
]
74+
],
4975
)
50-
def date_prop_get_fixture(self, request, core_properties):
51-
prop_name, expected_datetime = request.param
52-
return core_properties, prop_name, expected_datetime
76+
def it_knows_the_date_property_values(
77+
self, prop_name: str, expected_datetime: dt.datetime, core_properties: CoreProperties
78+
):
79+
actual_datetime = getattr(core_properties, prop_name)
80+
assert actual_datetime == expected_datetime
5381

54-
@pytest.fixture(
55-
params=[
82+
@pytest.mark.parametrize(
83+
("prop_name", "tagname", "value", "str_val", "attrs"),
84+
[
5685
(
5786
"created",
5887
"dcterms:created",
59-
datetime(2001, 2, 3, 4, 5),
88+
dt.datetime(2001, 2, 3, 4, 5),
6089
"2001-02-03T04:05:00Z",
6190
' xsi:type="dcterms:W3CDTF"',
6291
),
6392
(
6493
"last_printed",
6594
"cp:lastPrinted",
66-
datetime(2014, 6, 4, 4),
95+
dt.datetime(2014, 6, 4, 4),
6796
"2014-06-04T04:00:00Z",
6897
"",
6998
),
7099
(
71100
"modified",
72101
"dcterms:modified",
73-
datetime(2005, 4, 3, 2, 1),
102+
dt.datetime(2005, 4, 3, 2, 1),
74103
"2005-04-03T02:01:00Z",
75104
' xsi:type="dcterms:W3CDTF"',
76105
),
77-
]
106+
],
78107
)
79-
def date_prop_set_fixture(self, request):
80-
prop_name, tagname, value, str_val, attrs = request.param
81-
coreProperties = self.coreProperties(None, None)
82-
core_properties = CoreProperties(parse_xml(coreProperties))
108+
def it_can_change_the_date_property_values(
109+
self, prop_name: str, tagname: str, value: dt.datetime, str_val: str, attrs: str
110+
):
111+
coreProperties = self.coreProperties(tagname="", str_val="")
112+
core_properties = CoreProperties(cast("CT_CoreProperties", parse_xml(coreProperties)))
83113
expected_xml = self.coreProperties(tagname, str_val, attrs)
84-
return core_properties, prop_name, value, expected_xml
85114

86-
@pytest.fixture(
87-
params=[("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)]
88-
)
89-
def revision_get_fixture(self, request):
90-
str_val, expected_revision = request.param
91-
tagname = "" if str_val is None else "cp:revision"
92-
coreProperties = self.coreProperties(tagname, str_val)
93-
core_properties = CoreProperties(parse_xml(coreProperties))
94-
return core_properties, expected_revision
95-
96-
@pytest.fixture(
97-
params=[
98-
(42, "42"),
99-
]
115+
setattr(core_properties, prop_name, value)
116+
117+
assert core_properties._element.xml == expected_xml
118+
119+
@pytest.mark.parametrize(
120+
("str_val", "expected_value"),
121+
[("42", 42), (None, 0), ("foobar", 0), ("-17", 0), ("32.7", 0)],
100122
)
101-
def revision_set_fixture(self, request):
102-
value, str_val = request.param
103-
coreProperties = self.coreProperties(None, None)
104-
core_properties = CoreProperties(parse_xml(coreProperties))
123+
def it_knows_the_revision_number(self, str_val: str | None, expected_value: int):
124+
tagname, str_val = ("cp:revision", str_val) if str_val else ("", "")
125+
coreProperties = self.coreProperties(tagname, str_val or "")
126+
core_properties = CoreProperties(cast("CT_CoreProperties", parse_xml(coreProperties)))
127+
128+
assert core_properties.revision == expected_value
129+
130+
@pytest.mark.parametrize(("value", "str_val"), [(42, "42")])
131+
def it_can_change_the_revision_number(self, value: int, str_val: str):
132+
coreProperties = self.coreProperties(tagname="", str_val="")
133+
core_properties = CoreProperties(cast("CT_CoreProperties", parse_xml(coreProperties)))
105134
expected_xml = self.coreProperties("cp:revision", str_val)
106-
return core_properties, value, expected_xml
107135

108-
@pytest.fixture(
109-
params=[
110-
("author", "python-docx"),
111-
("category", ""),
112-
("comments", ""),
113-
("content_status", "DRAFT"),
114-
("identifier", "GXS 10.2.1ab"),
115-
("keywords", "foo bar baz"),
116-
("language", "US-EN"),
117-
("last_modified_by", "Steve Canny"),
118-
("subject", "Spam"),
119-
("title", "Word Document"),
120-
("version", "1.2.88"),
121-
]
122-
)
123-
def text_prop_get_fixture(self, request, core_properties):
124-
prop_name, expected_value = request.param
125-
return core_properties, prop_name, expected_value
136+
core_properties.revision = value
126137

127-
@pytest.fixture(
128-
params=[
129-
("author", "dc:creator", "scanny"),
130-
("category", "cp:category", "silly stories"),
131-
("comments", "dc:description", "Bar foo to you"),
132-
("content_status", "cp:contentStatus", "FINAL"),
133-
("identifier", "dc:identifier", "GT 5.2.xab"),
134-
("keywords", "cp:keywords", "dog cat moo"),
135-
("language", "dc:language", "GB-EN"),
136-
("last_modified_by", "cp:lastModifiedBy", "Billy Bob"),
137-
("subject", "dc:subject", "Eggs"),
138-
("title", "dc:title", "Dissertation"),
139-
("version", "cp:version", "81.2.8"),
140-
]
141-
)
142-
def text_prop_set_fixture(self, request):
143-
prop_name, tagname, value = request.param
144-
coreProperties = self.coreProperties(None, None)
145-
core_properties = CoreProperties(parse_xml(coreProperties))
146-
expected_xml = self.coreProperties(tagname, value)
147-
return core_properties, prop_name, value, expected_xml
138+
assert core_properties._element.xml == expected_xml
148139

149-
# fixture components ---------------------------------------------
140+
# fixtures -------------------------------------------------------
150141

151-
def coreProperties(self, tagname, str_val, attrs=""):
142+
def coreProperties(self, tagname: str, str_val: str, attrs: str = "") -> str:
152143
tmpl = (
153-
'<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/'
154-
'package/2006/metadata/core-properties" xmlns:dc="http://purl.or'
155-
'g/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype'
156-
'/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://'
157-
'www.w3.org/2001/XMLSchema-instance">%s</cp:coreProperties>\n'
144+
"<cp:coreProperties"
145+
' xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"'
146+
' xmlns:dc="http://purl.org/dc/elements/1.1/"'
147+
' xmlns:dcmitype="http://purl.org/dc/dcmitype/"'
148+
' xmlns:dcterms="http://purl.org/dc/terms/"'
149+
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"'
150+
">%s</cp:coreProperties>\n"
158151
)
159152
if not tagname:
160153
child_element = ""
@@ -166,27 +159,30 @@ def coreProperties(self, tagname, str_val, attrs=""):
166159

167160
@pytest.fixture
168161
def core_properties(self):
169-
element = parse_xml(
170-
b"<?xml version='1.0' encoding='UTF-8' standalone='yes'?>"
171-
b'\n<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.o'
172-
b'rg/package/2006/metadata/core-properties" xmlns:dc="http://pur'
173-
b'l.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcm'
174-
b'itype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="h'
175-
b'ttp://www.w3.org/2001/XMLSchema-instance">\n'
176-
b" <cp:contentStatus>DRAFT</cp:contentStatus>\n"
177-
b" <dc:creator>python-docx</dc:creator>\n"
178-
b' <dcterms:created xsi:type="dcterms:W3CDTF">2012-11-17T11:07:'
179-
b"40-05:30</dcterms:created>\n"
180-
b" <dc:description/>\n"
181-
b" <dc:identifier>GXS 10.2.1ab</dc:identifier>\n"
182-
b" <dc:language>US-EN</dc:language>\n"
183-
b" <cp:lastPrinted>2014-06-04T04:28:00Z</cp:lastPrinted>\n"
184-
b" <cp:keywords>foo bar baz</cp:keywords>\n"
185-
b" <cp:lastModifiedBy>Steve Canny</cp:lastModifiedBy>\n"
186-
b" <cp:revision>4</cp:revision>\n"
187-
b" <dc:subject>Spam</dc:subject>\n"
188-
b" <dc:title>Word Document</dc:title>\n"
189-
b" <cp:version>1.2.88</cp:version>\n"
190-
b"</cp:coreProperties>\n"
162+
element = cast(
163+
"CT_CoreProperties",
164+
parse_xml(
165+
b"<?xml version='1.0' encoding='UTF-8' standalone='yes'?>"
166+
b'\n<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.o'
167+
b'rg/package/2006/metadata/core-properties" xmlns:dc="http://pur'
168+
b'l.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcm'
169+
b'itype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="h'
170+
b'ttp://www.w3.org/2001/XMLSchema-instance">\n'
171+
b" <cp:contentStatus>DRAFT</cp:contentStatus>\n"
172+
b" <dc:creator>python-docx</dc:creator>\n"
173+
b' <dcterms:created xsi:type="dcterms:W3CDTF">2012-11-17T11:07:'
174+
b"40-05:30</dcterms:created>\n"
175+
b" <dc:description/>\n"
176+
b" <dc:identifier>GXS 10.2.1ab</dc:identifier>\n"
177+
b" <dc:language>US-EN</dc:language>\n"
178+
b" <cp:lastPrinted>2014-06-04T04:28:00Z</cp:lastPrinted>\n"
179+
b" <cp:keywords>foo bar baz</cp:keywords>\n"
180+
b" <cp:lastModifiedBy>Steve Canny</cp:lastModifiedBy>\n"
181+
b" <cp:revision>4</cp:revision>\n"
182+
b" <dc:subject>Spam</dc:subject>\n"
183+
b" <dc:title>Word Document</dc:title>\n"
184+
b" <cp:version>1.2.88</cp:version>\n"
185+
b"</cp:coreProperties>\n"
186+
),
191187
)
192188
return CoreProperties(element)

‎tests/opc/test_part.py‎

Lines changed: 28 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -28,99 +28,54 @@
2828

2929

3030
class DescribePart:
31-
def it_can_be_constructed_by_PartFactory(
32-
self, partname_, content_type_, blob_, package_, __init_
33-
):
34-
part = Part.load(partname_, content_type_, blob_, package_)
31+
"""Unit-test suite for `docx.opc.part.Part` objects."""
3532

36-
__init_.assert_called_once_with(ANY, partname_, content_type_, blob_, package_)
33+
def it_can_be_constructed_by_PartFactory(self, package_: Mock, init__: Mock):
34+
part = Part.load(PackURI("/part/name"), "content/type", b"1be2", package_)
35+
36+
init__.assert_called_once_with(ANY, "/part/name", "content/type", b"1be2", package_)
3737
assert isinstance(part, Part)
3838

39-
def it_knows_its_partname(self, partname_get_fixture):
40-
part, expected_partname = partname_get_fixture
41-
assert part.partname == expected_partname
39+
def it_knows_its_partname(self):
40+
part = Part(PackURI("/part/name"), "content/type")
41+
assert part.partname == "/part/name"
4242

43-
def it_can_change_its_partname(self, partname_set_fixture):
44-
part, new_partname = partname_set_fixture
45-
part.partname = new_partname
46-
assert part.partname == new_partname
43+
def it_can_change_its_partname(self):
44+
part = Part(PackURI("/old/part/name"), "content/type")
45+
part.partname = PackURI("/new/part/name")
46+
assert part.partname == "/new/part/name"
4747

48-
def it_knows_its_content_type(self, content_type_fixture):
49-
part, expected_content_type = content_type_fixture
50-
assert part.content_type == expected_content_type
48+
def it_knows_its_content_type(self):
49+
part = Part(PackURI("/part/name"), "content/type")
50+
assert part.content_type == "content/type"
5151

52-
def it_knows_the_package_it_belongs_to(self, package_get_fixture):
53-
part, expected_package = package_get_fixture
54-
assert part.package == expected_package
52+
def it_knows_the_package_it_belongs_to(self, package_: Mock):
53+
part = Part(PackURI("/part/name"), "content/type", package=package_)
54+
assert part.package is package_
5555

56-
def it_can_be_notified_after_unmarshalling_is_complete(self, part):
56+
def it_can_be_notified_after_unmarshalling_is_complete(self):
57+
part = Part(PackURI("/part/name"), "content/type")
5758
part.after_unmarshal()
5859

59-
def it_can_be_notified_before_marshalling_is_started(self, part):
60+
def it_can_be_notified_before_marshalling_is_started(self):
61+
part = Part(PackURI("/part/name"), "content/type")
6062
part.before_marshal()
6163

62-
def it_uses_the_load_blob_as_its_blob(self, blob_fixture):
63-
part, load_blob = blob_fixture
64-
assert part.blob is load_blob
64+
def it_uses_the_load_blob_as_its_blob(self):
65+
blob = b"abcde"
66+
part = Part(PackURI("/part/name"), "content/type", blob)
67+
assert part.blob is blob
6568

6669
# fixtures ---------------------------------------------
6770

6871
@pytest.fixture
69-
def blob_fixture(self, blob_):
70-
part = Part(None, None, blob_, None)
71-
return part, blob_
72-
73-
@pytest.fixture
74-
def content_type_fixture(self):
75-
content_type = "content/type"
76-
part = Part(None, content_type, None, None)
77-
return part, content_type
78-
79-
@pytest.fixture
80-
def package_get_fixture(self, package_):
81-
part = Part(None, None, None, package_)
82-
return part, package_
83-
84-
@pytest.fixture
85-
def part(self):
86-
part = Part(None, None)
87-
return part
88-
89-
@pytest.fixture
90-
def partname_get_fixture(self):
91-
partname = PackURI("/part/name")
92-
part = Part(partname, None, None, None)
93-
return part, partname
94-
95-
@pytest.fixture
96-
def partname_set_fixture(self):
97-
old_partname = PackURI("/old/part/name")
98-
new_partname = PackURI("/new/part/name")
99-
part = Part(old_partname, None, None, None)
100-
return part, new_partname
101-
102-
# fixture components ---------------------------------------------
103-
104-
@pytest.fixture
105-
def blob_(self, request):
106-
return instance_mock(request, bytes)
107-
108-
@pytest.fixture
109-
def content_type_(self, request):
110-
return instance_mock(request, str)
111-
112-
@pytest.fixture
113-
def __init_(self, request):
72+
def init__(self, request: FixtureRequest):
11473
return initializer_mock(request, Part)
11574

11675
@pytest.fixture
117-
def package_(self, request):
76+
def package_(self, request: FixtureRequest):
11877
return instance_mock(request, OpcPackage)
11978

120-
@pytest.fixture
121-
def partname_(self, request):
122-
return instance_mock(request, PackURI)
123-
12479

12580
class DescribePartRelationshipManagementInterface:
12681
"""Unit-test suite for `docx.opc.package.Part` relationship behaviors."""

0 commit comments

Comments
 (0)
Please sign in to comment.