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 1e1e814

Browse files
tiberlas=
authored and
=
committedMay 13, 2024
rfctr: Blacken footnote support
1 parent f46762c commit 1e1e814

File tree

15 files changed

+304
-203
lines changed

15 files changed

+304
-203
lines changed
 

‎features/steps/footnotes.py

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Step implementations for footnote-related features."""
22

3-
from behave import given, when, then
3+
from behave import given, then, when
44
from behave.runner import Context
55

66
from docx import Document
@@ -44,7 +44,9 @@ def given_a_paragraph_in_a_document_without_footnotes(context: Context):
4444
context.footnotes = document.footnotes
4545

4646

47-
@given("a document with paragraphs[0] containing one, paragraphs[1] containing none, and paragraphs[2] containing two footnotes")
47+
@given(
48+
"a document with paragraphs[0] containing one, paragraphs[1] containing none, and paragraphs[2] containing two footnotes"
49+
)
4850
def given_a_document_with_3_footnotes(context: Context):
4951
document = Document(test_docx("footnotes"))
5052
context.paragraphs = document.paragraphs
@@ -64,14 +66,18 @@ def when_I_try_to_access_a_footnote_with_invalid_reference_id(context: Context):
6466

6567

6668
@when("I add a footnote to the paragraphs[{parId}] with text '{footnoteText}'")
67-
def when_I_add_a_footnote_to_the_paragraph_with_text_text(context: Context, parId: str, footnoteText: str):
69+
def when_I_add_a_footnote_to_the_paragraph_with_text_text(
70+
context: Context, parId: str, footnoteText: str
71+
):
6872
par = context.paragraphs[int(parId)]
6973
new_footnote = par.add_footnote()
7074
new_footnote.add_paragraph(footnoteText)
7175

7276

7377
@when("I change footnote property {propName} to {value}")
74-
def when_I_change_footnote_property_propName_to_value(context: Context, propName: str, value: str):
78+
def when_I_change_footnote_property_propName_to_value(
79+
context: Context, propName: str, value: str
80+
):
7581
context.section.__setattr__(propName, eval(value))
7682

7783

@@ -81,7 +87,9 @@ def when_I_change_footnote_property_propName_to_value(context: Context, propName
8187
@then("len(footnotes) is {expectedLen}")
8288
def then_len_footnotes_is_len(context: Context, expectedLen: str):
8389
footnotes = context.footnotes
84-
assert len(footnotes) == int(expectedLen), f"expected len(footnotes) of {expectedLen}, got {len(footnotes)}"
90+
assert len(footnotes) == int(
91+
expectedLen
92+
), f"expected len(footnotes) of {expectedLen}, got {len(footnotes)}"
8593

8694

8795
@then("I can access a footnote by footnote reference id")
@@ -107,35 +115,57 @@ def then_it_trows_an_IndexError(context: Context, exceptionType: str):
107115

108116

109117
@then("I can access footnote property {propName} with value {value}")
110-
def then_I_can_access_footnote_propery_name_with_value_value(context: Context, propName: str, value: str):
118+
def then_I_can_access_footnote_propery_name_with_value_value(
119+
context: Context, propName: str, value: str
120+
):
111121
actual_value = context.section.__getattribute__(propName)
112122
expected = eval(value)
113-
assert actual_value == expected, f"expected section.{propName} {value}, got {expected}"
123+
assert (
124+
actual_value == expected
125+
), f"expected section.{propName} {value}, got {expected}"
114126

115127

116-
@then("the document contains a footnote with footnote reference id of {refId} with text '{footnoteText}'")
117-
def then_the_document_contains_a_footnote_with_footnote_reference_id_of_refId_with_text_text(context: Context, refId: str, footnoteText: str):
128+
@then(
129+
"the document contains a footnote with footnote reference id of {refId} with text '{footnoteText}'"
130+
)
131+
def then_the_document_contains_a_footnote_with_footnote_reference_id_of_refId_with_text_text(
132+
context: Context, refId: str, footnoteText: str
133+
):
118134
par = context.paragraphs[1]
119135
f = par.footnotes[0]
120136
assert f.id == int(refId), f"expected {refId}, got {f.id}"
121-
assert f.paragraphs[0].text == footnoteText, f"expected {footnoteText}, got {f.paragraphs[0].text}"
137+
assert (
138+
f.paragraphs[0].text == footnoteText
139+
), f"expected {footnoteText}, got {f.paragraphs[0].text}"
122140

123141

124-
@then("paragraphs[{parId}] has footnote reference ids of {refIds}, with footnote text {fText}")
125-
def then_paragraph_has_footnote_reference_ids_of_refIds_with_footnote_text_text(context: Context, parId: str, refIds: str, fText: str):
142+
@then(
143+
"paragraphs[{parId}] has footnote reference ids of {refIds}, with footnote text {fText}"
144+
)
145+
def then_paragraph_has_footnote_reference_ids_of_refIds_with_footnote_text_text(
146+
context: Context, parId: str, refIds: str, fText: str
147+
):
126148
par = context.paragraphs[int(parId)]
127149
refIds = eval(refIds)
128150
fText = eval(fText)
129151
if refIds is not None:
130152
if type(refIds) is list:
131153
for i in range(len(refIds)):
132154
f = par.footnotes[i]
133-
assert isinstance(f, Footnote), f"expected to be instance of Footnote, got {type(f)}"
155+
assert isinstance(
156+
f, Footnote
157+
), f"expected to be instance of Footnote, got {type(f)}"
134158
assert f.id == refIds[i], f"expected {refIds[i]}, got {f.id}"
135-
assert f.paragraphs[0].text == fText[i], f"expected '{fText[i]}', got '{f.paragraphs[0].text}'"
159+
assert (
160+
f.paragraphs[0].text == fText[i]
161+
), f"expected '{fText[i]}', got '{f.paragraphs[0].text}'"
136162
else:
137163
f = par.footnotes[0]
138164
assert f.id == int(refIds), f"expected {refIds}, got {f.id}"
139-
assert f.paragraphs[0].text == fText, f"expected '{fText}', got '{f.paragraphs[0].text}'"
165+
assert (
166+
f.paragraphs[0].text == fText
167+
), f"expected '{fText}', got '{f.paragraphs[0].text}'"
140168
else:
141-
assert len(par.footnotes) == 0, f"expected an empty list, got {len(par.footnotes)} elements"
169+
assert (
170+
len(par.footnotes) == 0
171+
), f"expected an empty list, got {len(par.footnotes)} elements"

‎src/docx/document.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
if TYPE_CHECKING:
1717
import docx.types as t
1818
from docx.oxml.document import CT_Body, CT_Document
19+
from docx.oxml.footnote import CT_Footnotes, CT_FtnEnd
20+
from docx.oxml.text.paragraph import CT_P
1921
from docx.parts.document import DocumentPart
2022
from docx.settings import Settings
2123
from docx.shared import Length
2224
from docx.styles.style import ParagraphStyle, _TableStyle
2325
from docx.table import Table
2426
from docx.text.paragraph import Paragraph
25-
from docx.oxml.footnote import CT_Footnotes, CT_FtnEnd
26-
from docx.oxml.text.paragraph import CT_P
2727

2828

2929
class Document(ElementProxy):
@@ -199,11 +199,9 @@ def _body(self) -> _Body:
199199
return self.__body
200200

201201
def _calculate_next_footnote_reference_id(self, p: CT_P) -> int:
202-
"""
203-
Return the appropriate footnote reference id number for
204-
a new footnote added at the end of paragraph `p`.
205-
"""
206-
# When adding a footnote it can be inserted
202+
"""Return the appropriate footnote reference id number for
203+
a new footnote added at the end of paragraph `p`."""
204+
# When adding a footnote it can be inserted
207205
# in front of some other footnotes, so
208206
# we need to sort footnotes by `footnote_reference_id`
209207
# in |Footnotes| and in |Paragraph|
@@ -234,7 +232,7 @@ def _calculate_next_footnote_reference_id(self, p: CT_P) -> int:
234232
else:
235233
# This is the last footnote before the new footnote, so we use its
236234
# value to determent the value of the new footnote.
237-
new_fr_id = max(self.paragraphs[p_i]._p.footnote_reference_ids)+1
235+
new_fr_id = max(self.paragraphs[p_i]._p.footnote_reference_ids) + 1
238236
break
239237
return new_fr_id
240238

‎src/docx/footnotes.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,18 @@
99

1010
if TYPE_CHECKING:
1111
from docx import types as t
12-
from docx.oxml.footnote import CT_FtnEnd, CT_Footnotes
12+
from docx.oxml.footnote import CT_Footnotes, CT_FtnEnd
13+
1314

1415
class Footnotes(Parented):
15-
"""
16-
Proxy object wrapping ``<w:footnotes>`` element.
17-
"""
16+
"""Proxy object wrapping ``<w:footnotes>`` element."""
17+
1818
def __init__(self, footnotes: CT_Footnotes, parent: t.ProvidesStoryPart):
1919
super(Footnotes, self).__init__(parent)
2020
self._element = self._footnotes = footnotes
2121

2222
def __getitem__(self, reference_id: int) -> Footnote:
23-
"""
24-
A |Footnote| for a specific footnote of reference id, defined with ``w:id`` argument of ``<w:footnoteReference>``.
25-
If reference id is invalid raises an |IndexError|
26-
"""
23+
"""A |Footnote| for a specific footnote of reference id, defined with ``w:id`` argument of ``<w:footnoteReference>``. If reference id is invalid raises an |IndexError|"""
2724
footnote = self._element.get_by_id(reference_id)
2825
if footnote is None:
2926
raise IndexError
@@ -33,15 +30,13 @@ def __len__(self) -> int:
3330
return len(self._element)
3431

3532
def add_footnote(self, footnote_reference_id: int) -> Footnote:
36-
"""
37-
Return a newly created |Footnote|, the new footnote will
33+
"""Return a newly created |Footnote|, the new footnote will
3834
be inserted in the correct spot by `footnote_reference_id`.
39-
The footnotes are kept in order by `footnote_reference_id`.
40-
"""
41-
elements = self._element # for easy access
35+
The footnotes are kept in order by `footnote_reference_id`."""
36+
elements = self._element # for easy access
4237
new_footnote = None
4338
if elements.get_by_id(footnote_reference_id) is not None:
44-
# When adding a footnote it can be inserted
39+
# When adding a footnote it can be inserted
4540
# in front of some other footnotes, so
4641
# we need to sort footnotes by `footnote_reference_id`
4742
# in |Footnotes| and in |Paragraph|
@@ -54,7 +49,9 @@ def add_footnote(self, footnote_reference_id: int) -> Footnote:
5449
for index in reversed(range(len(elements))):
5550
if elements[index].id == footnote_reference_id:
5651
elements[index].id += 1
57-
new_footnote = elements[index].add_footnote_before(footnote_reference_id)
52+
new_footnote = elements[index].add_footnote_before(
53+
footnote_reference_id
54+
)
5855
break
5956
else:
6057
elements[index].id += 1
@@ -65,10 +62,9 @@ def add_footnote(self, footnote_reference_id: int) -> Footnote:
6562

6663

6764
class Footnote(BlockItemContainer):
68-
"""
69-
Proxy object wrapping ``<w:footnote>`` element.
70-
"""
71-
def __init__(self, f: CT_FtnEnd, parent: t.ProvidesStoryPart):
65+
"""Proxy object wrapping ``<w:footnote>`` element."""
66+
67+
def __init__(self, f: CT_FtnEnd, parent: t.ProvidesStoryPart):
7268
super(Footnote, self).__init__(f, parent)
7369
self._f = self._element = f
7470

‎src/docx/oxml/__init__.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -117,17 +117,17 @@
117117
CT_SectType,
118118
)
119119

120-
register_element_cls('w:footnotePr', CT_FtnProps)
120+
register_element_cls("w:footnotePr", CT_FtnProps)
121121
register_element_cls("w:footerReference", CT_HdrFtrRef)
122122
register_element_cls("w:ftr", CT_HdrFtr)
123123
register_element_cls("w:hdr", CT_HdrFtr)
124124
register_element_cls("w:headerReference", CT_HdrFtrRef)
125-
register_element_cls('w:numFmt', CT_NumFmt)
126-
register_element_cls('w:numStart', CT_DecimalNumber)
127-
register_element_cls('w:numRestart', CT_NumRestart)
125+
register_element_cls("w:numFmt", CT_NumFmt)
126+
register_element_cls("w:numStart", CT_DecimalNumber)
127+
register_element_cls("w:numRestart", CT_NumRestart)
128128
register_element_cls("w:pgMar", CT_PageMar)
129129
register_element_cls("w:pgSz", CT_PageSz)
130-
register_element_cls('w:pos', CT_FtnPos)
130+
register_element_cls("w:pos", CT_FtnPos)
131131
register_element_cls("w:sectPr", CT_SectPr)
132132
register_element_cls("w:type", CT_SectType)
133133

@@ -254,11 +254,9 @@
254254
# ---------------------------------------------------------------------------
255255
# footnote-related mappings
256256

257-
from .footnote import (
258-
CT_FtnEnd,
259-
CT_Footnotes
260-
)
257+
from .footnote import CT_Footnotes, CT_FtnEnd
261258
from .text.footnote_reference import CT_FtnEdnRef
262-
register_element_cls('w:footnoteReference', CT_FtnEdnRef)
263-
register_element_cls('w:footnote', CT_FtnEnd)
264-
register_element_cls('w:footnotes', CT_Footnotes)
259+
260+
register_element_cls("w:footnoteReference", CT_FtnEdnRef)
261+
register_element_cls("w:footnote", CT_FtnEnd)
262+
register_element_cls("w:footnotes", CT_Footnotes)

‎src/docx/oxml/footnote.py

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,23 @@
66

77
from docx.oxml.ns import qn
88
from docx.oxml.parser import OxmlElement
9-
from docx.oxml.xmlchemy import (
10-
BaseOxmlElement, RequiredAttribute, ZeroOrMore, OneOrMore
11-
)
12-
from docx.oxml.simpletypes import (
13-
ST_DecimalNumber
14-
)
9+
from docx.oxml.simpletypes import ST_DecimalNumber
10+
from docx.oxml.xmlchemy import BaseOxmlElement, OneOrMore, RequiredAttribute, ZeroOrMore
1511

1612
if TYPE_CHECKING:
1713
from docx.oxml.text.paragraph import CT_P
1814

1915

2016
class CT_FtnEnd(BaseOxmlElement):
21-
"""
22-
``<w:footnote>`` element, containing the properties for a specific footnote
23-
"""
24-
id = RequiredAttribute('w:id', ST_DecimalNumber)
25-
p = ZeroOrMore('w:p')
17+
"""``<w:footnote>`` element, containing the properties for a specific footnote"""
18+
19+
id = RequiredAttribute("w:id", ST_DecimalNumber)
20+
p = ZeroOrMore("w:p")
2621

2722
def add_footnote_before(self, footnote_reference_id: int) -> CT_FtnEnd:
28-
"""
29-
Create a ``<w:footnote>`` element with `footnote_reference_id`
30-
and insert it before the current element.
31-
"""
32-
new_footnote = OxmlElement('w:footnote')
23+
"""Create a ``<w:footnote>`` element with `footnote_reference_id`
24+
and insert it before the current element."""
25+
new_footnote = OxmlElement("w:footnote")
3326
new_footnote.id = footnote_reference_id
3427
self.addprevious(new_footnote)
3528
return new_footnote
@@ -40,23 +33,20 @@ def paragraphs(self) -> List[CT_P]:
4033

4134
paragraphs = []
4235
for child in self:
43-
if child.tag == qn('w:p'):
36+
if child.tag == qn("w:p"):
4437
paragraphs.append(child)
4538
return paragraphs
4639

4740

4841
class CT_Footnotes(BaseOxmlElement):
49-
"""
50-
``<w:footnotes>`` element, containing a sequence of footnote (w:footnote) elements
51-
"""
42+
"""``<w:footnotes>`` element, containing a sequence of footnote (w:footnote) elements"""
43+
5244
add_footnote_sequence: Callable[[], CT_FtnEnd]
5345

54-
footnote_sequence = OneOrMore('w:footnote')
46+
footnote_sequence = OneOrMore("w:footnote")
5547

5648
def add_footnote(self, footnote_reference_id: int) -> CT_FtnEnd:
57-
"""
58-
Create a ``<w:footnote>`` element with `footnote_reference_id`.
59-
"""
49+
"""Create a ``<w:footnote>`` element with `footnote_reference_id`."""
6050
new_f = self.add_footnote_sequence()
6151
new_f.id = footnote_reference_id
6252
return new_f

‎src/docx/oxml/section.py

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@
22

33
from __future__ import annotations
44

5-
from warnings import warn
6-
75
from copy import deepcopy
86
from typing import Callable, Iterator, List, Sequence, cast
7+
from warnings import warn
98

109
from lxml import etree
1110
from typing_extensions import TypeAlias
1211

1312
from docx.enum.section import WD_HEADER_FOOTER, WD_ORIENTATION, WD_SECTION_START
1413
from docx.oxml.ns import nsmap
15-
from docx.oxml.shared import CT_OnOff, CT_DecimalNumber
16-
from docx.oxml.simpletypes import ST_SignedTwipsMeasure, ST_TwipsMeasure, XsdString, ST_FtnPos, ST_NumberFormat, ST_RestartNumber, ST_DecimalNumber
14+
from docx.oxml.shared import CT_DecimalNumber, CT_OnOff
15+
from docx.oxml.simpletypes import (
16+
ST_DecimalNumber,
17+
ST_FtnPos,
18+
ST_NumberFormat,
19+
ST_RestartNumber,
20+
ST_SignedTwipsMeasure,
21+
ST_TwipsMeasure,
22+
XsdString,
23+
)
1724
from docx.oxml.table import CT_Tbl
1825
from docx.oxml.text.paragraph import CT_P
1926
from docx.oxml.xmlchemy import (
@@ -30,7 +37,8 @@
3037

3138
class CT_FtnPos(BaseOxmlElement):
3239
"""``<w:pos>`` element, footnote placement"""
33-
val = RequiredAttribute('w:val', ST_FtnPos)
40+
41+
val = RequiredAttribute("w:val", ST_FtnPos)
3442

3543

3644
class CT_FtnProps(BaseOxmlElement):
@@ -41,13 +49,19 @@ class CT_FtnProps(BaseOxmlElement):
4149
get_or_add_numStart: Callable[[], CT_DecimalNumber]
4250
get_or_add_numRestart: Callable[[], CT_NumRestart]
4351

44-
_tag_seq = (
45-
'w:pos', 'w:numFmt', 'w:numStart', 'w:numRestart'
46-
)
47-
pos: CT_FtnPos | None = ZeroOrOne('w:pos', successors=_tag_seq) # pyright: ignore[reportGeneralTypeIssues]
48-
numFmt: CT_NumFmt | None = ZeroOrOne('w:numFmt', successors=_tag_seq[1:]) # pyright: ignore[reportGeneralTypeIssues]
49-
numStart: CT_DecimalNumber | None = ZeroOrOne('w:numStart', successors=_tag_seq[2:]) # pyright: ignore[reportGeneralTypeIssues]
50-
numRestart: CT_NumRestart | None = ZeroOrOne('w:numRestart', successors=_tag_seq[3:]) # pyright: ignore[reportGeneralTypeIssues]
52+
_tag_seq = ("w:pos", "w:numFmt", "w:numStart", "w:numRestart")
53+
pos: CT_FtnPos | None = ZeroOrOne(
54+
"w:pos", successors=_tag_seq
55+
) # pyright: ignore[reportGeneralTypeIssues]
56+
numFmt: CT_NumFmt | None = ZeroOrOne(
57+
"w:numFmt", successors=_tag_seq[1:]
58+
) # pyright: ignore[reportGeneralTypeIssues]
59+
numStart: CT_DecimalNumber | None = ZeroOrOne(
60+
"w:numStart", successors=_tag_seq[2:]
61+
) # pyright: ignore[reportGeneralTypeIssues]
62+
numRestart: CT_NumRestart | None = ZeroOrOne(
63+
"w:numRestart", successors=_tag_seq[3:]
64+
) # pyright: ignore[reportGeneralTypeIssues]
5165

5266

5367
class CT_HdrFtr(BaseOxmlElement):
@@ -83,12 +97,14 @@ class CT_HdrFtrRef(BaseOxmlElement):
8397

8498
class CT_NumFmt(BaseOxmlElement):
8599
"""``<w:numFmt>`` element, footnote numbering format"""
86-
val = RequiredAttribute('w:val', ST_NumberFormat)
100+
101+
val = RequiredAttribute("w:val", ST_NumberFormat)
87102

88103

89104
class CT_NumRestart(BaseOxmlElement):
90105
"""``<w:numStart>`` element, footnote numbering restart location"""
91-
val = RequiredAttribute('w:val', ST_RestartNumber)
106+
107+
val = RequiredAttribute("w:val", ST_RestartNumber)
92108

93109

94110
class CT_PageMar(BaseOxmlElement):
@@ -180,8 +196,10 @@ class CT_SectPr(BaseOxmlElement):
180196
titlePg: CT_OnOff | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
181197
"w:titlePg", successors=_tag_seq[14:]
182198
)
183-
footnotePr: CT_FtnProps | None = ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
184-
"w:footnotePr", successors=_tag_seq[1:]
199+
footnotePr: CT_FtnProps | None = (
200+
ZeroOrOne( # pyright: ignore[reportGeneralTypeIssues]
201+
"w:footnotePr", successors=_tag_seq[1:]
202+
)
185203
)
186204
del _tag_seq
187205

@@ -251,11 +269,9 @@ def footer(self, value: int | Length | None):
251269

252270
@property
253271
def footnote_number_format(self) -> ST_NumberFormat | None:
254-
"""
255-
The value of the ``w:val`` attribute in the ``<w:numFmt>`` child
272+
"""The value of the ``w:val`` attribute in the ``<w:numFmt>`` child
256273
element of ``<w:footnotePr>`` element, as a |String|, or |None| if either the element or the
257-
attribute is not present.
258-
"""
274+
attribute is not present."""
259275
fPr = self.footnotePr
260276
if fPr is None or fPr.numFmt is None:
261277
return None
@@ -269,11 +285,9 @@ def footnote_number_format(self, value: ST_NumberFormat | None):
269285

270286
@property
271287
def footnote_numbering_restart_location(self) -> ST_RestartNumber | None:
272-
"""
273-
The value of the ``w:val`` attribute in the ``<w:numRestart>`` child
288+
"""The value of the ``w:val`` attribute in the ``<w:numRestart>`` child
274289
element of ``<w:footnotePr>`` element, as a |String|, or |None| if either the element or the
275-
attribute is not present.
276-
"""
290+
attribute is not present."""
277291
fPr = self.footnotePr
278292
if fPr is None or fPr.numRestart is None:
279293
return None
@@ -287,18 +301,16 @@ def footnote_numbering_restart_location(self, value: ST_RestartNumber | None):
287301
numRestart.val = value
288302
if len(numStart.values()) == 0:
289303
numStart.val = 1
290-
elif value != 'continuous':
304+
elif value != "continuous":
291305
numStart.val = 1
292306
msg = "When ``<w:numRestart> is not 'continuous', then ``<w:numStart>`` must be 1."
293307
warn(msg, UserWarning, stacklevel=2)
294308

295309
@property
296310
def footnote_numbering_start_value(self) -> ST_DecimalNumber | None:
297-
"""
298-
The value of the ``w:val`` attribute in the ``<w:numStart>`` child
311+
"""The value of the ``w:val`` attribute in the ``<w:numStart>`` child
299312
element of ``<w:footnotePr>`` element, as a |Number|, or |None| if either the element or the
300-
attribute is not present.
301-
"""
313+
attribute is not present."""
302314
fPr = self.footnotePr
303315
if fPr is None or fPr.numStart is None:
304316
return None
@@ -311,19 +323,17 @@ def footnote_numbering_start_value(self, value: ST_DecimalNumber | None):
311323
numRestart = fPr.get_or_add_numRestart()
312324
numStart.val = value
313325
if len(numRestart.values()) == 0:
314-
numRestart.val = 'continuous'
326+
numRestart.val = "continuous"
315327
elif value != 1:
316-
numRestart.val = 'continuous'
328+
numRestart.val = "continuous"
317329
msg = "When ``<w:numStart> is not 1, then ``<w:numRestart>`` must be 'continuous'."
318330
warn(msg, UserWarning, stacklevel=2)
319331

320332
@property
321333
def footnote_position(self) -> ST_FtnPos | None:
322-
"""
323-
The value of the ``w:val`` attribute in the ``<w:pos>`` child
334+
"""The value of the ``w:val`` attribute in the ``<w:pos>`` child
324335
element of ``<w:footnotePr>`` element, as a |String|, or |None| if either the element or the
325-
attribute is not present.
326-
"""
336+
attribute is not present."""
327337
fPr = self.footnotePr
328338
if fPr is None or fPr.pos is None:
329339
return None

‎src/docx/oxml/simpletypes.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -222,15 +222,12 @@ class ST_DrawingElementId(XsdUnsignedInt):
222222

223223

224224
class ST_FtnPos(XsdString):
225-
226225
@classmethod
227226
def validate(cls, value):
228227
cls.validate_string(value)
229-
valid_values = ('pageBottom', 'beneathText', 'sectEnd', 'docEnd')
228+
valid_values = ("pageBottom", "beneathText", "sectEnd", "docEnd")
230229
if value not in valid_values:
231-
raise ValueError(
232-
"must be one of %s, got '%s'" % (valid_values, value)
233-
)
230+
raise ValueError("must be one of %s, got '%s'" % (valid_values, value))
234231

235232

236233
class ST_HexColor(BaseStringType):
@@ -322,15 +319,13 @@ class ST_RelationshipId(XsdString):
322319

323320

324321
class ST_RestartNumber(XsdString):
325-
326322
@classmethod
327323
def validate(cls, value):
328324
cls.validate_string(value)
329-
valid_values = ('continuous', 'eachSect', 'eachPage')
325+
valid_values = ("continuous", "eachSect", "eachPage")
330326
if value not in valid_values:
331-
raise ValueError(
332-
"must be one of %s, got '%s'" % (valid_values, value)
333-
)
327+
raise ValueError("must be one of %s, got '%s'" % (valid_values, value))
328+
334329

335330
class ST_SignedTwipsMeasure(XsdInt):
336331
@classmethod
Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
"""Custom element classes related to footnote references (CT_FtnEdnRef)."""
22

3-
from docx.oxml.xmlchemy import (
4-
BaseOxmlElement, RequiredAttribute, OptionalAttribute
5-
)
6-
from docx.oxml.simpletypes import (
7-
ST_DecimalNumber, ST_OnOff
8-
)
3+
from docx.oxml.simpletypes import ST_DecimalNumber, ST_OnOff
4+
from docx.oxml.xmlchemy import BaseOxmlElement, OptionalAttribute, RequiredAttribute
5+
96

107
class CT_FtnEdnRef(BaseOxmlElement):
11-
"""
12-
``<w:footnoteReference>`` element, containing the properties for a footnote reference
13-
"""
14-
id = RequiredAttribute('w:id', ST_DecimalNumber)
15-
customMarkFollows = OptionalAttribute('w:customMarkFollows', ST_OnOff)
8+
"""``<w:footnoteReference>`` element, containing the properties for a footnote reference"""
9+
10+
id = RequiredAttribute("w:id", ST_DecimalNumber)
11+
customMarkFollows = OptionalAttribute("w:customMarkFollows", ST_OnOff)

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

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313

1414
if TYPE_CHECKING:
1515
from docx.oxml.shape import CT_Anchor, CT_Inline
16+
from docx.oxml.text.footnote_reference import CT_FtnEdnRef
1617
from docx.oxml.text.pagebreak import CT_LastRenderedPageBreak
1718
from docx.oxml.text.parfmt import CT_TabStop
18-
from docx.oxml.text.footnote_reference import CT_FtnEdnRef
1919

2020
# ------------------------------------------------------------------------------------
2121
# Run-level elements
@@ -39,15 +39,13 @@ class CT_R(BaseOxmlElement):
3939
drawing = ZeroOrMore("w:drawing")
4040
t = ZeroOrMore("w:t")
4141
tab = ZeroOrMore("w:tab")
42-
footnoteReference = ZeroOrMore('w:footnoteReference')
42+
footnoteReference = ZeroOrMore("w:footnoteReference")
4343

4444
def add_footnoteReference(self, id: int) -> CT_FtnEdnRef:
45-
"""
46-
Return a newly added ``<w:footnoteReference>`` element containing
47-
the footnote reference id.
48-
"""
45+
"""Return a newly added ``<w:footnoteReference>`` element containing
46+
the footnote reference id."""
4947
rPr = self._add_rPr()
50-
rPr.style = 'FootnoteReference'
48+
rPr.style = "FootnoteReference"
5149
new_fr = self._add_footnoteReference()
5250
new_fr.id = id
5351
return new_fr
@@ -110,20 +108,17 @@ def lastRenderedPageBreaks(self) -> List[CT_LastRenderedPageBreak]:
110108

111109
@property
112110
def footnote_reference_ids(self) -> List[int] | None:
113-
"""
114-
Return all footnote reference ids (``<w:footnoteReference>``), or |None| if not present.
115-
"""
111+
"""Return all footnote reference ids (``<w:footnoteReference>``), or |None| if not present."""
116112
references = []
117113
for child in self:
118-
if child.tag == qn('w:footnoteReference'):
119-
references.append(child.id)
114+
if child.tag == qn("w:footnoteReference"):
115+
references.append(child.id)
120116
if references == []:
121117
references = None
122118
return references
123119

124120
def increment_containing_footnote_reference_ids(self) -> CT_FtnEdnRef | None:
125-
"""
126-
Increment all footnote reference ids by one if they exist.
121+
"""Increment all footnote reference ids by one if they exist.
127122
Return all footnote reference ids (``<w:footnoteReference>``), or |None| if not present.
128123
"""
129124
if self.footnoteReference_lst is not None:

