Skip to content

feat: Slide / Master / Layout headers-footers public API (Phase 2)#49

Merged
MHoroszowski merged 1 commit into
masterfrom
feature/headers-footers-phase2
May 13, 2026
Merged

feat: Slide / Master / Layout headers-footers public API (Phase 2)#49
MHoroszowski merged 1 commit into
masterfrom
feature/headers-footers-phase2

Conversation

@MHoroszowski
Copy link
Copy Markdown
Owner

Phase 2 of #20 — public Slide / Master / Layout API

Builds on Phase 1 (PR #48). Ships the user-facing surface that sits on top of the OOXML wrappers — what end users will actually call. Phase 3 adds Field-based date auto-update; Phase 5 adds the HandoutMaster Python class and a watermark helper.

Changes

  • SlideLayout / SlideMaster / NotesMaster — four show_* visibility properties each, via a new private _HeaderFooterVisibility mixin so the twelve property bodies are defined once and inherited three times.
    • show_slide_number, show_footer, show_date, show_header — bool getters/setters backed by <p:hf> from Phase 1.
    • Getter returns True when <p:hf> is absent (PowerPoint default). Setter creates <p:hf> on first False, never auto-cleans it when it becomes all-True (low-value churn).
  • Slide — per-slide accessors:
    • has_footer, footer (str | None, get/set). Setter clones the FOOTER LayoutPlaceholder from slide.slide_layout on first set; raises ValueError if the layout has no FOOTER placeholder. Setting to None or "" clears the text, keeps the placeholder.
    • has_slide_number — presence check; no paired text accessor (auto-filled by PowerPoint).
    • has_date, date_text (str | None, get/set) — parallel Fixed-mode semantics to footer. The <a:fld> auto-update path is Phase 3 scope, not landed here.
  • Two private helpers (_first_ph_of_type, _layout_ph_of_type) factor placeholder iteration so the public properties stay readable.

Out of scope (deliberate)

  • Field class / Run.add_field() — Phase 3 (port of scanny#797).
  • HandoutMaster Python class / HandoutMasterPart plumbing — Phase 5.
  • Watermark helper — Phase 5.
  • Cross-slide "apply to all" semantics — caller iterates explicitly.
  • DATE Fixed-vs-AutoUpdate mode toggle — Phase 3.

Verification (local, CPython 3.14.4)

python3 -m pytest tests/ -q                       → 3598 passed in 5.75s (+84 vs Phase 1 baseline)
python3 -m ruff check src tests                   → All checks passed!
python3 -m ruff format --check src tests          → 216 files already formatted
python3 -m behave features/ --no-color            → 1048 scenarios, 0 failed
python3 uat/uat_headers_footers_phase2.py         → PASS (layout.show_footer + slide.footer both round-trip)

41 new test methods (84 pytest items with parameterizations) across DescribeSlide, DescribeSlideLayout, DescribeSlideMaster, and the new DescribeNotesMaster.

Refs #20.

Public Python API for the headers/footers/slide-numbers/dates epic (#20).
Phase 2 lands the user-facing surface on top of the Phase 1 (PR #48)
OOXML primitives. Phase 3 adds Field-based date auto-update; Phase 5
adds the HandoutMaster Python class and watermark helper.

Changes:
- pptx.slide._HeaderFooterVisibility (NEW) — mixin providing the four
  `show_*` properties (show_slide_number, show_footer, show_date,
  show_header) for any template element that carries a `<p:hf>` child.
  Inherited by SlideLayout, SlideMaster, and NotesMaster. Getter
  semantics: `<p:hf>` absent → True (PowerPoint default); present →
  the effective attribute value (each defaults to True per Phase 1's
  OptionalAttribute(default=True)). Setter semantics: assigning True
  when `<p:hf>` is absent is a no-op (default-True needs no element);
  assigning False creates `<p:hf>` via the Phase 1 ZeroOrOne accessor
  (`get_or_add_hf`) and writes the attribute as "0". An existing
  `<p:hf>` element is retained when all attrs become True — avoiding
  low-value XML churn on toggle-back-on.
- pptx.slide.Slide — gains `has_footer`, `footer` (str | None, with
  setter), `has_slide_number` (read-only — auto-filled by PowerPoint),
  `has_date`, and `date_text` (str | None, with setter, Fixed-mode only;
  `<a:fld>` auto-update remains Phase 3 scope). Two private helpers
  centralize the placeholder iteration: `_first_ph_of_type` walks the
  slide's own placeholders, `_layout_ph_of_type` walks the layout's
  placeholders for the clone-on-first-write path. Both return the first
  match in document order. Text getters call `text_frame.text` on the
  matched placeholder; text setters clone the layout placeholder via
  `self.shapes.clone_placeholder` when the slide has no matching
  placeholder yet, mirroring how PowerPoint promotes a layout-level
  placeholder to slide-level on first edit. Setting None or "" clears
  the text but does not remove the placeholder shape. Setting a
  non-empty string when the layout itself has no FOOTER (or DATE)
  placeholder raises ValueError with a precise message.

Design notes:
- The mixin lives in pptx.slide (not a separate module) because its
  three users all live there and the API surface is small. The
  `_element` annotation on the mixin is a union of the three concrete
  template element types, gated by a TYPE_CHECKING import so runtime
  attribute access works on whichever element type the concrete class
  carries.
- Slide accessors lean on `placeholder_format.type` for type discovery
  rather than poking `element.ph_type`, matching the established
  `NotesSlide.notes_placeholder` style in this same file. The lookup
  helpers return `None` rather than raising so callers can use them
  as `is None` guards.
- The footer/date setters intentionally do NOT remove the placeholder
  on clear. Removing a shape just because its text is empty would be
  surprising and would also strip layout-derived formatting; clearing
  text matches what PowerPoint does when the user backspaces footer
  content.

Test counts:
- tests/test_slide.py: +41 new test methods covering all 38 ISCs in
  the working ISA (12 template `show_*` getter/setter cases across
  SlideLayout / SlideMaster / NotesMaster; Slide.footer/has_footer
  with cloning, idempotent rewrite, clear-on-None, ValueError on no
  layout placeholder; Slide.has_slide_number; Slide.has_date and
  date_text with the parallel set; helper coverage for first-match
  document-order semantics).
- pytest: 3598 passed (3514 baseline + 84 new — includes pytest
  parameterizations counted by collection rather than by `def`),
  0 failed. Wall clock 5.11s.
- ruff check: All checks passed. ruff format: 216 files already
  formatted (no diff).
- behave: 1048 scenarios passed, 0 failed (zero regression vs Phase 1
  baseline).
- uat/uat_headers_footers_phase2.py: PASS — toggles
  `layout.show_footer = False` on Layout 0 of test.pptx, sets
  `slide.footer = "Phase 2 round-trip"` on Slide 0, saves, reopens,
  and asserts both round-trip.

Refs #20.
Builds on Phase 1 (PR #48 / commit 0223199).
@MHoroszowski MHoroszowski merged commit dfe9905 into master May 13, 2026
16 checks passed
@MHoroszowski MHoroszowski deleted the feature/headers-footers-phase2 branch May 13, 2026 22:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant