Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/pptx/oxml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,8 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
CT_Background,
CT_BackgroundProperties,
CT_CommonSlideData,
CT_HandoutMaster,
CT_HeaderFooter,
CT_NotesMaster,
CT_NotesSlide,
CT_Slide,
Expand All @@ -479,6 +481,8 @@ def register_element_cls(nsptagname: str, cls: Type[BaseOxmlElement]):
register_element_cls("p:bgPr", CT_BackgroundProperties)
register_element_cls("p:childTnLst", CT_TimeNodeList)
register_element_cls("p:cSld", CT_CommonSlideData)
register_element_cls("p:handoutMaster", CT_HandoutMaster)
register_element_cls("p:hf", CT_HeaderFooter)
register_element_cls("p:notes", CT_NotesSlide)
register_element_cls("p:notesMaster", CT_NotesMaster)
register_element_cls("p:sld", CT_Slide)
Expand Down
46 changes: 46 additions & 0 deletions src/pptx/oxml/slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,51 @@ def _change_to_noFill_bg(self) -> CT_Background:
return bg


class CT_HandoutMaster(_BaseSlideElement):
"""`p:handoutMaster` element, root of a handout master part.

Content model per ECMA-376 §19.3.1.24: `(cSld, clrMap, hf?, extLst?)`.
"""

_tag_seq = ("p:cSld", "p:clrMap", "p:hf", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
hf: CT_HeaderFooter | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:hf", successors=_tag_seq[3:]
)
del _tag_seq


class CT_HeaderFooter(BaseOxmlElement):
"""`p:hf` element, configuring per-template visibility of slide-number, header, footer,
and date placeholders.

Appears as a child of `p:sldMaster`, `p:sldLayout`, `p:notesMaster`, and
`p:handoutMaster`. Each of the four boolean attributes defaults to True
when omitted (ECMA-376 §19.3.1.18).
"""

sldNum: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"sldNum", XsdBoolean, default=True
)
hdr: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"hdr", XsdBoolean, default=True
)
ftr: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"ftr", XsdBoolean, default=True
)
dt: bool = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"dt", XsdBoolean, default=True
)


class CT_NotesMaster(_BaseSlideElement):
"""`p:notesMaster` element, root of a notes master part."""

_tag_seq = ("p:cSld", "p:clrMap", "p:hf", "p:notesStyle", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
hf: CT_HeaderFooter | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:hf", successors=_tag_seq[3:]
)
del _tag_seq

@classmethod
Expand Down Expand Up @@ -259,6 +299,9 @@ class CT_SlideLayout(_BaseSlideElement):

_tag_seq = ("p:cSld", "p:clrMapOvr", "p:transition", "p:timing", "p:hf", "p:extLst")
cSld: CT_CommonSlideData = OneAndOnlyOne("p:cSld") # pyright: ignore[reportAssignmentType]
hf: CT_HeaderFooter | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:hf", successors=_tag_seq[5:]
)
del _tag_seq


Expand Down Expand Up @@ -301,6 +344,9 @@ class CT_SlideMaster(_BaseSlideElement):
sldLayoutIdLst: CT_SlideLayoutIdList = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:sldLayoutIdLst", successors=_tag_seq[3:]
)
hf: CT_HeaderFooter | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"p:hf", successors=_tag_seq[6:]
)
del _tag_seq


