Skip to content

Commit b7f5903

Browse files
committed
rfctr: resolve StoryChild conflation
We need one type for the base-class role and a different one for the parameter-type role. The base-class is concrete, the parameter-type is abstract. Introduce `docx.types.ProvidesStoryPart` for the parameter-type role. Add `ProvidesXmlPart` while we're at it for use by `ElementProxy`, which probably needs a little more thought too.
1 parent 523328c commit b7f5903

11 files changed

+59
-33
lines changed

src/docx/drawing/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
class Drawing(Parented):
1111
"""Container for a DrawingML object."""
1212

13-
def __init__(self, drawing: CT_Drawing, parent: t.StoryChild):
13+
def __init__(self, drawing: CT_Drawing, parent: t.ProvidesStoryPart):
1414
super().__init__(parent)
1515
self._parent = parent
1616
self._drawing = self._element = drawing

src/docx/text/hyperlink.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class Hyperlink(Parented):
2323
stored.
2424
"""
2525

26-
def __init__(self, hyperlink: CT_Hyperlink, parent: t.StoryChild):
26+
def __init__(self, hyperlink: CT_Hyperlink, parent: t.ProvidesStoryPart):
2727
super().__init__(parent)
2828
self._parent = parent
2929
self._hyperlink = self._element = hyperlink

src/docx/text/pagebreak.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ class RenderedPageBreak(Parented):
3636
"""
3737

3838
def __init__(
39-
self, lastRenderedPageBreak: CT_LastRenderedPageBreak, parent: t.StoryChild
39+
self,
40+
lastRenderedPageBreak: CT_LastRenderedPageBreak,
41+
parent: t.ProvidesStoryPart,
4042
):
4143
super().__init__(parent)
4244
self._element = lastRenderedPageBreak

src/docx/text/paragraph.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from typing_extensions import Self
88

9+
from docx import types as t
910
from docx.enum.style import WD_STYLE_TYPE
1011
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
1112
from docx.oxml.text.paragraph import CT_P
@@ -21,7 +22,7 @@
2122
class Paragraph(StoryChild):
2223
"""Proxy object wrapping a `<w:p>` element."""
2324

24-
def __init__(self, p: CT_P, parent: StoryChild):
25+
def __init__(self, p: CT_P, parent: t.ProvidesStoryPart):
2526
super(Paragraph, self).__init__(parent)
2627
self._p = self._element = p
2728

src/docx/text/run.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from typing import IO, TYPE_CHECKING, Iterator, cast
66

