diff --git a/src/pptx/slide.py b/src/pptx/slide.py index a66abf6cc..de6e0755f 100644 --- a/src/pptx/slide.py +++ b/src/pptx/slide.py @@ -24,15 +24,17 @@ from pptx.oxml.presentation import CT_SlideIdList, CT_SlideMasterIdList from pptx.oxml.slide import ( CT_CommonSlideData, + CT_NotesMaster, CT_NotesSlide, CT_Slide, + CT_SlideLayout, CT_SlideLayoutIdList, CT_SlideMaster, ) from pptx.parts.presentation import PresentationPart from pptx.parts.slide import SlideLayoutPart, SlideMasterPart, SlidePart from pptx.presentation import Presentation - from pptx.shapes.placeholder import LayoutPlaceholder, MasterPlaceholder + from pptx.shapes.placeholder import LayoutPlaceholder, MasterPlaceholder, SlidePlaceholder from pptx.shapes.shapetree import NotesSlidePlaceholder from pptx.text.text import TextFrame @@ -92,7 +94,84 @@ def shapes(self): return MasterShapes(self._element.spTree, self) -class NotesMaster(_BaseMaster): +class _HeaderFooterVisibility: + """Provides access to header/footer visibility settings on a slide template.""" + + _element: CT_SlideLayout | CT_SlideMaster | CT_NotesMaster + + def _get_hf_visibility(self, attr_name: str) -> bool: + """Return effective `attr_name` value, defaulting to |True| when `` is absent.""" + hf = self._element.hf + return True if hf is None else getattr(hf, attr_name) + + def _set_hf_visibility(self, attr_name: str, value: bool) -> None: + """Set `attr_name` on ``, creating the element only when needed. + + Assigning |True| when `` is absent is a no-op because the effective default is + already |True|. An existing `` element is retained even when all values become + |True|, avoiding low-value XML churn. + """ + hf = self._element.hf + if hf is None and value: + return + if hf is None: + hf = self._element.get_or_add_hf() + setattr(hf, attr_name, value) + + @property + def show_slide_number(self) -> bool: + """`True` when slide numbers are shown for this template, `False` otherwise. + + Assigning |False| creates a `` element when needed and writes `sldNum="0"`. + Assigning |True| preserves any existing `` element rather than removing it. + """ + return self._get_hf_visibility("sldNum") + + @show_slide_number.setter + def show_slide_number(self, value: bool) -> None: + self._set_hf_visibility("sldNum", value) + + @property + def show_footer(self) -> bool: + """`True` when footer placeholders are shown for this template, `False` otherwise. + + Assigning |False| creates a `` element when needed and writes `ftr="0"`. + Assigning |True| preserves any existing `` element rather than removing it. + """ + return self._get_hf_visibility("ftr") + + @show_footer.setter + def show_footer(self, value: bool) -> None: + self._set_hf_visibility("ftr", value) + + @property + def show_date(self) -> bool: + """`True` when date placeholders are shown for this template, `False` otherwise. + + Assigning |False| creates a `` element when needed and writes `dt="0"`. + Assigning |True| preserves any existing `` element rather than removing it. + """ + return self._get_hf_visibility("dt") + + @show_date.setter + def show_date(self, value: bool) -> None: + self._set_hf_visibility("dt", value) + + @property + def show_header(self) -> bool: + """`True` when header placeholders are shown for this template, `False` otherwise. + + Assigning |False| creates a `` element when needed and writes `hdr="0"`. + Assigning |True| preserves any existing `` element rather than removing it. + """ + return self._get_hf_visibility("hdr") + + @show_header.setter + def show_header(self, value: bool) -> None: + self._set_hf_visibility("hdr", value) + + +class NotesMaster(_HeaderFooterVisibility, _BaseMaster): """Proxy for the notes master XML document. Provides access to shapes, the most commonly used of which are placeholders. @@ -214,6 +293,33 @@ def has_notes_slide(self) -> bool: """ return self.part.has_notes_slide + @property + def has_date(self) -> bool: + """`True` if this slide has a date placeholder, `False` otherwise. + + This property is non-mutating; it reports only whether a DATE placeholder is already + present on the slide. + """ + return self._first_ph_of_type(PP_PLACEHOLDER.DATE) is not None + + @property + def has_footer(self) -> bool: + """`True` if this slide has a footer placeholder, `False` otherwise. + + This property is non-mutating; it reports only whether a FOOTER placeholder is already + present on the slide. + """ + return self._first_ph_of_type(PP_PLACEHOLDER.FOOTER) is not None + + @property + def has_slide_number(self) -> bool: + """`True` if this slide has a slide-number placeholder, `False` otherwise. + + This property is non-mutating; it reports only whether a SLIDE_NUMBER placeholder is + already present on the slide. + """ + return self._first_ph_of_type(PP_PLACEHOLDER.SLIDE_NUMBER) is not None + @property def notes_slide(self) -> NotesSlide: """The |NotesSlide| instance for this slide. @@ -223,6 +329,60 @@ def notes_slide(self) -> NotesSlide: """ return self.part.notes_slide + @property + def date_text(self) -> str | None: + """Text of this slide's date placeholder, or |None| when no date placeholder is present. + + Reading this property does not create a placeholder. An existing empty DATE placeholder + returns an empty string. + """ + placeholder = self._first_ph_of_type(PP_PLACEHOLDER.DATE) + return None if placeholder is None else placeholder.text_frame.text + + @date_text.setter + def date_text(self, value: str | None) -> None: + placeholder = self._first_ph_of_type(PP_PLACEHOLDER.DATE) + if value in (None, ""): + if placeholder is not None: + placeholder.text_frame.text = "" + return + + if placeholder is None: + layout_ph = self._layout_ph_of_type(PP_PLACEHOLDER.DATE) + if layout_ph is None: + raise ValueError("slide layout has no DATE placeholder to clone from") + self.shapes.clone_placeholder(layout_ph) + placeholder = cast("SlidePlaceholder", self._first_ph_of_type(PP_PLACEHOLDER.DATE)) + + placeholder.text_frame.text = value + + @property + def footer(self) -> str | None: + """Text of this slide's footer placeholder, or |None| when no footer placeholder is present. + + Reading this property does not create a placeholder. An existing empty FOOTER placeholder + returns an empty string. + """ + placeholder = self._first_ph_of_type(PP_PLACEHOLDER.FOOTER) + return None if placeholder is None else placeholder.text_frame.text + + @footer.setter + def footer(self, value: str | None) -> None: + placeholder = self._first_ph_of_type(PP_PLACEHOLDER.FOOTER) + if value in (None, ""): + if placeholder is not None: + placeholder.text_frame.text = "" + return + + if placeholder is None: + layout_ph = self._layout_ph_of_type(PP_PLACEHOLDER.FOOTER) + if layout_ph is None: + raise ValueError("slide layout has no FOOTER placeholder to clone from") + self.shapes.clone_placeholder(layout_ph) + placeholder = cast("SlidePlaceholder", self._first_ph_of_type(PP_PLACEHOLDER.FOOTER)) + + placeholder.text_frame.text = value + @lazyproperty def placeholders(self) -> SlidePlaceholders: """Sequence of placeholder shapes in this slide.""" @@ -247,6 +407,28 @@ def slide_layout(self) -> SlideLayout: """|SlideLayout| object this slide inherits appearance from.""" return self.part.slide_layout + def _first_ph_of_type(self, ph_type: PP_PLACEHOLDER) -> SlidePlaceholder | None: + """Return the first SlidePlaceholder of `ph_type` in document order, or |None|. + + This helper is non-mutating and returns the first matching slide placeholder when multiple + placeholders of the same type are present. + """ + for placeholder in self.placeholders: + if placeholder.placeholder_format.type == ph_type: + return placeholder + return None + + def _layout_ph_of_type(self, ph_type: PP_PLACEHOLDER) -> LayoutPlaceholder | None: + """Return the first LayoutPlaceholder of `ph_type` on this slide's layout, or |None|. + + The layout placeholder is used as the source when promoting a latent placeholder to a + slide-level placeholder on first write. + """ + for placeholder in self.slide_layout.placeholders: + if placeholder.placeholder_format.type == ph_type: + return placeholder + return None + def delete(self) -> None: """Remove this slide from its presentation. @@ -426,7 +608,7 @@ def duplicate(self, slide: Slide, index: int | None = None) -> Slide: return new_slide_part.slide -class SlideLayout(_BaseSlide): +class SlideLayout(_HeaderFooterVisibility, _BaseSlide): """Slide layout object. Provides access to placeholders, regular shapes, and slide layout-level properties. @@ -544,7 +726,7 @@ def remove(self, slide_layout: SlideLayout) -> None: slide_layout.slide_master.part.drop_rel(target_sldLayoutId.rId) -class SlideMaster(_BaseMaster): +class SlideMaster(_HeaderFooterVisibility, _BaseMaster): """Slide master object. Provides access to slide layouts. Access to placeholders, regular shapes, and slide master-level diff --git a/tests/test_slide.py b/tests/test_slide.py index 7a94b5ec2..7a8676b91 100644 --- a/tests/test_slide.py +++ b/tests/test_slide.py @@ -4,8 +4,11 @@ from __future__ import annotations +from io import BytesIO + import pytest +from pptx import Presentation as PresentationFactory from pptx.dml.fill import FillFormat from pptx.enum.shapes import PP_PLACEHOLDER from pptx.package import Package @@ -13,7 +16,7 @@ from pptx.parts.slide import SlideLayoutPart, SlideMasterPart, SlidePart from pptx.presentation import Presentation from pptx.shapes.base import BaseShape -from pptx.shapes.placeholder import LayoutPlaceholder, NotesSlidePlaceholder +from pptx.shapes.placeholder import LayoutPlaceholder, NotesSlidePlaceholder, SlidePlaceholder from pptx.shapes.shapetree import ( LayoutPlaceholders, LayoutShapes, @@ -43,6 +46,20 @@ from .unitutil.mock import call, class_mock, instance_mock, method_mock, property_mock +def _placeholder_with_type( + request, + placeholder_cls: type[LayoutPlaceholder | NotesSlidePlaceholder | SlidePlaceholder], + ph_type: PP_PLACEHOLDER, + text: str = "", +): + """Return placeholder mock configured with `ph_type` and `text`.""" + placeholder_ = instance_mock(request, placeholder_cls) + placeholder_.placeholder_format.type = ph_type + placeholder_.text_frame.text = text + placeholder_.element.ph_type = ph_type + return placeholder_ + + class Describe_BaseSlide(object): """Unit-test suite for `pptx.slide._BaseSlide` objects.""" @@ -372,6 +389,217 @@ def it_provides_access_to_its_notes_slide(self, notes_slide_fixture): slide, notes_slide_ = notes_slide_fixture assert slide.notes_slide is notes_slide_ + def it_knows_when_it_has_no_footer(self, request): + slide = Slide(None, None) + property_mock(request, Slide, "placeholders", return_value=()) + + assert slide.has_footer is False + assert slide.footer is None + + def it_knows_when_it_has_a_footer_with_empty_text(self, request): + footer_placeholder = _placeholder_with_type( + request, SlidePlaceholder, PP_PLACEHOLDER.FOOTER + ) + slide = Slide(None, None) + property_mock(request, Slide, "placeholders", return_value=(footer_placeholder,)) + + assert slide.has_footer is True + assert slide.footer == "" + + def it_knows_its_footer_text(self, request): + footer_placeholder = _placeholder_with_type( + request, SlidePlaceholder, PP_PLACEHOLDER.FOOTER, "Quarter Close" + ) + slide = Slide(None, None) + property_mock(request, Slide, "placeholders", return_value=(footer_placeholder,)) + + assert slide.footer == "Quarter Close" + + def it_returns_the_first_footer_in_document_order(self, request): + first_footer = _placeholder_with_type( + request, SlidePlaceholder, PP_PLACEHOLDER.FOOTER, "First footer" + ) + date_placeholder = _placeholder_with_type( + request, SlidePlaceholder, PP_PLACEHOLDER.DATE, "2026-05-13" + ) + second_footer = _placeholder_with_type( + request, SlidePlaceholder, PP_PLACEHOLDER.FOOTER, "Second footer" + ) + slide = Slide(None, None) + property_mock( + request, + Slide, + "placeholders", + return_value=(first_footer, date_placeholder, second_footer), + ) + + assert slide.footer == "First footer" + + def it_can_set_footer_text_when_already_present(self, request): + footer_placeholder = _placeholder_with_type( + request, SlidePlaceholder, PP_PLACEHOLDER.FOOTER + ) + slide = Slide(None, None) + property_mock(request, Slide, "placeholders", return_value=(footer_placeholder,)) + + slide.footer = "Phase 2 footer" + + assert slide.has_footer is True + assert footer_placeholder.text_frame.text == "Phase 2 footer" + + def it_can_set_footer_text_when_absent_by_cloning_from_layout(self): + prs = PresentationFactory() + slide = prs.slides.add_slide(prs.slide_layouts[0]) + + assert slide.has_footer is False + assert slide.footer is None + + slide.footer = "Phase 2 footer" + + assert slide.has_footer is True + assert slide.footer == "Phase 2 footer" + + out = BytesIO() + prs.save(out) + out.seek(0) + reopened = PresentationFactory(out) + assert reopened.slides[0].footer == "Phase 2 footer" + + def it_can_clear_footer_text_by_setting_None(self): + prs = PresentationFactory() + slide = prs.slides.add_slide(prs.slide_layouts[0]) + slide.footer = "Phase 2 footer" + + slide.footer = None + + assert slide.has_footer is True + assert slide.footer == "" + + def it_can_clear_footer_text_by_setting_empty_string(self): + prs = PresentationFactory() + slide = prs.slides.add_slide(prs.slide_layouts[0]) + slide.footer = "Phase 2 footer" + + slide.footer = "" + + assert slide.has_footer is True + assert slide.footer == "" + + def it_leaves_footer_absent_when_clearing_nonexistent_footer(self): + prs = PresentationFactory() + slide = prs.slides.add_slide(prs.slide_layouts[0]) + + slide.footer = None + + assert slide.has_footer is False + assert slide.footer is None + + def it_raises_when_setting_footer_with_no_layout_FOOTER_placeholder(self, request): + object_placeholder = _placeholder_with_type( + request, LayoutPlaceholder, PP_PLACEHOLDER.OBJECT + ) + slide_layout_ = instance_mock(request, SlideLayout) + slide_layout_.placeholders = (object_placeholder,) + property_mock(request, Slide, "slide_layout", return_value=slide_layout_) + property_mock(request, Slide, "placeholders", return_value=()) + slide = Slide(None, None) + + with pytest.raises(ValueError) as excinfo: + slide.footer = "Phase 2 footer" + + assert str(excinfo.value) == "slide layout has no FOOTER placeholder to clone from" + + def it_knows_when_it_has_a_slide_number_placeholder(self, request): + slide_number_placeholder = _placeholder_with_type( + request, SlidePlaceholder, PP_PLACEHOLDER.SLIDE_NUMBER + ) + slide = Slide(None, None) + property_mock(request, Slide, "placeholders", return_value=(slide_number_placeholder,)) + + assert slide.has_slide_number is True + + def it_knows_when_it_has_no_slide_number_placeholder(self, request): + footer_placeholder = _placeholder_with_type( + request, SlidePlaceholder, PP_PLACEHOLDER.FOOTER + ) + slide = Slide(None, None) + property_mock(request, Slide, "placeholders", return_value=(footer_placeholder,)) + + assert slide.has_slide_number is False + + def it_knows_when_it_has_no_date(self, request): + slide = Slide(None, None) + property_mock(request, Slide, "placeholders", return_value=()) + + assert slide.has_date is False + assert slide.date_text is None + + def it_knows_when_it_has_a_date_placeholder_with_text(self, request): + date_placeholder = _placeholder_with_type( + request, SlidePlaceholder, PP_PLACEHOLDER.DATE, "2026-05-13" + ) + slide = Slide(None, None) + property_mock(request, Slide, "placeholders", return_value=(date_placeholder,)) + + assert slide.has_date is True + assert slide.date_text == "2026-05-13" + + def it_can_set_date_text_when_present(self, request): + date_placeholder = _placeholder_with_type(request, SlidePlaceholder, PP_PLACEHOLDER.DATE) + slide = Slide(None, None) + property_mock(request, Slide, "placeholders", return_value=(date_placeholder,)) + + slide.date_text = "2026-05-13" + + assert slide.has_date is True + assert date_placeholder.text_frame.text == "2026-05-13" + + def it_can_clear_date_text_by_setting_None(self): + prs = PresentationFactory() + slide = prs.slides.add_slide(prs.slide_layouts[0]) + slide.date_text = "2026-05-13" + + slide.date_text = None + + assert slide.has_date is True + assert slide.date_text == "" + + def it_can_clone_date_from_layout_on_set(self): + prs = PresentationFactory() + slide = prs.slides.add_slide(prs.slide_layouts[0]) + + assert slide.has_date is False + assert slide.date_text is None + + slide.date_text = "2026-05-13" + + assert slide.has_date is True + assert slide.date_text == "2026-05-13" + + def it_leaves_date_absent_when_clearing_nonexistent_date(self): + prs = PresentationFactory() + slide = prs.slides.add_slide(prs.slide_layouts[0]) + + slide.date_text = "" + + assert slide.has_date is False + assert slide.date_text is None + + def it_raises_when_setting_date_with_no_layout_DATE_placeholder(self, request): + object_placeholder = _placeholder_with_type( + request, LayoutPlaceholder, PP_PLACEHOLDER.OBJECT + ) + slide_layout_ = instance_mock(request, SlideLayout) + slide_layout_.placeholders = (object_placeholder,) + property_mock(request, Slide, "slide_layout", return_value=slide_layout_) + property_mock(request, Slide, "placeholders", return_value=()) + slide = Slide(None, None) + + with pytest.raises(ValueError) as excinfo: + slide.date_text = "2026-05-13" + + assert str(excinfo.value) == "slide layout has no DATE placeholder to clone from" + # fixtures ------------------------------------------------------- @pytest.fixture @@ -679,6 +907,113 @@ def it_knows_which_slides_are_based_on_it( assert used_by_slides == expected_value + def it_knows_show_slide_number_defaults_True_when_hf_absent(self): + slide_layout = SlideLayout(element("p:sldLayout/p:cSld/p:spTree"), None) + assert slide_layout.show_slide_number is True + + def it_knows_show_footer_defaults_True_when_hf_absent(self): + slide_layout = SlideLayout(element("p:sldLayout/p:cSld/p:spTree"), None) + assert slide_layout.show_footer is True + + def it_knows_show_date_defaults_True_when_hf_absent(self): + slide_layout = SlideLayout(element("p:sldLayout/p:cSld/p:spTree"), None) + assert slide_layout.show_date is True + + def it_knows_show_header_defaults_True_when_hf_absent(self): + slide_layout = SlideLayout(element("p:sldLayout/p:cSld/p:spTree"), None) + assert slide_layout.show_header is True + + @pytest.mark.parametrize( + ("prop_name", "sldLayout_cxml", "expected_value"), + [ + ("show_slide_number", "p:sldLayout/(p:cSld/p:spTree,p:hf{sldNum=0})", False), + ("show_footer", "p:sldLayout/(p:cSld/p:spTree,p:hf{ftr=0})", False), + ("show_date", "p:sldLayout/(p:cSld/p:spTree,p:hf{dt=0})", False), + ("show_header", "p:sldLayout/(p:cSld/p:spTree,p:hf{hdr=0})", False), + ("show_slide_number", "p:sldLayout/(p:cSld/p:spTree,p:hf{sldNum=1})", True), + ("show_footer", "p:sldLayout/(p:cSld/p:spTree,p:hf{ftr=1})", True), + ("show_date", "p:sldLayout/(p:cSld/p:spTree,p:hf{dt=1})", True), + ("show_header", "p:sldLayout/(p:cSld/p:spTree,p:hf{hdr=1})", True), + ], + ) + def it_knows_show_attrs_when_hf_present( + self, prop_name: str, sldLayout_cxml: str, expected_value: bool + ): + slide_layout = SlideLayout(element(sldLayout_cxml), None) + assert getattr(slide_layout, prop_name) is expected_value + + def it_can_set_show_slide_number_False_creates_hf(self): + slide_layout = SlideLayout(element("p:sldLayout/p:cSld/p:spTree"), None) + + slide_layout.show_slide_number = False + + assert slide_layout._element.xml == xml("p:sldLayout/(p:cSld/p:spTree,p:hf{sldNum=0})") + + def it_can_set_show_footer_False_creates_hf(self): + slide_layout = SlideLayout(element("p:sldLayout/p:cSld/p:spTree"), None) + + slide_layout.show_footer = False + + assert slide_layout._element.xml == xml("p:sldLayout/(p:cSld/p:spTree,p:hf{ftr=0})") + + def it_can_set_show_date_False_creates_hf(self): + slide_layout = SlideLayout(element("p:sldLayout/p:cSld/p:spTree"), None) + + slide_layout.show_date = False + + assert slide_layout._element.xml == xml("p:sldLayout/(p:cSld/p:spTree,p:hf{dt=0})") + + def it_can_set_show_header_False_creates_hf(self): + slide_layout = SlideLayout(element("p:sldLayout/p:cSld/p:spTree"), None) + + slide_layout.show_header = False + + assert slide_layout._element.xml == xml("p:sldLayout/(p:cSld/p:spTree,p:hf{hdr=0})") + + @pytest.mark.parametrize( + "prop_name", ["show_slide_number", "show_footer", "show_date", "show_header"] + ) + def it_setting_show_attr_True_when_hf_absent_is_no_op(self, prop_name: str): + slide_layout = SlideLayout(element("p:sldLayout/p:cSld/p:spTree"), None) + + setattr(slide_layout, prop_name, True) + + assert slide_layout._element.xml == xml("p:sldLayout/p:cSld/p:spTree") + + @pytest.mark.parametrize( + ("prop_name", "sldLayout_cxml", "expected_cxml"), + [ + ( + "show_slide_number", + "p:sldLayout/(p:cSld/p:spTree,p:hf{sldNum=0})", + "p:sldLayout/(p:cSld/p:spTree,p:hf)", + ), + ( + "show_footer", + "p:sldLayout/(p:cSld/p:spTree,p:hf{ftr=0})", + "p:sldLayout/(p:cSld/p:spTree,p:hf)", + ), + ( + "show_date", + "p:sldLayout/(p:cSld/p:spTree,p:hf{dt=0})", + "p:sldLayout/(p:cSld/p:spTree,p:hf)", + ), + ( + "show_header", + "p:sldLayout/(p:cSld/p:spTree,p:hf{hdr=0})", + "p:sldLayout/(p:cSld/p:spTree,p:hf)", + ), + ], + ) + def it_setting_show_attr_True_when_hf_present_keeps_hf( + self, prop_name: str, sldLayout_cxml: str, expected_cxml: str + ): + slide_layout = SlideLayout(element(sldLayout_cxml), None) + + setattr(slide_layout, prop_name, True) + + assert slide_layout._element.xml == xml(expected_cxml) + # fixtures ------------------------------------------------------- @pytest.fixture( @@ -941,6 +1276,72 @@ def it_provides_access_to_its_slide_layouts(self, layouts_fixture): SlideLayouts_.assert_called_once_with(sldLayoutIdLst, slide_master) assert slide_layouts is slide_layouts_ + @pytest.mark.parametrize( + "prop_name", ["show_slide_number", "show_footer", "show_date", "show_header"] + ) + def it_knows_show_attrs_default_True_when_hf_absent(self, prop_name: str): + slide_master = SlideMaster(element("p:sldMaster/p:cSld/p:spTree"), None) + assert getattr(slide_master, prop_name) is True + + @pytest.mark.parametrize( + ("prop_name", "sldMaster_cxml", "expected_value"), + [ + ("show_slide_number", "p:sldMaster/(p:cSld/p:spTree,p:hf{sldNum=0})", False), + ("show_footer", "p:sldMaster/(p:cSld/p:spTree,p:hf{ftr=0})", False), + ("show_date", "p:sldMaster/(p:cSld/p:spTree,p:hf{dt=0})", False), + ("show_header", "p:sldMaster/(p:cSld/p:spTree,p:hf{hdr=0})", False), + ], + ) + def it_knows_show_attrs_when_hf_present( + self, prop_name: str, sldMaster_cxml: str, expected_value: bool + ): + slide_master = SlideMaster(element(sldMaster_cxml), None) + assert getattr(slide_master, prop_name) is expected_value + + @pytest.mark.parametrize( + ("prop_name", "expected_cxml"), + [ + ("show_slide_number", "p:sldMaster/(p:cSld/p:spTree,p:hf{sldNum=0})"), + ("show_footer", "p:sldMaster/(p:cSld/p:spTree,p:hf{ftr=0})"), + ("show_date", "p:sldMaster/(p:cSld/p:spTree,p:hf{dt=0})"), + ("show_header", "p:sldMaster/(p:cSld/p:spTree,p:hf{hdr=0})"), + ], + ) + def it_can_set_show_attr_False_creates_hf(self, prop_name: str, expected_cxml: str): + slide_master = SlideMaster(element("p:sldMaster/p:cSld/p:spTree"), None) + + setattr(slide_master, prop_name, False) + + assert slide_master._element.xml == xml(expected_cxml) + + @pytest.mark.parametrize( + "prop_name", ["show_slide_number", "show_footer", "show_date", "show_header"] + ) + def it_setting_show_attr_True_when_hf_absent_is_no_op(self, prop_name: str): + slide_master = SlideMaster(element("p:sldMaster/p:cSld/p:spTree"), None) + + setattr(slide_master, prop_name, True) + + assert slide_master._element.xml == xml("p:sldMaster/p:cSld/p:spTree") + + @pytest.mark.parametrize( + ("prop_name", "sldMaster_cxml"), + [ + ("show_slide_number", "p:sldMaster/(p:cSld/p:spTree,p:hf{sldNum=0})"), + ("show_footer", "p:sldMaster/(p:cSld/p:spTree,p:hf{ftr=0})"), + ("show_date", "p:sldMaster/(p:cSld/p:spTree,p:hf{dt=0})"), + ("show_header", "p:sldMaster/(p:cSld/p:spTree,p:hf{hdr=0})"), + ], + ) + def it_setting_show_attr_True_when_hf_present_keeps_hf( + self, prop_name: str, sldMaster_cxml: str + ): + slide_master = SlideMaster(element(sldMaster_cxml), None) + + setattr(slide_master, prop_name, True) + + assert slide_master._element.xml == xml("p:sldMaster/(p:cSld/p:spTree,p:hf)") + # fixtures ------------------------------------------------------- @pytest.fixture @@ -965,6 +1366,80 @@ def slide_layouts_(self, request): return instance_mock(request, SlideLayouts) +class DescribeNotesMaster(object): + """Unit-test suite for `pptx.slide.NotesMaster` objects.""" + + def it_is_a_BaseMaster_subclass(self): + notes_master = NotesMaster(None, None) + assert isinstance(notes_master, _BaseMaster) + + @pytest.mark.parametrize( + "prop_name", ["show_slide_number", "show_footer", "show_date", "show_header"] + ) + def it_knows_show_attrs_default_True_when_hf_absent(self, prop_name: str): + notes_master = NotesMaster(element("p:notesMaster/p:cSld/p:spTree"), None) + assert getattr(notes_master, prop_name) is True + + @pytest.mark.parametrize( + ("prop_name", "notesMaster_cxml", "expected_value"), + [ + ("show_slide_number", "p:notesMaster/(p:cSld/p:spTree,p:hf{sldNum=0})", False), + ("show_footer", "p:notesMaster/(p:cSld/p:spTree,p:hf{ftr=0})", False), + ("show_date", "p:notesMaster/(p:cSld/p:spTree,p:hf{dt=0})", False), + ("show_header", "p:notesMaster/(p:cSld/p:spTree,p:hf{hdr=0})", False), + ], + ) + def it_knows_show_attrs_when_hf_present( + self, prop_name: str, notesMaster_cxml: str, expected_value: bool + ): + notes_master = NotesMaster(element(notesMaster_cxml), None) + assert getattr(notes_master, prop_name) is expected_value + + @pytest.mark.parametrize( + ("prop_name", "expected_cxml"), + [ + ("show_slide_number", "p:notesMaster/(p:cSld/p:spTree,p:hf{sldNum=0})"), + ("show_footer", "p:notesMaster/(p:cSld/p:spTree,p:hf{ftr=0})"), + ("show_date", "p:notesMaster/(p:cSld/p:spTree,p:hf{dt=0})"), + ("show_header", "p:notesMaster/(p:cSld/p:spTree,p:hf{hdr=0})"), + ], + ) + def it_can_set_show_attr_False_creates_hf(self, prop_name: str, expected_cxml: str): + notes_master = NotesMaster(element("p:notesMaster/p:cSld/p:spTree"), None) + + setattr(notes_master, prop_name, False) + + assert notes_master._element.xml == xml(expected_cxml) + + @pytest.mark.parametrize( + "prop_name", ["show_slide_number", "show_footer", "show_date", "show_header"] + ) + def it_setting_show_attr_True_when_hf_absent_is_no_op(self, prop_name: str): + notes_master = NotesMaster(element("p:notesMaster/p:cSld/p:spTree"), None) + + setattr(notes_master, prop_name, True) + + assert notes_master._element.xml == xml("p:notesMaster/p:cSld/p:spTree") + + @pytest.mark.parametrize( + ("prop_name", "notesMaster_cxml"), + [ + ("show_slide_number", "p:notesMaster/(p:cSld/p:spTree,p:hf{sldNum=0})"), + ("show_footer", "p:notesMaster/(p:cSld/p:spTree,p:hf{ftr=0})"), + ("show_date", "p:notesMaster/(p:cSld/p:spTree,p:hf{dt=0})"), + ("show_header", "p:notesMaster/(p:cSld/p:spTree,p:hf{hdr=0})"), + ], + ) + def it_setting_show_attr_True_when_hf_present_keeps_hf( + self, prop_name: str, notesMaster_cxml: str + ): + notes_master = NotesMaster(element(notesMaster_cxml), None) + + setattr(notes_master, prop_name, True) + + assert notes_master._element.xml == xml("p:notesMaster/(p:cSld/p:spTree,p:hf)") + + class DescribeSlideMasters(object): """Unit-test suite for `pptx.slide.SlideMasters` objects."""