feat(oxml): CT_HeaderFooter + CT_HandoutMaster + CT_TextField attrs (headers-footers Phase 1)#48
Merged
Conversation
…headers-footers Phase 1) OOXML foundation for the headers/footers/slide-numbers/dates/watermarks epic (#20). Phase 1 ships element-level wrappers only — the public Slide/Master/Field API lands in Phase 2 and later. Changes: - pptx.oxml.slide.CT_HeaderFooter (NEW) — `<p:hf>` element wrapper, with the four ECMA-376 §19.3.1.18 boolean attributes (sldNum, hdr, ftr, dt), each OptionalAttribute defaulting to True when absent. - pptx.oxml.slide.CT_HandoutMaster (NEW) — `<p:handoutMaster>` element with content model (cSld, clrMap, hf?, extLst?) per §19.3.1.24. Registered against `p:handoutMaster` so deepcopy + parse + xmlchemy flow against the right class. - `hf` ZeroOrOne accessor added to CT_SlideMaster, CT_SlideLayout, and CT_NotesMaster, using each class's existing `_tag_seq` to compute the correct successor tuple (so insertion never violates schema order). - pptx.oxml.text.CT_TextField — surfaces the two attributes the field authoring API in later phases needs: `id` (RequiredAttribute, XsdString, per spec a GUID) and `type` (OptionalAttribute, XsdString — values like `slidenum`, `datetime1`..`datetime13`, `title`). Existing fld parse paths are unaffected — none of them read .id today. Out of scope for Phase 1 (deliberate): - No public Slide.footer / has_slide_number / has_date API (Phase 2) - No Field / Run.add_field authoring surface (Phase 3 — scanny#797 port) - No HandoutMaster Python class or HandoutMasterPart plumbing (Phase 5) - No watermark helper (Phase 5) Plan correction surfaced during investigation: per ECMA-376, `<p:hf>` is a child of slide LAYOUTS and the three master types (slide, notes, handout) — not of individual slides. The original plan listed CT_Slide; the correct slot list is the four templates. Per-slide footer text flows through the FOOTER-typed placeholder shape (Phase 2's Slide.footer). Verification (local, CPython 3.14.4): - python3 -m pytest tests/ -q → 3514 passed in 6.31s (+29 vs baseline) - tests/oxml/test_slide.py + tests/oxml/test_text.py: 31 new passing tests - 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_phase1.py → PASS (hf attrs round-trip on a real .pptx) Refs #20.
MHoroszowski
added a commit
that referenced
this pull request
May 13, 2026
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).
This was referenced May 13, 2026
MHoroszowski
added a commit
that referenced
this pull request
May 14, 2026
Sharpens the agent/maintainer boundary on UAT: agents may execute
uat/uat_*.py to QA the script itself (verify it asserts what it claims,
doesn't crash, exits non-zero on failure), but a green script PASS does
NOT constitute signoff. Signoff requires a human opening the .pptx in
PowerPoint or Keynote, unless explicitly delegated in a specific case.
Adds:
- Explicit examples of acceptable vs. unacceptable summary phrasing.
- Clarification that the §7 trinity (pytest + ruff + behave) is the
agent's full self-verification surface; UAT is not the fourth gate.
- Stop-and-ask directive when uncertain whether signoff has been
delegated for a particular case.
The prior §6 step 4 ("The maintainer runs the UAT") was the seed; this
codifies the execute-vs-signoff distinction that was implied but not
enforced. Five recent epic phases (#48..#52) collapsed the distinction
in their PR bodies; §6a closes that loophole.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 1 of #20 — OOXML foundation
First slice of the headers / footers / slide-numbers / dates / watermarks epic. Element-level wrappers only — the public
Slide/Master/FieldAPI lands in Phase 2+.Changes
pptx.oxml.slide.CT_HeaderFooter(NEW) —<p:hf>wrapper with the four ECMA-376 §19.3.1.18 booleans (sldNum,hdr,ftr,dt), eachOptionalAttributedefaulting toTruewhen absent.pptx.oxml.slide.CT_HandoutMaster(NEW) —<p:handoutMaster>with content model(cSld, clrMap, hf?, extLst?)per §19.3.1.24. Registered againstp:handoutMasterso deepcopy + parse + xmlchemy flow against the right class.hfZeroOrOneaccessor added toCT_SlideMaster,CT_SlideLayout, andCT_NotesMaster, using each class's own_tag_seqto compute the correct successor tuple — schema order preserved on insertion.pptx.oxml.text.CT_TextField— surfaces the two attrs the field authoring API in later phases needs:id(RequiredAttribute,XsdString, spec says GUID) andtype(OptionalAttribute,XsdString—slidenum,datetime1..datetime13,title, etc.). Existing fld parse paths are unaffected.Out of scope for Phase 1 (deliberate)
Slide.footer/has_slide_number/has_dateAPI (Phase 2)Field/Run.add_fieldauthoring surface (Phase 3 —scanny#797port)HandoutMasterPython class orHandoutMasterPartplumbing (Phase 5)Plan correction
Per ECMA-376,
<p:hf>is a child of slide layouts and the three master types (slide, notes, handout) — not of individual slides. The original plan listedCT_Slide; the correct slot list is the four templates. Per-slide footer text flows through theFOOTER-typed placeholder shape (Phase 2'sSlide.footer).Verification (local, CPython 3.14.4)
31 new tests across
tests/oxml/test_slide.py(CT_HeaderFooter×11,CT_HandoutMaster×3, hf-accessors ×6) andtests/oxml/test_text.py(new file —CT_TextField×9).Refs #20.