7+
from docx import types as t
78
from docx.drawing import Drawing
89
from docx.enum.style import WD_STYLE_TYPE
910
from docx.enum.text import WD_BREAK
@@ -30,7 +31,7 @@ class Run(StoryChild):
3031
the style hierarchy.
3132
"""
3233

33-
def __init__(self, r: CT_R, parent: StoryChild):
34+
def __init__(self, r: CT_R, parent: t.ProvidesStoryPart):
3435
super().__init__(parent)
3536
self._r = self._element = self.element = r
3637

src/docx/types.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
from __future__ import annotations
44

5+
from typing import TYPE_CHECKING
6+
57
from typing_extensions import Protocol
68

7-
from docx.parts.story import StoryPart
9+
if TYPE_CHECKING:
10+
from docx.opc.part import XmlPart
11+
from docx.parts.story import StoryPart
812

913

10-
class StoryChild(Protocol):
11-
"""An object that can fulfill the `parent` role in a `Parented` class.
14+
class ProvidesStoryPart(Protocol):
15+
"""An object that provides access to the StoryPart.
1216
1317
This type is for objects that have a story part like document or header as their
1418
root part.
@@ -17,3 +21,16 @@ class StoryChild(Protocol):
1721
@property
1822
def part(self) -> StoryPart:
1923
...
24+
25+
26+
class ProvidesXmlPart(Protocol):
27+
"""An object that provides access to its XmlPart.
28+
29+
This type is for objects that need access to their part but it either isn't a
30+
StoryPart or they don't care, possibly because they just need access to the package
31+
or related parts.
32+
"""
33+
34+
@property
35+
def part(self) -> XmlPart:
36+
...

tests/conftest.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
"""pytest fixtures that are shared across test modules."""
22

3+
from __future__ import annotations
4+
5+
from typing import TYPE_CHECKING
6+
37
import pytest
48

5-
from docx import types as t
6-
from docx.parts.story import StoryPart
9+
if TYPE_CHECKING:
10+
from docx import types as t
11+
from docx.parts.story import StoryPart
712

813

914
@pytest.fixture
10-
def fake_parent() -> t.StoryChild:
11-
class StoryChild:
15+
def fake_parent() -> t.ProvidesStoryPart:
16+
class ProvidesStoryPart:
1217
@property
1318
def part(self) -> StoryPart:
1419
raise NotImplementedError
1520

16-
return StoryChild()
21+
return ProvidesStoryPart()

tests/text/test_hyperlink.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class DescribeHyperlink:
2626
],
2727
)
2828
def it_knows_the_hyperlink_address(
29-
self, hlink_cxml: str, expected_value: str, fake_parent: t.StoryChild
29+
self, hlink_cxml: str, expected_value: str, fake_parent: t.ProvidesStoryPart
3030
):
3131
hlink = cast(CT_Hyperlink, element(hlink_cxml))
3232
hyperlink = Hyperlink(hlink, fake_parent)
@@ -44,7 +44,7 @@ def it_knows_the_hyperlink_address(
4444
],
4545
)
4646
def it_knows_whether_it_contains_a_page_break(
47-
self, hlink_cxml: str, expected_value: bool, fake_parent: t.StoryChild
47+
self, hlink_cxml: str, expected_value: bool, fake_parent: t.ProvidesStoryPart
4848
):
4949
hlink = cast(CT_Hyperlink, element(hlink_cxml))
5050
hyperlink = Hyperlink(hlink, fake_parent)
@@ -59,7 +59,7 @@ def it_knows_whether_it_contains_a_page_break(
5959
],
6060
)
6161
def it_knows_the_link_fragment_when_there_is_one(
62-
self, hlink_cxml: str, expected_value: str, fake_parent: t.StoryChild
62+
self, hlink_cxml: str, expected_value: str, fake_parent: t.ProvidesStoryPart
6363
):
6464
hlink = cast(CT_Hyperlink, element(hlink_cxml))
6565
hyperlink = Hyperlink(hlink, fake_parent)
@@ -78,7 +78,7 @@ def it_knows_the_link_fragment_when_there_is_one(
7878
],
7979
)
8080
def it_provides_access_to_the_runs_it_contains(
81-
self, hlink_cxml: str, count: int, fake_parent: t.StoryChild
81+
self, hlink_cxml: str, count: int, fake_parent: t.ProvidesStoryPart
8282
):
8383
hlink = cast(CT_Hyperlink, element(hlink_cxml))
8484
hyperlink = Hyperlink(hlink, fake_parent)
@@ -100,7 +100,7 @@ def it_provides_access_to_the_runs_it_contains(
100100
],
101101
)
102102
def it_knows_the_visible_text_of_the_link(
103-
self, hlink_cxml: str, expected_text: str, fake_parent: t.StoryChild
103+
self, hlink_cxml: str, expected_text: str, fake_parent: t.ProvidesStoryPart
104104
):
105105
hlink = cast(CT_Hyperlink, element(hlink_cxml))
106106
hyperlink = Hyperlink(hlink, fake_parent)
@@ -122,7 +122,7 @@ def it_knows_the_visible_text_of_the_link(
122122
],
123123
)
124124
def it_knows_the_full_url_for_web_addresses(
125-
self, hlink_cxml: str, expected_value: str, fake_parent: t.StoryChild
125+
self, hlink_cxml: str, expected_value: str, fake_parent: t.ProvidesStoryPart
126126
):
127127
hlink = cast(CT_Hyperlink, element(hlink_cxml))
128128
hyperlink = Hyperlink(hlink, fake_parent)
@@ -132,7 +132,7 @@ def it_knows_the_full_url_for_web_addresses(
132132
# -- fixtures --------------------------------------------------------------------
133133

134134
@pytest.fixture
135-
def fake_parent(self, story_part: Mock, rel: Mock) -> t.StoryChild:
135+
def fake_parent(self, story_part: Mock, rel: Mock) -> t.ProvidesStoryPart:
136136
class StoryChild:
137137
@property
138138
def part(self) -> StoryPart:

tests/text/test_pagebreak.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class DescribeRenderedPageBreak:
1717
"""Unit-test suite for the docx.text.pagebreak.RenderedPageBreak object."""
1818

1919
def it_raises_on_preceding_fragment_when_page_break_is_not_first_in_paragrah(
20-
self, fake_parent: t.StoryChild
20+
self, fake_parent: t.ProvidesStoryPart
2121
):
2222
p_cxml = 'w:p/(w:r/(w:t"abc",w:lastRenderedPageBreak,w:lastRenderedPageBreak))'
2323
p = cast(CT_P, element(p_cxml))
@@ -28,7 +28,7 @@ def it_raises_on_preceding_fragment_when_page_break_is_not_first_in_paragrah(
2828
page_break.preceding_paragraph_fragment
2929

3030
def it_produces_None_for_preceding_fragment_when_page_break_is_leading(
31-
self, fake_parent: t.StoryChild
31+
self, fake_parent: t.ProvidesStoryPart
3232
):
3333
"""A page-break with no preceding content is "leading"."""
3434
p_cxml = 'w:p/(w:pPr/w:ind,w:r/(w:lastRenderedPageBreak,w:t"foo",w:t"bar"))'
@@ -41,7 +41,7 @@ def it_produces_None_for_preceding_fragment_when_page_break_is_leading(
4141
assert preceding_fragment is None
4242

4343
def it_can_split_off_the_preceding_paragraph_content_when_in_a_run(
44-
self, fake_parent: t.StoryChild
44+
self, fake_parent: t.ProvidesStoryPart
4545
):
4646
p_cxml = (
4747
"w:p/("
@@ -61,7 +61,7 @@ def it_can_split_off_the_preceding_paragraph_content_when_in_a_run(
6161
assert preceding_fragment._p.xml == xml(expected_cxml)
6262

6363
def and_it_can_split_off_the_preceding_paragraph_content_when_in_a_hyperlink(
64-
self, fake_parent: t.StoryChild
64+
self, fake_parent: t.ProvidesStoryPart
6565
):
6666
p_cxml = (
6767
"w:p/("
@@ -81,7 +81,7 @@ def and_it_can_split_off_the_preceding_paragraph_content_when_in_a_hyperlink(
8181
assert preceding_fragment._p.xml == xml(expected_cxml)
8282

8383
def it_raises_on_following_fragment_when_page_break_is_not_first_in_paragrah(
84-
self, fake_parent: t.StoryChild
84+
self, fake_parent: t.ProvidesStoryPart
8585
):
8686
p_cxml = 'w:p/(w:r/(w:lastRenderedPageBreak,w:lastRenderedPageBreak,w:t"abc"))'
8787
p = cast(CT_P, element(p_cxml))
@@ -92,7 +92,7 @@ def it_raises_on_following_fragment_when_page_break_is_not_first_in_paragrah(
9292
page_break.following_paragraph_fragment
9393

9494
def it_produces_None_for_following_fragment_when_page_break_is_trailing(
95-
self, fake_parent: t.StoryChild
95+
self, fake_parent: t.ProvidesStoryPart
9696
):
9797
"""A page-break with no following content is "trailing"."""
9898
p_cxml = 'w:p/(w:pPr/w:ind,w:r/(w:t"foo",w:t"bar",w:lastRenderedPageBreak))'
@@ -105,7 +105,7 @@ def it_produces_None_for_following_fragment_when_page_break_is_trailing(
105105
assert following_fragment is None
106106

107107
def it_can_split_off_the_following_paragraph_content_when_in_a_run(
108-
self, fake_parent: t.StoryChild
108+
self, fake_parent: t.ProvidesStoryPart
109109
):
110110
p_cxml = (
111111
"w:p/("
@@ -125,7 +125,7 @@ def it_can_split_off_the_following_paragraph_content_when_in_a_run(
125125
assert following_fragment._p.xml == xml(expected_cxml)
126126

127127
def and_it_can_split_off_the_following_paragraph_content_when_in_a_hyperlink(
128-
self, fake_parent: t.StoryChild
128+
self, fake_parent: t.ProvidesStoryPart
129129
):
130130
p_cxml = (
131131
"w:p/("

tests/text/test_paragraph.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class DescribeParagraph:
3131
],
3232
)
3333
def it_knows_whether_it_contains_a_page_break(
34-
self, p_cxml: str, expected_value: bool, fake_parent: t.StoryChild
34+
self, p_cxml: str, expected_value: bool, fake_parent: t.ProvidesStoryPart
3535
):
3636
p = cast(CT_P, element(p_cxml))
3737
paragraph = Paragraph(p, fake_parent)
@@ -50,7 +50,7 @@ def it_knows_whether_it_contains_a_page_break(
5050
],
5151
)
5252
def it_provides_access_to_the_hyperlinks_it_contains(
53-
self, p_cxml: str, count: int, fake_parent: t.StoryChild
53+
self, p_cxml: str, count: int, fake_parent: t.ProvidesStoryPart
5454
):
5555
p = cast(CT_P, element(p_cxml))
5656
paragraph = Paragraph(p, fake_parent)
@@ -72,7 +72,7 @@ def it_provides_access_to_the_hyperlinks_it_contains(
7272
],
7373
)
7474
def it_can_iterate_its_inner_content_items(
75-
self, p_cxml: str, expected: List[str], fake_parent: t.StoryChild
75+
self, p_cxml: str, expected: List[str], fake_parent: t.ProvidesStoryPart
7676
):
7777
p = cast(CT_P, element(p_cxml))
7878
paragraph = Paragraph(p, fake_parent)
@@ -120,7 +120,7 @@ def it_can_change_its_paragraph_style(self, style_set_fixture):
120120
],
121121
)
122122
def it_provides_access_to_the_rendered_page_breaks_it_contains(
123-
self, p_cxml: str, count: int, fake_parent: t.StoryChild
123+
self, p_cxml: str, count: int, fake_parent: t.ProvidesStoryPart
124124
):
125125
p = cast(CT_P, element(p_cxml))
126126
paragraph = Paragraph(p, fake_parent)

tests/text/test_run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def it_knows_whether_it_contains_a_page_break(
7070
],
7171
)
7272
def it_can_iterate_its_inner_content_items(
73-
self, r_cxml: str, expected: List[str], fake_parent: t.StoryChild
73+
self, r_cxml: str, expected: List[str], fake_parent: t.ProvidesStoryPart
7474
):
7575
r = cast(CT_R, element(r_cxml))
7676
run = Run(r, fake_parent)

0 commit comments

Comments
 (0)