Skip to content

Commit 6f65019

Browse files
committed
feat: HandoutMaster + add_text_watermark helper — Phase 5, closing #20
Closes the headers/footers epic (#20). Phase 1 (PR #48) shipped the `CT_HandoutMaster` OOXML wrapper; Phases 2-4 (PRs #49, #50, #51) built out the public headers/footers/fields surface. Phase 5 adds the two remaining pieces: a public `HandoutMaster` Python class so users can read a handout master's visibility settings without dropping into XML, and a `SlideMaster.add_text_watermark()` helper that wraps the common "DRAFT"-style watermark pattern into one call. Why this is small. `_HeaderFooterVisibility` (Phase 2) and `CT_HandoutMaster` (Phase 1) already do the heavy lifting; Phase 5 just wires a Python proxy + part + accessor onto them and adds a thin convenience method. Changes: - pptx.slide.HandoutMaster — new class, inherits `_HeaderFooterVisibility, _BaseMaster` in the same order as `NotesMaster`. Docstring names the deferred auto-create. - pptx.slide.SlideMaster.add_text_watermark — new method. Adds a centered 6in x 1.5in textbox to the master at standard 10in x 7.5in slide coordinates (left=2in, top=3in). Text gets `PP_ALIGN.CENTER`; the first run gets `font.name`, `font.size`, mid-gray solid fill, and the configured `transparency` (default 0.7). Returns the new `Shape` so callers can re-position or restyle. - pptx.slide._HeaderFooterVisibility — `_element` type union widened to include `CT_HandoutMaster` so the mixin types cleanly under `HandoutMaster`. - pptx.parts.slide.HandoutMasterPart — new `BaseSlidePart` subclass. Defines `handout_master` lazyproperty; deliberately omits `create_default` / `_new` / `_new_theme_part` because no `handoutMaster.xml` template ships in this fork yet. - pptx.parts.presentation.PresentationPart.handout_master_part — new lazyproperty. Returns `part_related_by(RT.HANDOUT_MASTER)` when present; raises `ValueError` with a "no handout master" message (chained `from KeyError`) when absent. Unlike `notes_master_part`, this does NOT auto-create. - pptx.parts.presentation.PresentationPart.handout_master — new lazyproperty pass-through to `.handout_master_part.handout_master`. - pptx.presentation.Presentation.handout_master — new property pass-through, matching the shape of `notes_master`. - pptx.shapes.shapetree.MasterShapes.add_textbox — new method, parallel to the existing `SlideShapes.add_textbox`. Needed so `SlideMaster.add_text_watermark` can drop a textbox directly on the master's shape tree (master shapes previously had no `add_textbox`, only the slide / layout collections did). - pptx/__init__.py — registers `HandoutMasterPart` against `CT.PML_HANDOUT_MASTER` in `content_type_to_part_class_map` and adds it to the cleanup `del (...)` tuple. Out of scope for Phase 5 (deliberate, see ISA Out of Scope): - Auto-create of handout master when absent. Synthesizing one from scratch needs a known-good `handoutMaster.xml` template, and shipping that template + the theme wiring is risky without a reference deck. Phase 6+ if real users ask. The error message surfaces the deferral explicitly. - `HandoutMaster.add_placeholder()` / placeholder cloning — read-only this phase; the inherited visibility props are write-enabled, but the placeholder set on the handout master is static. - Image-watermark helper — `master.shapes.add_picture(...)` already does this in one line; wrapping it adds little value. - Per-slide watermark — `add_text_watermark` lives on `SlideMaster` so every slide using that master gets the watermark by inheritance; per-slide watermark is just `slide.shapes.add_textbox(...)` today. - `MSO_WATERMARK_PRESET` enum or canned styles — string text + numeric transparency is the entire API surface. Anti-criteria upheld: - No `handoutMaster.xml` template file added (verified by `find src -name '*handoutMaster*'`). - `Presentation.handout_master` does NOT silently auto-create — the `but_it_raises_ValueError_when_no_handout_master_is_present` test in `tests/parts/test_presentation.py` pins this. - No `add_image_watermark` or `MSO_WATERMARK_PRESET` (verified by `grep -rn 'add_image_watermark|MSO_WATERMARK' src/ tests/`). - `NotesMaster` and `NotesMasterPart` bodies are unchanged; only import lines that previously named just `NotesMaster` now also name `HandoutMaster`. - `it_has_no_create_default_classmethod` in `tests/parts/test_slide.py` regression-pins that `HandoutMasterPart` does NOT acquire `create_default` over time. Test counts: 19 new test cases (16 new `it_/but_/and_` functions; the `it_round_trips_show_attrs_via_hf` parametrize fans out to 4 cases). Distribution: 6 in `DescribeHandoutMaster`, 4 in `DescribeSlideMasterAddTextWatermark`, 4 in `DescribeHandoutMasterPart`, 3 in `DescribePresentationPart`, 1 in `DescribePresentation`. Pytest runs at 3651 passed (baseline 3632 + 19). Verification (local, CPython 3.14.4): $ python3 -m pytest tests/ -q | tail -3 ........................................................................ [100%] ................................................... [100%] 3651 passed in 5.28s $ python3 -m ruff check src tests | tail -3 All checks passed! $ python3 -m ruff format --check src tests | tail -3 216 files already formatted $ python3 -m behave features/ --no-color 2>&1 | tail -3 1048 scenarios passed, 0 failed, 0 skipped 3151 steps passed, 0 failed, 0 skipped Took 0min 1.696s $ python3 uat/uat_headers_footers_phase5.py opening /Users/mhoroszowski/Projects/AI/python-pptx/tests/test_files/test.pptx OK — missing handout master raises ValueError: presentation has no handout master; auto-create is deferred because no handout master template ships in this fork yet adding watermark 'DRAFT' to slide_masters[0] watermark shape: left=1828800 top=2743200 width=5486400 height=1371600 saving to /Users/mhoroszowski/Projects/AI/python-pptx/uat/out_headers_footers_phase5.pptx re-opening /Users/mhoroszowski/Projects/AI/python-pptx/uat/out_headers_footers_phase5.pptx round-tripped watermark: text='DRAFT' font='Calibri' size=72.0 transparency=0.7 OK — watermark transparency round-tripped at 0.7 PASS — handout-master absent-path and watermark helper verified Closes #20.
1 parent 87b22c7 commit 6f65019

10 files changed

Lines changed: 290 additions & 10 deletions

File tree

src/pptx/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from pptx.parts.media import MediaPart
1818
from pptx.parts.presentation import PresentationPart
1919
from pptx.parts.slide import (
20+
HandoutMasterPart,
2021
NotesMasterPart,
2122
NotesSlidePart,
2223
SlideLayoutPart,
@@ -46,6 +47,7 @@
4647
# `Plans/customxml-implementation-plan.md` §3.6. The Phase-3
4748
# `CustomXmlParts` collection wraps loaded base `Part` instances
4849
# at enumeration time.
50+
CT.PML_HANDOUT_MASTER: HandoutMasterPart,
4951
CT.PML_NOTES_MASTER: NotesMasterPart,
5052
CT.PML_NOTES_SLIDE: NotesSlidePart,
5153
CT.PML_SLIDE: SlidePart,
@@ -81,6 +83,7 @@
8183
CorePropertiesPart,
8284
CustomPropertiesPart,
8385
CustomXmlPropertiesPart,
86+
HandoutMasterPart,
8487
ImagePart,
8588
MediaPart,
8689
SlidePart,

src/pptx/parts/presentation.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@
77
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
88
from pptx.opc.package import XmlPart
99
from pptx.opc.packuri import PackURI
10-
from pptx.parts.slide import NotesMasterPart, SlidePart
10+
from pptx.parts.slide import HandoutMasterPart, NotesMasterPart, SlidePart
1111
from pptx.presentation import Presentation
1212
from pptx.util import lazyproperty
1313

1414
if TYPE_CHECKING:
1515
from pptx.custom_properties import CustomProperties
1616
from pptx.custom_xml import CustomXmlParts
1717
from pptx.parts.coreprops import CorePropertiesPart
18-
from pptx.slide import NotesMaster, Slide, SlideLayout, SlideMaster
18+
from pptx.slide import HandoutMaster, NotesMaster, Slide, SlideLayout, SlideMaster
1919

2020

2121
class PresentationPart(XmlPart):
@@ -101,6 +101,30 @@ def notes_master_part(self) -> NotesMasterPart:
101101
self.relate_to(notes_master_part, RT.NOTES_MASTER)
102102
return notes_master_part
103103

104+
@lazyproperty
105+
def handout_master(self) -> HandoutMaster:
106+
"""Return the |HandoutMaster| object for this presentation.
107+
108+
Raises |ValueError| when the presentation has no handout master because auto-create is
109+
deliberately deferred until a built-in handout-master template ships in this fork.
110+
"""
111+
return self.handout_master_part.handout_master
112+
113+
@lazyproperty
114+
def handout_master_part(self) -> HandoutMasterPart:
115+
"""Return the |HandoutMasterPart| object for this presentation.
116+
117+
Raises |ValueError| when the presentation has no handout master because auto-create is
118+
deliberately deferred until a built-in handout-master template ships in this fork.
119+
"""
120+
try:
121+
return self.part_related_by(RT.HANDOUT_MASTER)
122+
except KeyError as e:
123+
raise ValueError(
124+
"presentation has no handout master; auto-create is deferred because no "
125+
"handout master template ships in this fork yet"
126+
) from e
127+
104128
@lazyproperty
105129
def presentation(self):
106130
"""

src/pptx/parts/slide.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from pptx.parts.chart import ChartPart
2020
from pptx.parts.embeddedpackage import EmbeddedPackagePart
2121
from pptx.parts.image import Image, ImagePart
22-
from pptx.slide import NotesMaster, NotesSlide, Slide, SlideLayout, SlideMaster
22+
from pptx.slide import HandoutMaster, NotesMaster, NotesSlide, Slide, SlideLayout, SlideMaster
2323
from pptx.util import lazyproperty
2424

2525
if TYPE_CHECKING:
@@ -114,6 +114,20 @@ def _new_theme_part(cls, package):
114114
)
115115

116116

117+
class HandoutMasterPart(BaseSlidePart):
118+
"""Handout master part.
119+
120+
Corresponds to package file `ppt/handoutMasters/handoutMaster1.xml` when present.
121+
Auto-create is deliberately deferred until this fork ships a built-in handout-master
122+
template and theme wiring.
123+
"""
124+
125+
@lazyproperty
126+
def handout_master(self):
127+
"""Return the |HandoutMaster| object that proxies this handout master part."""
128+
return HandoutMaster(self._element, self)
129+
130+
117131
class NotesSlidePart(BaseSlidePart):
118132
"""Notes slide part.
119133

src/pptx/presentation.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pptx.custom_xml import CustomXmlParts
1616
from pptx.oxml.presentation import CT_Presentation, CT_SlideId
1717
from pptx.parts.presentation import PresentationPart
18-
from pptx.slide import NotesMaster, Slide, SlideLayouts
18+
from pptx.slide import HandoutMaster, NotesMaster, Slide, SlideLayouts
1919
from pptx.util import Length
2020

2121

@@ -67,6 +67,15 @@ def notes_master(self) -> NotesMaster:
6767
"""
6868
return self.part.notes_master
6969

70+
@property
71+
def handout_master(self) -> HandoutMaster:
72+
"""Instance of |HandoutMaster| for this presentation.
73+
74+
Raises |ValueError| when the presentation has no handout master because auto-create is
75+
deliberately deferred until a built-in handout-master template ships in this fork.
76+
"""
77+
return self.part.handout_master
78+
7079
def save(self, file: str | os.PathLike[str] | IO[bytes]):
7180
"""Writes this presentation to `file`.
7281

src/pptx/shapes/shapetree.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,13 @@ class MasterShapes(_BaseShapes):
831831
Supports indexed access, len(), and iteration.
832832
"""
833833

834+
def add_textbox(self, left: Length, top: Length, width: Length, height: Length) -> Shape:
835+
"""Return newly added text box shape appended to this master shape tree."""
836+
shape_id = self._next_shape_id
837+
name = "TextBox %d" % (shape_id - 1)
838+
sp = self._spTree.add_textbox(shape_id, name, left, top, width, height)
839+
return cast(Shape, self._shape_factory(sp))
840+
834841
def _shape_factory(self, shape_elm: ShapeElement) -> BaseShape:
835842
"""Return an instance of the appropriate shape proxy class for `shape_elm`."""
836843
return _MasterShapeFactory(shape_elm, self)

src/pptx/slide.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
from typing import TYPE_CHECKING, Iterator, cast
66

7+
from pptx.dml.color import RGBColor
78
from pptx.dml.fill import FillFormat
89
from pptx.enum.shapes import PP_PLACEHOLDER
10+
from pptx.enum.text import PP_ALIGN
911
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
1012
from pptx.shapes.shapetree import (
1113
LayoutPlaceholders,
@@ -18,12 +20,13 @@
1820
SlideShapes,
1921
)
2022
from pptx.shared import ElementProxy, ParentedElementProxy, PartElementProxy
21-
from pptx.util import lazyproperty
23+
from pptx.util import Inches, Length, Pt, lazyproperty
2224

2325
if TYPE_CHECKING:
2426
from pptx.oxml.presentation import CT_SlideIdList, CT_SlideMasterIdList
2527
from pptx.oxml.slide import (
2628
CT_CommonSlideData,
29+
CT_HandoutMaster,
2730
CT_NotesMaster,
2831
CT_NotesSlide,
2932
CT_Slide,
@@ -34,6 +37,7 @@
3437
from pptx.parts.presentation import PresentationPart
3538
from pptx.parts.slide import SlideLayoutPart, SlideMasterPart, SlidePart
3639
from pptx.presentation import Presentation
40+
from pptx.shapes.autoshape import Shape
3741
from pptx.shapes.placeholder import LayoutPlaceholder, MasterPlaceholder, SlidePlaceholder
3842
from pptx.shapes.shapetree import NotesSlidePlaceholder
3943
from pptx.text.text import TextFrame
@@ -97,7 +101,7 @@ def shapes(self):
97101
class _HeaderFooterVisibility:
98102
"""Provides access to header/footer visibility settings on a slide template."""
99103

100-
_element: CT_SlideLayout | CT_SlideMaster | CT_NotesMaster
104+
_element: CT_SlideLayout | CT_SlideMaster | CT_NotesMaster | CT_HandoutMaster
101105

102106
def _get_hf_visibility(self, attr_name: str) -> bool:
103107
"""Return effective `attr_name` value, defaulting to |True| when `<p:hf>` is absent."""
@@ -178,6 +182,15 @@ class NotesMaster(_HeaderFooterVisibility, _BaseMaster):
178182
"""
179183

180184

185+
class HandoutMaster(_HeaderFooterVisibility, _BaseMaster):
186+
"""Proxy for the handout master XML document.
187+
188+
Provides access to shapes and header/footer visibility settings when a deck already
189+
contains a handout master. Auto-create is deliberately deferred until a built-in
190+
`handoutMaster.xml` template ships in this fork.
191+
"""
192+
193+
181194
class NotesSlide(_BaseSlide):
182195
"""Notes slide object.
183196
@@ -735,6 +748,39 @@ class SlideMaster(_HeaderFooterVisibility, _BaseMaster):
735748

736749
_element: CT_SlideMaster # pyright: ignore[reportIncompatibleVariableOverride]
737750

751+
def add_text_watermark(
752+
self,
753+
text: str,
754+
*,
755+
font_size: Length = Pt(72),
756+
transparency: float = 0.7,
757+
font_name: str = "Calibri",
758+
) -> Shape:
759+
"""Add and return a centered watermark textbox on this slide master.
760+
761+
The watermark is a large mid-gray text box sized for the standard 10in x 7.5in
762+
slide canvas. `transparency` is applied to the run's solid font fill.
763+
"""
764+
slide_width = Inches(10)
765+
slide_height = Inches(7.5)
766+
textbox_width = Inches(6)
767+
textbox_height = Inches(1.5)
768+
# ---center a 6in x 1.5in textbox on a standard 10in x 7.5in slide---
769+
left = (slide_width - textbox_width) // 2
770+
top = (slide_height - textbox_height) // 2
771+
772+
shape = self.shapes.add_textbox(left, top, textbox_width, textbox_height)
773+
paragraph = shape.text_frame.paragraphs[0]
774+
paragraph.text = text
775+
paragraph.alignment = PP_ALIGN.CENTER
776+
run = paragraph.runs[0]
777+
run.font.name = font_name
778+
run.font.size = font_size
779+
run.font.fill.solid()
780+
run.font.fill.fore_color.rgb = RGBColor(0x80, 0x80, 0x80)
781+
run.font.fill.transparency = transparency
782+
return shape
783+
738784
@lazyproperty
739785
def slide_layouts(self) -> SlideLayouts:
740786
"""|SlideLayouts| object providing access to this slide-master's layouts."""

tests/parts/test_presentation.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
from pptx.package import Package
1010
from pptx.parts.coreprops import CorePropertiesPart
1111
from pptx.parts.presentation import PresentationPart
12-
from pptx.parts.slide import NotesMasterPart, SlideMasterPart, SlidePart
12+
from pptx.parts.slide import HandoutMasterPart, NotesMasterPart, SlideMasterPart, SlidePart
1313
from pptx.presentation import Presentation
14-
from pptx.slide import NotesMaster, Slide, SlideLayout, SlideMaster
14+
from pptx.slide import HandoutMaster, NotesMaster, Slide, SlideLayout, SlideMaster
1515

1616
from ..unitutil.cxml import element
1717
from ..unitutil.mock import call, class_mock, instance_mock, method_mock, property_mock
@@ -86,6 +86,40 @@ def it_provides_access_to_its_notes_master(self, request, notes_master_part_):
8686

8787
assert prs_part.notes_master is notes_master_
8888

89+
def it_provides_access_to_an_existing_handout_master_part(
90+
self, handout_master_part_, part_related_by_
91+
):
92+
prs_part = PresentationPart(None, None, None, None)
93+
part_related_by_.return_value = handout_master_part_
94+
95+
handout_master_part = prs_part.handout_master_part
96+
97+
prs_part.part_related_by.assert_called_once_with(prs_part, RT.HANDOUT_MASTER)
98+
assert handout_master_part is handout_master_part_
99+
100+
def but_it_raises_ValueError_when_no_handout_master_is_present(self, part_related_by_):
101+
missing_rel = KeyError("rId99")
102+
part_related_by_.side_effect = missing_rel
103+
prs_part = PresentationPart(None, None, None, None)
104+
105+
with pytest.raises(ValueError, match="no handout master") as exc_info:
106+
prs_part.handout_master_part
107+
108+
assert exc_info.value.__cause__ is missing_rel
109+
110+
def it_provides_access_to_its_handout_master(self, request, handout_master_part_):
111+
handout_master_ = instance_mock(request, HandoutMaster)
112+
property_mock(
113+
request,
114+
PresentationPart,
115+
"handout_master_part",
116+
return_value=handout_master_part_,
117+
)
118+
handout_master_part_.handout_master = handout_master_
119+
prs_part = PresentationPart(None, None, None, None)
120+
121+
assert prs_part.handout_master is handout_master_
122+
89123
def it_provides_access_to_a_related_slide(self, request, slide_, related_part_):
90124
slide_part_ = instance_mock(request, SlidePart, slide=slide_)
91125
related_part_.return_value = slide_part_
@@ -192,6 +226,10 @@ def it_knows_the_next_slide_partname_to_help(self):
192226

193227
# fixture components ---------------------------------------------
194228

229+
@pytest.fixture
230+
def handout_master_part_(self, request):
231+
return instance_mock(request, HandoutMasterPart)
232+
195233
@pytest.fixture
196234
def notes_master_part_(self, request):
197235
return instance_mock(request, NotesMasterPart)

tests/parts/test_slide.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,14 @@
2222
from pptx.parts.presentation import PresentationPart
2323
from pptx.parts.slide import (
2424
BaseSlidePart,
25+
HandoutMasterPart,
2526
NotesMasterPart,
2627
NotesSlidePart,
2728
SlideLayoutPart,
2829
SlideMasterPart,
2930
SlidePart,
3031
)
31-
from pptx.slide import NotesMaster, NotesSlide, Slide, SlideLayout, SlideMaster
32+
from pptx.slide import HandoutMaster, NotesMaster, NotesSlide, Slide, SlideLayout, SlideMaster
3233

3334
from ..unitutil.cxml import element
3435
from ..unitutil.file import absjoin, test_file_dir
@@ -179,6 +180,34 @@ def theme_part_(self, request):
179180
return instance_mock(request, Part)
180181

181182

183+
class DescribeHandoutMasterPart(object):
184+
"""Unit-test suite for `pptx.parts.slide.HandoutMasterPart` objects."""
185+
186+
def it_is_a_BaseSlidePart_subclass(self):
187+
assert issubclass(HandoutMasterPart, BaseSlidePart)
188+
189+
def it_provides_access_to_its_handout_master(self, request):
190+
handout_master_ = instance_mock(request, HandoutMaster)
191+
HandoutMaster_ = class_mock(
192+
request, "pptx.parts.slide.HandoutMaster", return_value=handout_master_
193+
)
194+
handoutMaster = element("p:handoutMaster")
195+
handout_master_part = HandoutMasterPart(None, None, None, handoutMaster)
196+
197+
handout_master = handout_master_part.handout_master
198+
199+
HandoutMaster_.assert_called_once_with(handoutMaster, handout_master_part)
200+
assert handout_master is handout_master_
201+
202+
def it_has_no_create_default_classmethod(self):
203+
assert not hasattr(HandoutMasterPart, "create_default")
204+
205+
def it_can_be_constructed_as_a_handout_master_part(self):
206+
handout_master_part = HandoutMasterPart(None, None, None, element("p:handoutMaster"))
207+
208+
assert isinstance(handout_master_part, HandoutMasterPart)
209+
210+
182211
class DescribeNotesSlidePart(object):
183212
"""Unit-test suite for `pptx.parts.slide.NotesSlidePart` objects."""
184213

tests/test_presentation.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pptx.parts.presentation import PresentationPart
99
from pptx.parts.slide import NotesMasterPart
1010
from pptx.presentation import Presentation
11-
from pptx.slide import SlideLayouts, SlideMaster, SlideMasters, Slides
11+
from pptx.slide import HandoutMaster, SlideLayouts, SlideMaster, SlideMasters, Slides
1212

1313
from .unitutil.cxml import element, xml
1414
from .unitutil.mock import class_mock, instance_mock, property_mock
@@ -45,6 +45,10 @@ def it_provides_access_to_its_notes_master(self, notes_master_fixture):
4545
prs, notes_master_ = notes_master_fixture
4646
assert prs.notes_master is notes_master_
4747

48+
def it_provides_access_to_its_handout_master(self, handout_master_fixture):
49+
prs, handout_master_ = handout_master_fixture
50+
assert prs.handout_master is handout_master_
51+
4852
def it_provides_access_to_its_slides(self, slides_fixture):
4953
prs, rename_slide_parts_, rIds = slides_fixture[:3]
5054
Slides_, slides_, expected_xml = slides_fixture[3:]
@@ -90,6 +94,12 @@ def layouts_fixture(self, masters_prop_, slide_layouts_):
9094
masters_prop_.return_value.__getitem__.return_value.slide_layouts = slide_layouts_
9195
return prs, slide_layouts_
9296

97+
@pytest.fixture
98+
def handout_master_fixture(self, prs_part_, handout_master_):
99+
prs = Presentation(None, prs_part_)
100+
prs_part_.handout_master = handout_master_
101+
return prs, handout_master_
102+
93103
@pytest.fixture
94104
def master_fixture(self, masters_prop_, slide_master_):
95105
prs = Presentation(None, None)
@@ -194,6 +204,10 @@ def core_properties_(self, request):
194204
def masters_prop_(self, request):
195205
return property_mock(request, Presentation, "slide_masters")
196206

207+
@pytest.fixture
208+
def handout_master_(self, request):
209+
return instance_mock(request, HandoutMaster)
210+
197211
@pytest.fixture
198212
def notes_master_(self, request):
199213
return instance_mock(request, NotesMasterPart)

0 commit comments

Comments
 (0)