‎src/docx/parts/footnotes.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,36 @@
22

33
import os
44

5+
from docx.footnotes import Footnotes
56
from docx.opc.constants import CONTENT_TYPE as CT
67
from docx.opc.packuri import PackURI
78
from docx.oxml import parse_xml
8-
from docx.footnotes import Footnotes
99
from docx.parts.story import StoryPart
1010

1111

1212
class FootnotesPart(StoryPart):
13-
"""
14-
Proxy for the footnotes.xml part containing footnotes definitions for a document.
15-
"""
13+
"""Proxy for the footnotes.xml part containing footnotes definitions for a document."""
14+
1615
@classmethod
1716
def default(cls, package):
18-
"""
19-
Return a newly created footnote part, containing a default set of elements.
20-
"""
21-
partname = PackURI('/word/footnotes.xml')
17+
"""Return a newly created footnote part, containing a default set of elements."""
18+
partname = PackURI("/word/footnotes.xml")
2219
content_type = CT.WML_FOOTNOTES
2320
element = parse_xml(cls._default_footnote_xml())
2421
return cls(partname, content_type, element, package)
2522

2623
@property
2724
def footnotes(self):
28-
"""
29-
The |Footnotes| instance containing the footnotes (<w:footnotes> element
30-
proxies) for this footnotes part.
31-
"""
25+
"""The |Footnotes| instance containing the footnotes (<w:footnotes> element
26+
proxies) for this footnotes part."""
3227
return Footnotes(self.element, self)
3328

