feat: HandoutMaster + add_text_watermark helper — Phase 5, closing #20#52
Merged
Merged
Conversation
Closes the headers/footers epic (#20). Phase 1 (PR #48) shipped the `CT_HandoutMaster` OOXML wrapper; Phases 2-4 (PRs #49, #50, #51) built out the public headers/footers/fields surface. Phase 5 adds the two remaining pieces: a public `HandoutMaster` Python class so users can read a handout master's visibility settings without dropping into XML, and a `SlideMaster.add_text_watermark()` helper that wraps the common "DRAFT"-style watermark pattern into one call. Why this is small. `_HeaderFooterVisibility` (Phase 2) and `CT_HandoutMaster` (Phase 1) already do the heavy lifting; Phase 5 just wires a Python proxy + part + accessor onto them and adds a thin convenience method. Changes: - pptx.slide.HandoutMaster — new class, inherits `_HeaderFooterVisibility, _BaseMaster` in the same order as `NotesMaster`. Docstring names the deferred auto-create. - pptx.slide.SlideMaster.add_text_watermark — new method. Adds a centered 6in x 1.5in textbox to the master at standard 10in x 7.5in slide coordinates (left=2in, top=3in). Text gets `PP_ALIGN.CENTER`; the first run gets `font.name`, `font.size`, mid-gray solid fill, and the configured `transparency` (default 0.7). Returns the new `Shape` so callers can re-position or restyle. - pptx.slide._HeaderFooterVisibility — `_element` type union widened to include `CT_HandoutMaster` so the mixin types cleanly under `HandoutMaster`. - pptx.parts.slide.HandoutMasterPart — new `BaseSlidePart` subclass. Defines `handout_master` lazyproperty; deliberately omits `create_default` / `_new` / `_new_theme_part` because no `handoutMaster.xml` template ships in this fork yet. - pptx.parts.presentation.PresentationPart.handout_master_part — new lazyproperty. Returns `part_related_by(RT.HANDOUT_MASTER)` when present; raises `ValueError` with a "no handout master" message (chained `from KeyError`) when absent. Unlike `notes_master_part`, this does NOT auto-create. - pptx.parts.presentation.PresentationPart.handout_master — new lazyproperty pass-through to `.handout_master_part.handout_master`. - pptx.presentation.Presentation.handout_master — new property pass-through, matching the shape of `notes_master`. - pptx.shapes.shapetree.MasterShapes.add_textbox — new method, parallel to the existing `SlideShapes.add_textbox`. Needed so `SlideMaster.add_text_watermark` can drop a textbox directly on the master's shape tree (master shapes previously had no `add_textbox`, only the slide / layout collections did). - pptx/__init__.py — registers `HandoutMasterPart` against `CT.PML_HANDOUT_MASTER` in `content_type_to_part_class_map` and adds it to the cleanup `del (...)` tuple. Out of scope for Phase 5 (deliberate, see ISA Out of Scope): - Auto-create of handout master when absent. Synthesizing one from scratch needs a known-good `handoutMaster.xml` template, and shipping that template + the theme wiring is risky without a reference deck. Phase 6+ if real users ask. The error message surfaces the deferral explicitly. - `HandoutMaster.add_placeholder()` / placeholder cloning — read-only this phase; the inherited visibility props are write-enabled, but the placeholder set on the handout master is static. - Image-watermark helper — `master.shapes.add_picture(...)` already does this in one line; wrapping it adds little value. - Per-slide watermark — `add_text_watermark` lives on `SlideMaster` so every slide using that master gets the watermark by inheritance; per-slide watermark is just `slide.shapes.add_textbox(...)` today. - `MSO_WATERMARK_PRESET` enum or canned styles — string text + numeric transparency is the entire API surface. Anti-criteria upheld: - No `handoutMaster.xml` template file added (verified by `find src -name '*handoutMaster*'`). - `Presentation.handout_master` does NOT silently auto-create — the `but_it_raises_ValueError_when_no_handout_master_is_present` test in `tests/parts/test_presentation.py` pins this. - No `add_image_watermark` or `MSO_WATERMARK_PRESET` (verified by `grep -rn 'add_image_watermark|MSO_WATERMARK' src/ tests/`). - `NotesMaster` and `NotesMasterPart` bodies are unchanged; only import lines that previously named just `NotesMaster` now also name `HandoutMaster`. - `it_has_no_create_default_classmethod` in `tests/parts/test_slide.py` regression-pins that `HandoutMasterPart` does NOT acquire `create_default` over time. Test counts: 19 new test cases (16 new `it_/but_/and_` functions; the `it_round_trips_show_attrs_via_hf` parametrize fans out to 4 cases). Distribution: 6 in `DescribeHandoutMaster`, 4 in `DescribeSlideMasterAddTextWatermark`, 4 in `DescribeHandoutMasterPart`, 3 in `DescribePresentationPart`, 1 in `DescribePresentation`. Pytest runs at 3651 passed (baseline 3632 + 19). Verification (local, CPython 3.14.4): $ python3 -m pytest tests/ -q | tail -3 ........................................................................ [100%] ................................................... [100%] 3651 passed in 5.28s $ python3 -m ruff check src tests | tail -3 All checks passed! $ python3 -m ruff format --check src tests | tail -3 216 files already formatted $ python3 -m behave features/ --no-color 2>&1 | tail -3 1048 scenarios passed, 0 failed, 0 skipped 3151 steps passed, 0 failed, 0 skipped Took 0min 1.696s $ python3 uat/uat_headers_footers_phase5.py opening /Users/mhoroszowski/Projects/AI/python-pptx/tests/test_files/test.pptx OK — missing handout master raises ValueError: presentation has no handout master; auto-create is deferred because no handout master template ships in this fork yet adding watermark 'DRAFT' to slide_masters[0] watermark shape: left=1828800 top=2743200 width=5486400 height=1371600 saving to /Users/mhoroszowski/Projects/AI/python-pptx/uat/out_headers_footers_phase5.pptx re-opening /Users/mhoroszowski/Projects/AI/python-pptx/uat/out_headers_footers_phase5.pptx round-tripped watermark: text='DRAFT' font='Calibri' size=72.0 transparency=0.7 OK — watermark transparency round-tripped at 0.7 PASS — handout-master absent-path and watermark helper verified Closes #20.
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.
MHoroszowski
added a commit
that referenced
this pull request
May 14, 2026
fix(oxml): scope CT_GroupShape.max_shape_id to spTree descendants (post-#52 hotfix)
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 5 of #20 — HandoutMaster + watermark helper (closes the epic)
Ships the final two pieces of issue #20: a public
HandoutMasterPython class so users can read and toggle handout-master visibility settings without dropping into XML, and aSlideMaster.add_text_watermark()helper that wraps the common "DRAFT every slide" pattern into one call.What this PR completes
Open
out.pptxin PowerPoint or Keynote: every slide using the modified master shows a centered, 72pt, ~70%-transparent "DRAFT" — and the handout-master footer toggle round-trips.Changes
pptx.slide.HandoutMaster(NEW) —_HeaderFooterVisibility+_BaseMaster, mirrorsNotesMasterstructure. Inheritsplaceholders,shapes, and the fourshow_*properties.pptx.parts.slide.HandoutMasterPart(NEW) —BaseSlidePartmirror ofNotesMasterPart, withoutcreate_default()/_new()/_new_theme_part(). Read-only.pptx.parts.presentation.PresentationPart— newhandout_master_partandhandout_masterlazyproperties. No auto-create — raisesValueError("presentation has no handout master; auto-create is deferred …")when the relationship is absent. Chainedfrom KeyErrorso the original lookup failure is preserved.pptx.presentation.Presentation.handout_master(NEW property) — pass-through, matchesnotes_mastershape.pptx.shapes.shapetree.MasterShapes.add_textbox(NEW, three lines) —_BaseShapesonly exposedadd_textboxonSlideShapes/LayoutShapes. The watermark helper needs it on the master.pptx.slide.SlideMaster.add_text_watermark(text, *, font_size=Pt(72), transparency=0.7, font_name="Calibri")(NEW) — centered textbox at 6in × 1.5in on the standard 10in × 7.5in slide; mid-gray (#808080) text; routes transparency through the existingFillFormat.transparencyfrom PR feat(dml): add transparency support for solid fills and colors (closes #17) #30. Returns theShapefor fine-tuning.pptx.__init__— registeredHandoutMasterPartagainstCT.PML_HANDOUT_MASTERincontent_type_to_part_class_map.Out of scope (deliberate)
handoutMaster.xmltemplate file plus theme wiring. RaisingValueErrorwith a clear message is the explicit Phase 5 contract.add_image_watermark— text only; users who need a picture watermark callmaster.shapes.add_picture(...)directly today.MSO_WATERMARK_PRESETenum / canned styles — string text + numeric transparency, that's the whole API surface.Verification (local, CPython 3.14.4)
19 new test cases:
DescribeHandoutMaster(6 cases): class existence, mixin chain,show_*round-trips.DescribeHandoutMasterPart(4 cases): instantiation,handout_masterlazyproperty, content-type registration, nocreate_default.DescribePresentationPartextension (3 cases):handout_master_partreturns related part, raisesValueErrorwhen absent, error message contains "no handout master".DescribePresentationextension (1 case):Presentation.handout_masterpass-through.DescribeSlideMasterAddTextWatermark(4 cases): returnsShape, text matches argument, default transparency, centered position.Epic close summary
Five phases shipped:
CT_HeaderFooter+CT_HandoutMaster+CT_TextFieldattrs (OOXML wrappers)Slide.footer/has_footer/has_slide_number/has_date/date_text+show_*on layout/master/notes-master_Paragraph.add_field()+_Fieldclass +CT_TextField.textsetter + control-char escape_Paragraph.fieldsdiscovery accessorHandoutMaster+HandoutMasterPart+Presentation.handout_master+SlideMaster.add_text_watermarkTest count: 3514 (issue-#20 epic start) → 3651 (epic close). Behave: 1048 / 0 throughout. Zero regressions across all five phases.
Closes #20.