Skip to content

feat(text): Field authoring — _Paragraph.add_field, _Field class, CT_TextField.text setter (Phase 3)#50

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

feat(text): Field authoring — _Paragraph.add_field, _Field class, CT_TextField.text setter (Phase 3)#50
MHoroszowski merged 1 commit into
masterfrom
feature/headers-footers-phase3

Conversation

@MHoroszowski
Copy link
Copy Markdown
Owner

Phase 3 of #20 — Field authoring API

Ports scanny/python-pptx#797 ("Added a:fld type to paragraphs for page numbers and datetimes") onto Phase 1's (PR #48) OOXML primitives and Phase 2's (PR #49) public slide/master surface. Manually re-derived against this fork's ruff-formatted, post-Phase-1 source per CLAUDE.md §2 — git cherry-pick would conflict on every touched file because upstream predates the repo-wide ruff format pass (PR #10).

What this PR lets users do

p = slide.placeholders[1].text_frame.paragraphs[0]
p.clear()
fld = p.add_field()
fld.type = "slidenum"     # or "datetime1".."datetime13", "title", etc.
fld.text = "<#>"          # the placeholder glyph PowerPoint shows before resolving
prs.save("out.pptx")

Open the resulting .pptx in PowerPoint or Keynote and the slide number renders automatically. The id GUID is generated transparently in the format PowerPoint emits when the user runs Insert → Slide Number.

Changes

  • pptx.oxml.simpletypes.ST_FieldType (NEW) — XsdString subclass for the a:fld@type attribute value. Phase 1 used a plain XsdString placeholder; this is the named subtype.
  • pptx.oxml.text.CT_TextField.text — read-only property from Phase 1 now has a setter. Writes through get_or_add_t() and routes the value through CT_TextField._escape_ctrl_chars (NEW static method) which replaces chars in [\x00-\x08\x0B-\x1F] with _xNNNN_ uppercase-hex form per OOXML §22.9.2.19, leaving \t (0x09) and \n (0x0A) alone.
  • pptx.oxml.text.CT_TextParagraph.fldZeroOrMore("a:fld", successors=("a:endParaRPr",)) accessor. The a:pPr successor tuple already named a:fld per Phase 1 (forward declaration). xmlchemy auto-generates _add_fld() from the ZeroOrMore.
  • pptx.text.text._Field (NEW) — public-via-add_field-return-value class wrapping <a:fld>. Leading-underscore private name matches _Run and _Paragraph. Properties: font (Font wrapping rPr), text (read/write, routes through the escaping setter), type (read/write, str | None).
  • pptx.text.text._Paragraph.add_field() (NEW) — appends a fresh <a:fld> with a uuid4 GUID id wrapped in braces, uppercase hex. Returns a _Field; caller sets type and optionally text. Run-style symmetry is deliberate — users who know add_run() don't have to learn a new pattern.

Out of scope (deliberate)

  • Field discovery during paragraph iterationp.runs continues to yield only _Run objects. Phase 4 will surface _Field instances via a separate accessor.
  • HandoutMaster Python class and watermark helper — Phase 5.
  • MSO_FIELD_TYPE enum — type stays plain str for now to mirror Added a:fld type to paragraphs for page numbers and datetimes scanny/python-pptx#797. An enum can land in a later cleanup once the canonical field-type list is settled.

Verification (local, CPython 3.14.4)