3429
@classmethod
3530
def _default_footnote_xml(cls):
36-
"""
37-
Return a bytestream containing XML for a default styles part.
38-
"""
31+
"""Return a bytestream containing XML for a default styles part."""
3932
path = os.path.join(
40-
os.path.split(__file__)[0], '..', 'templates',
41-
'default-footnotes.xml'
33+
os.path.split(__file__)[0], "..", "templates", "default-footnotes.xml"
4234
)
43-
with open(path, 'rb') as f:
35+
with open(path, "rb") as f:
4436
xml_bytes = f.read()
4537
return xml_bytes

‎src/docx/text/paragraph.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
if TYPE_CHECKING:
1818
import docx.types as t
1919
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
20+
from docx.oxml.footnote import CT_FtnEnd
2021
from docx.oxml.text.paragraph import CT_P
2122
from docx.styles.style import CharacterStyle
22-
from docx.oxml.footnote import CT_FtnEnd
2323

2424

2525
class Paragraph(StoryChild):
@@ -30,8 +30,8 @@ def __init__(self, p: CT_P, parent: t.ProvidesStoryPart):
3030
self._p = self._element = p
3131

3232
def add_footnote(self) -> CT_FtnEnd:
33-
"""
34-
Append a run that contains a ``<w:footnoteReferenceId>`` element.
33+
"""Append a run that contains a ``<w:footnoteReferenceId>`` element.
34+
3535
The footnotes are kept in order by `footnote_reference_id`, so
3636
the appropriate id is calculated based on the current state.
3737
"""

‎src/docx/text/run.py

Lines changed: 1 addition & 1 deletion
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, List, Iterator, cast
5+
from typing import IO, TYPE_CHECKING, Iterator, List, cast
66

77
from docx.drawing import Drawing
88
from docx.enum.style import WD_STYLE_TYPE

‎tests/test_document.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from docx.document import Document, _Body
1313
from docx.enum.section import WD_SECTION
1414
from docx.enum.text import WD_BREAK
15+
from docx.footnotes import Footnotes
1516
from docx.opc.coreprops import CoreProperties
1617
from docx.oxml.document import CT_Document
1718
from docx.parts.document import DocumentPart
@@ -23,7 +24,6 @@
2324
from docx.table import Table
2425
from docx.text.paragraph import Paragraph
2526
from docx.text.run import Run
26-
from docx.footnotes import Footnotes
2727

2828
from .unitutil.cxml import element, xml
2929
from .unitutil.mock import Mock, class_mock, instance_mock, method_mock, property_mock
@@ -251,11 +251,19 @@ def core_props_fixture(self, document_part_, core_properties_):
251251
document_part_.core_properties = core_properties_
252252
return document, core_properties_
253253

254-
@pytest.fixture(params=[
255-
('w:footnotes/(w:footnote{w:id=-1}/w:p/w:r/w:t"minus one note", w:footnote{w:id=0}/w:p/w:r/w:t"zero note")'),
256-
('w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note")'),
257-
('w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note", w:footnote{w:id=3}/w:p/w:r/w:t"third note")'),
258-
])
254+
@pytest.fixture(
255+
params=[
256+
(
257+
'w:footnotes/(w:footnote{w:id=-1}/w:p/w:r/w:t"minus one note", w:footnote{w:id=0}/w:p/w:r/w:t"zero note")'
258+
),
259+
(
260+
'w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note")'
261+
),
262+
(
263+
'w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note", w:footnote{w:id=3}/w:p/w:r/w:t"third note")'
264+
),
265+
]
266+
)
259267
def footnotes_fixture(self, request, document_part_):
260268
footnotes_cxml = request.param
261269
document = Document(None, document_part_)

‎tests/test_section.py

Lines changed: 92 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,56 @@ def it_can_change_whether_the_document_has_distinct_odd_and_even_headers(
164164
@pytest.mark.parametrize(
165165
("sectPr_cxml", "footnote_prop_name", "expected_value"),
166166
[
167-
("w:sectPr/w:footnotePr/w:numFmt{w:val=decimal}", "footnote_number_format", "decimal"),
168-
("w:sectPr/w:footnotePr/w:numFmt{w:val=upperRoman}", "footnote_number_format", "upperRoman"),
169-
("w:sectPr/w:footnotePr/w:numFmt{w:val=lowerLetter}", "footnote_number_format", "lowerLetter"),
170-
("w:sectPr/w:footnotePr/w:numFmt{w:val=bullet}", "footnote_number_format", "bullet"),
171-
("w:sectPr/w:footnotePr/w:pos{w:val=pageBottom}", "footnote_number_format", None),
172-
("w:sectPr/w:footnotePr/w:pos{w:val=pageBottom}", "footnote_position", "pageBottom"),
173-
("w:sectPr/w:footnotePr/w:numStart{w:val=5}", "footnote_numbering_start_value", 5),
174-
("w:sectPr/w:footnotePr/w:numStart{w:val=13}", "footnote_numbering_start_value", 13),
175-
("w:sectPr/w:footnotePr/w:numRestart{w:val=eachSect}", "footnote_numbering_restart_location", "eachSect"),
176-
("w:sectPr/w:footnotePr/w:numRestart{w:val=eachPage}", "footnote_numbering_restart_location", "eachPage"),
167+
(
168+
"w:sectPr/w:footnotePr/w:numFmt{w:val=decimal}",
169+
"footnote_number_format",
170+
"decimal",
171+
),
172+
(
173+
"w:sectPr/w:footnotePr/w:numFmt{w:val=upperRoman}",
174+
"footnote_number_format",
175+
"upperRoman",
176+
),
177+
(
178+
"w:sectPr/w:footnotePr/w:numFmt{w:val=lowerLetter}",
179+
"footnote_number_format",
180+
"lowerLetter",
181+
),
182+
(
183+
"w:sectPr/w:footnotePr/w:numFmt{w:val=bullet}",
184+
"footnote_number_format",
185+
"bullet",
186+
),
187+
(
188+
"w:sectPr/w:footnotePr/w:pos{w:val=pageBottom}",
189+
"footnote_number_format",
190+
None,
191+
),
192+
(
193+
"w:sectPr/w:footnotePr/w:pos{w:val=pageBottom}",
194+
"footnote_position",
195+
"pageBottom",
196+
),
197+
(
198+
"w:sectPr/w:footnotePr/w:numStart{w:val=5}",
199+
"footnote_numbering_start_value",
200+
5,
201+
),
202+
(
203+
"w:sectPr/w:footnotePr/w:numStart{w:val=13}",
204+
"footnote_numbering_start_value",
205+
13,
206+
),
207+
(
208+
"w:sectPr/w:footnotePr/w:numRestart{w:val=eachSect}",
209+
"footnote_numbering_restart_location",
210+
"eachSect",
211+
),
212+
(
213+
"w:sectPr/w:footnotePr/w:numRestart{w:val=eachPage}",
214+
"footnote_numbering_restart_location",
215+
"eachPage",
216+
),
177217
],
178218
)
179219
def it_knows_its_footnote_properties(
@@ -193,13 +233,48 @@ def it_knows_its_footnote_properties(
193233
@pytest.mark.parametrize(
194234
("sectPr_cxml", "footnote_prop_name", "value", "expected_cxml"),
195235
[
196-
("w:sectPr", "footnote_number_format", "upperRoman", "w:sectPr/w:footnotePr/w:numFmt{w:val=upperRoman}"),
197-
("w:sectPr/w:footnotePr/w:numFmt{w:val=decimal}", "footnote_number_format", "upperRoman", "w:sectPr/w:footnotePr/w:numFmt{w:val=upperRoman}"),
198-
("w:sectPr", "footnote_position", "pageBottom", "w:sectPr/w:footnotePr/w:pos{w:val=pageBottom}"),
199-
("w:sectPr", "footnote_numbering_start_value", 1, "w:sectPr/w:footnotePr/(w:numStart{w:val=1},w:numRestart{w:val=continuous})"),
200-
("w:sectPr", "footnote_numbering_start_value", 5, "w:sectPr/w:footnotePr/(w:numStart{w:val=5},w:numRestart{w:val=continuous})"),
201-
("w:sectPr", "footnote_numbering_restart_location", "eachSect", "w:sectPr/w:footnotePr/(w:numStart{w:val=1},w:numRestart{w:val=eachSect})"),
202-
("w:sectPr", "footnote_numbering_restart_location", "continuous", "w:sectPr/w:footnotePr/(w:numStart{w:val=1},w:numRestart{w:val=continuous})"),
236+
(
237+
"w:sectPr",
238+
"footnote_number_format",
239+
"upperRoman",
240+
"w:sectPr/w:footnotePr/w:numFmt{w:val=upperRoman}",
241+
),
242+
(
243+
"w:sectPr/w:footnotePr/w:numFmt{w:val=decimal}",
244+
"footnote_number_format",
245+
"upperRoman",
246+
"w:sectPr/w:footnotePr/w:numFmt{w:val=upperRoman}",
247+
),
248+
(
249+
"w:sectPr",
250+
"footnote_position",
251+
"pageBottom",
252+
"w:sectPr/w:footnotePr/w:pos{w:val=pageBottom}",
253+
),
254+
(
255+
"w:sectPr",
256+
"footnote_numbering_start_value",
257+
1,
258+
"w:sectPr/w:footnotePr/(w:numStart{w:val=1},w:numRestart{w:val=continuous})",
259+
),
260+
(
261+
"w:sectPr",
262+
"footnote_numbering_start_value",
263+
5,
264+
"w:sectPr/w:footnotePr/(w:numStart{w:val=5},w:numRestart{w:val=continuous})",
265+
),
266+
(
267+
"w:sectPr",
268+
"footnote_numbering_restart_location",
269+
"eachSect",
270+
"w:sectPr/w:footnotePr/(w:numStart{w:val=1},w:numRestart{w:val=eachSect})",
271+
),
272+
(
273+
"w:sectPr",
274+
"footnote_numbering_restart_location",
275+
"continuous",
276+
"w:sectPr/w:footnotePr/(w:numStart{w:val=1},w:numRestart{w:val=continuous})",
277+
),
203278
],
204279
)
205280
def it_can_change_its_footnote_properties(

‎tests/text/test_paragraph.py

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

77
from docx import types as t
8+
from docx.document import Document
89
from docx.enum.style import WD_STYLE_TYPE
910
from docx.enum.text import WD_ALIGN_PARAGRAPH
11+
from docx.footnotes import Footnotes
1012
from docx.oxml.text.paragraph import CT_P
1113
from docx.oxml.text.run import CT_R
1214
from docx.parts.document import DocumentPart
1315
from docx.text.paragraph import Paragraph
1416
from docx.text.parfmt import ParagraphFormat
1517
from docx.text.run import Run
16-
from docx.footnotes import Footnotes
17-
from docx.document import Document
1818

1919
from ..unitutil.cxml import element, xml
2020
from ..unitutil.mock import call, class_mock, instance_mock, method_mock, property_mock
@@ -221,15 +221,33 @@ def it_inserts_a_paragraph_before_to_help(self, _insert_before_fixture):
221221

222222
# fixtures -------------------------------------------------------
223223

224-
@pytest.fixture(params=[
225-
('w:p/w:r/w:footnoteReference{w:id=2}', 'w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note")', [2]),
226-
('w:p/w:r/w:footnoteReference{w:id=1}', 'w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note")', [1]),
227-
('w:p/w:r/(w:footnoteReference{w:id=1}, w:footnoteReference{w:id=2})', 'w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note")', [1,2]),
228-
('w:p/w:r/(w:footnoteReference{w:id=3}, w:footnoteReference{w:id=2})', 'w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note", w:footnote{w:id=3}/w:p/w:r/w:t"third note")', [3,2]),
229-
])
224+
@pytest.fixture(
225+
params=[
226+
(
227+
"w:p/w:r/w:footnoteReference{w:id=2}",
228+
'w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note")',
229+
[2],
230+
),
231+
(
232+
"w:p/w:r/w:footnoteReference{w:id=1}",
233+
'w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note")',
234+
[1],
235+
),
236+
(
237+
"w:p/w:r/(w:footnoteReference{w:id=1}, w:footnoteReference{w:id=2})",
238+
'w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note")',
239+
[1, 2],
240+
),
241+
(
242+
"w:p/w:r/(w:footnoteReference{w:id=3}, w:footnoteReference{w:id=2})",
243+
'w:footnotes/(w:footnote{w:id=1}/w:p/w:r/w:t"first note", w:footnote{w:id=2}/w:p/w:r/w:t"second note", w:footnote{w:id=3}/w:p/w:r/w:t"third note")',
244+
[3, 2],
245+
),
246+
]
247+
)
230248
def footnotes_fixture(self, request, document_part_):
231249
paragraph_cxml, footnotes_cxml, footnote_ids_in_p = request.param
232-
document_elm = element('w:document/w:body')
250+
document_elm = element("w:document/w:body")
233251
document = Document(document_elm, document_part_)
234252
paragraph = Paragraph(element(paragraph_cxml), document._body)
235253
footnotes = Footnotes(element(footnotes_cxml), None)

0 commit comments

Comments
 (0)
Please sign in to comment.