Expand Down
13 changes: 12 additions & 1 deletion src/pptx/oxml/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
ST_TextTypeface,
ST_TextWrappingType,
XsdBoolean,
XsdString,
)
from pptx.oxml.xmlchemy import (
BaseOxmlElement,
Expand Down Expand Up @@ -329,7 +330,13 @@ def add_hlinkClick(self, rId: str) -> CT_Hyperlink:


class CT_TextField(BaseOxmlElement):
"""`a:fld` field element, for either a slide number or date field."""
"""`a:fld` field element, for either a slide number or date field.

`id` is a required GUID identifier per ECMA-376 §A.4.1. `type` names the
field semantics — `slidenum`, `datetime1`..`datetime13`, `title`, etc. —
and is optional in the OOXML schema, though PowerPoint emits it on
every field it authors.
"""

get_or_add_rPr: Callable[[], CT_TextCharacterProperties]

Expand All @@ -339,6 +346,10 @@ class CT_TextField(BaseOxmlElement):
t: BaseOxmlElement | None = ZeroOrOne( # pyright: ignore[reportAssignmentType]
"a:t", successors=()
)
id: str = RequiredAttribute("id", XsdString) # pyright: ignore[reportAssignmentType]
type: str | None = OptionalAttribute( # pyright: ignore[reportAssignmentType]
"type", XsdString
)

@property
def text(self) -> str: # pyright: ignore[reportIncompatibleMethodOverride]
Expand Down
115 changes: 114 additions & 1 deletion tests/oxml/test_slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@

from __future__ import annotations

from pptx.oxml.slide import CT_NotesMaster, CT_NotesSlide
from typing import cast

import pytest

from pptx.oxml.slide import (
CT_HandoutMaster,
CT_HeaderFooter,
CT_NotesMaster,
CT_NotesSlide,
CT_SlideLayout,
CT_SlideMaster,
)

from ..unitutil.cxml import element
from ..unitutil.file import snippet_text


Expand All @@ -21,3 +33,104 @@ class DescribeCT_NotesSlide(object):
def it_can_create_a_new_notes_element(self):
notes = CT_NotesSlide.new()
assert notes.xml == snippet_text("default-notes")


class DescribeCT_HeaderFooter(object):
"""Unit-test suite for `pptx.oxml.slide.CT_HeaderFooter` objects."""

@pytest.mark.parametrize(
("hf_cxml", "expected"),
[
("p:hf", (True, True, True, True)),
("p:hf{sldNum=0,hdr=0,ftr=0,dt=0}", (False, False, False, False)),
("p:hf{sldNum=1,hdr=1,ftr=1,dt=1}", (True, True, True, True)),
("p:hf{sldNum=0}", (False, True, True, True)),
("p:hf{hdr=0}", (True, False, True, True)),
("p:hf{ftr=0}", (True, True, False, True)),
("p:hf{dt=0}", (True, True, True, False)),
],
)
def it_provides_boolean_access_to_its_four_visibility_attrs(
self, hf_cxml: str, expected: tuple[bool, bool, bool, bool]
):
hf = cast(CT_HeaderFooter, element(hf_cxml))
assert (hf.sldNum, hf.hdr, hf.ftr, hf.dt) == expected

@pytest.mark.parametrize("attr_name", ["sldNum", "hdr", "ftr", "dt"])
def it_can_toggle_each_attribute_via_setter(self, attr_name: str):
hf = cast(CT_HeaderFooter, element("p:hf"))
# ---default value is True when attr is absent---
assert getattr(hf, attr_name) is True
# ---set False, read back False---
setattr(hf, attr_name, False)
assert getattr(hf, attr_name) is False
# ---set back to True---
setattr(hf, attr_name, True)
assert getattr(hf, attr_name) is True


class DescribeCT_HandoutMaster(object):
"""Unit-test suite for `pptx.oxml.slide.CT_HandoutMaster` objects."""

def it_provides_access_to_its_hf_child(self):
handoutMaster = cast(
CT_HandoutMaster,
element("p:handoutMaster/(p:cSld/p:spTree,p:clrMap,p:hf{ftr=0})"),
)
assert handoutMaster.hf is not None
assert handoutMaster.hf.ftr is False

def it_returns_None_for_hf_when_absent(self):
handoutMaster = cast(
CT_HandoutMaster, element("p:handoutMaster/(p:cSld/p:spTree,p:clrMap)")
)
assert handoutMaster.hf is None

def it_can_add_an_hf_child_via_get_or_add(self):
handoutMaster = cast(
CT_HandoutMaster, element("p:handoutMaster/(p:cSld/p:spTree,p:clrMap)")
)
hf = handoutMaster.get_or_add_hf()
assert hf is handoutMaster.hf
# ---defaults all True on a freshly-added <p:hf/>---
assert (hf.sldNum, hf.hdr, hf.ftr, hf.dt) == (True, True, True, True)


class DescribeHFAccessOnTemplates(object):
"""`hf` ZeroOrOne accessor on SlideMaster / SlideLayout / NotesMaster / HandoutMaster."""

def it_reads_hf_on_a_sldMaster(self):
sldMaster = cast(
CT_SlideMaster,
element("p:sldMaster/(p:cSld/p:spTree,p:hf{sldNum=0,ftr=0})"),
)
assert sldMaster.hf is not None
assert sldMaster.hf.sldNum is False
assert sldMaster.hf.ftr is False
assert sldMaster.hf.dt is True

def it_returns_None_for_hf_on_a_sldMaster_when_absent(self):
sldMaster = cast(CT_SlideMaster, element("p:sldMaster/p:cSld/p:spTree"))
assert sldMaster.hf is None

def it_reads_hf_on_a_sldLayout(self):
sldLayout = cast(CT_SlideLayout, element("p:sldLayout/(p:cSld/p:spTree,p:hf{dt=0})"))
assert sldLayout.hf is not None
assert sldLayout.hf.dt is False

def it_returns_None_for_hf_on_a_sldLayout_when_absent(self):
sldLayout = cast(CT_SlideLayout, element("p:sldLayout/p:cSld/p:spTree"))
assert sldLayout.hf is None

def it_reads_hf_on_a_notesMaster(self):
notesMaster = cast(
CT_NotesMaster,
element("p:notesMaster/(p:cSld/p:spTree,p:hf{hdr=0,ftr=0})"),
)
assert notesMaster.hf is not None
assert notesMaster.hf.hdr is False
assert notesMaster.hf.ftr is False

def it_returns_None_for_hf_on_a_notesMaster_when_absent(self):
notesMaster = cast(CT_NotesMaster, element("p:notesMaster/p:cSld/p:spTree"))
assert notesMaster.hf is None
53 changes: 53 additions & 0 deletions tests/oxml/test_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Unit-test suite for `pptx.oxml.text` module."""

from __future__ import annotations

from typing import cast

import pytest

from pptx.exc import InvalidXmlError
from pptx.oxml.text import CT_TextField

from ..unitutil.cxml import element


class DescribeCT_TextField(object):
"""Unit-test suite for `pptx.oxml.text.CT_TextField` (the `a:fld` element)."""

def it_provides_read_access_to_its_id_attribute(self):
# ---cxml's grammar reserves `{` and `}` as attribute delimiters, so the
# ---literal `{GUID}` form cannot appear inline. The id attribute on
# ---<a:fld> is XsdString-typed so any token works for round-trip tests;
# ---real fields generate {uuid4()} values at author time (Phase 3).
fld = cast(CT_TextField, element("a:fld{id=fld-1,type=slidenum}"))
assert fld.id == "fld-1"

def it_raises_InvalidXmlError_when_id_is_missing(self):
fld = cast(CT_TextField, element("a:fld"))
with pytest.raises(InvalidXmlError):
_ = fld.id

@pytest.mark.parametrize(
("fld_cxml", "expected_type"),
[
("a:fld{id=foo,type=slidenum}", "slidenum"),
("a:fld{id=foo,type=datetime1}", "datetime1"),
("a:fld{id=foo,type=datetime13}", "datetime13"),
("a:fld{id=foo,type=title}", "title"),
("a:fld{id=foo}", None),
],
)
def it_provides_read_access_to_its_type_attribute(
self, fld_cxml: str, expected_type: str | None
):
fld = cast(CT_TextField, element(fld_cxml))
assert fld.type == expected_type

def it_returns_empty_string_for_text_when_a_t_is_absent(self):
fld = cast(CT_TextField, element("a:fld{id=foo}"))
assert fld.text == ""

def it_reads_the_text_of_its_a_t_child(self):
fld = cast(CT_TextField, element('a:fld{id=foo,type=slidenum}/a:t"42"'))
assert fld.text == "42"
Loading