python3 -m pytest tests/ -q                       → 3626 passed in 5.32s (+28 vs Phase 2 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_phase3.py         → PASS (id={2ED44585-07B5-4BC8-93B2-49122D50BCC2}
                                                          + type='slidenum' + text='<#>' all round-tripped)

28 new tests:

  • 14 in tests/oxml/test_text.pyCT_TextField setter, _escape_ctrl_chars (BEL/tab/newline edges), CT_TextParagraph.fld/_add_fld.
  • 14 in tests/text/test_text.pyDescribe_Field (10 tests: font, text get/set with escape, type get/set/clear) + Describe_Paragraph_add_field (4 tests: GUID regex match, distinct ids on consecutive calls, fld-after-runs ordering, parent chain).

Refs #20.

…TextField.text setter (Phase 3)

Public Python API for the headers/footers/slide-numbers/dates epic (#20).
Phase 3 adds the field-authoring surface that lets users create
auto-updating slide numbers, dates, and other PowerPoint-resolved fields
inside any paragraph. Builds on Phase 1 (PR #48) OOXML primitives and
Phase 2 (PR #49) slide/master public API.

Design source: scanny#797 ("Added a:fld type to paragraphs
for page numbers and datetimes"). Manually ported — per CLAUDE.md §2,
this fork's master had a repo-wide ruff format pass (PR #10) while
upstream did not, so cherry-pick conflicts on whitespace across every
touched file. Semantic diff re-derived against the current ruff-
formatted, post-Phase-1 source.

Changes:
- pptx.oxml.simpletypes.ST_FieldType (NEW) — XsdString subclass for
  the `a:fld@type` attribute value, replacing the plain XsdString
  declaration Phase 1 used as a placeholder.
- pptx.oxml.text.CT_TextField.text — read-only property from Phase 1
  now has a setter. Writes through get_or_add_t() and routes the value
  through CT_TextField._escape_ctrl_chars (NEW static method) which
  replaces chars in `[\x00-\x08\x0B-\x1F]` with `_xNNNN_` uppercase-hex
  form per OOXML §22.9.2.19, leaving `\t` (0x09) and `\n` (0x0A) alone.
- pptx.oxml.text.CT_TextParagraph.fld — ZeroOrMore("a:fld", successors=
  ("a:endParaRPr",)) accessor; the `a:pPr` successor tuple already named
  `a:fld` per Phase 1 (forward declaration). xmlchemy auto-generates
  `_add_fld()` from the ZeroOrMore.
- pptx.text.text._Field (NEW) — public-via-add_field-return-value class
  wrapping `<a:fld>`. Leading-underscore private name matches `_Run` and
  `_Paragraph`. Properties: `font` (Font wrapping rPr), `text` (read/
  write, routes through the escaping setter), `type` (read/write,
  str | None).
- pptx.text.text._Paragraph.add_field() (NEW) — appends a fresh
  `<a:fld>` with a uuid4 GUID id wrapped in braces, uppercase hex —
  matches what PowerPoint's "Insert → Slide Number" writes. Returns
  a `_Field`; caller sets `type` and optionally `text`. The Run-style
  symmetry is deliberate: users who know `add_run()` should not have to
  learn a new pattern.

Out of scope for Phase 3 (deliberate):
- Field discovery during paragraph iteration — `p.runs` continues to
  yield only `_Run` objects. Phase 4 will surface `_Field` instances
  alongside, with a stable ordering rule.
- HandoutMaster Python class and watermark helper — Phase 5.
- MSO_FIELD_TYPE enum — `type` stays plain `str` for now to mirror
  scanny#797. An enum can land in a later cleanup once the canonical
  field-type list is settled.

Verification (local, CPython 3.14.4):
- python3 -m pytest tests/ -q                       → 3626 passed in 5.32s (+28 vs Phase 2 baseline)
  - 14 new tests in tests/oxml/test_text.py (CT_TextField setter +
    _escape_ctrl_chars + CT_TextParagraph.add_fld)
  - 14 new tests in tests/text/test_text.py (Describe_Field ×10 +
    Describe_Paragraph_add_field ×4)
- 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_phase3.py         → PASS (full <a:fld> with
  id, type, and text round-tripped through save+reopen; GUID preserved
  byte-for-byte at {2ED44585-07B5-4BC8-93B2-49122D50BCC2})

Refs #20.
@MHoroszowski MHoroszowski merged commit 3bd4216 into master May 13, 2026
16 checks passed
@MHoroszowski MHoroszowski deleted the feature/headers-footers-phase3 branch May 13, 2026 23:29
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