Skip to content

feat(oxml): CT_HeaderFooter + CT_HandoutMaster + CT_TextField attrs (headers-footers Phase 1)#48

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

feat(oxml): CT_HeaderFooter + CT_HandoutMaster + CT_TextField attrs (headers-footers Phase 1)#48
MHoroszowski merged 1 commit into
masterfrom
feature/headers-footers-phase1

Conversation

@MHoroszowski
Copy link
Copy Markdown
Owner

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 / Field API 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), each OptionalAttribute defaulting to True when absent.
  • pptx.oxml.slide.CT_HandoutMaster (NEW) — <p:handoutMaster> 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 own _tag_seq to 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) and type (OptionalAttribute, XsdStringslidenum, datetime1..datetime13, title, etc.). Existing fld parse paths are unaffected.

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

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)
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)

31 new tests across tests/oxml/test_slide.py (CT_HeaderFooter ×11, CT_HandoutMaster ×3, hf-accessors ×6) and tests/oxml/test_text.py (new file — CT_TextField ×9).

Refs #20.

…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 MHoroszowski merged commit 4daba7e into master May 13, 2026
18 checks passed
@MHoroszowski MHoroszowski deleted the feature/headers-footers-phase1 branch May 13, 2026 22:25
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).
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.
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