Skip to content

feat(text): _Paragraph.fields field-discovery accessor (Phase 4)#51

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

feat(text): _Paragraph.fields field-discovery accessor (Phase 4)#51
MHoroszowski merged 1 commit into
masterfrom
feature/headers-footers-phase4

Conversation

@MHoroszowski
Copy link
Copy Markdown
Owner

Phase 4 of #20 — field discovery accessor

The small read-side companion to Phase 3's authoring API (PR #50). Adds _Paragraph.fields, a parallel to _Paragraph.runs that returns the paragraph's <a:fld> children wrapped as _Field instances in document order. Combined with Phase 3, the round-trip story closes:

# Phase 3: author
p = slide.placeholders[1].text_frame.paragraphs[0]
fld = p.add_field()
fld.type = "slidenum"
prs.save("out.pptx")

# Phase 4: discover and mutate
prs = Presentation("out.pptx")
p = prs.slides[0].placeholders[1].text_frame.paragraphs[0]
for f in p.fields:
    if f.type == "slidenum":
        f.text = "Page <#>"

Why this PR is small

The heavy lift landed earlier:

Phase 4 is one @property body — wrap self._element.fld_lst in _Field instances and return as a tuple. The existing fld_lst does all the work.

Anti-criteria upheld

  • _Paragraph.runs continues to yield only _Run (regression-pinned by it_keeps_runs_field_free_on_mixed_paragraphs).
  • _Paragraph.text semantics unchanged (already field-inclusive since Phase 3).
  • _Field class itself unchanged — read-only addition.

Verification (local, CPython 3.14.4)

python3 -m pytest tests/ -q                       → 3632 passed in 5.24s (+6 vs Phase 3 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_phase4.py         → PASS

UAT confirms the full Phase 3 → Phase 4 round-trip: opens uat/out_headers_footers_phase3.pptx, walks to the slidenum field via .fields, mutates its text via the discovered _Field handle, re-saves, re-opens, and reads it back via .fields again — clean round-trip.

6 new tests in tests/text/test_text.py Describe_Paragraph:

  • it_returns_an_empty_tuple_of_fields_when_paragraph_has_none
  • it_provides_access_to_a_single_field
  • it_yields_multiple_fields_in_document_order
  • it_chains_each_field_parent_back_to_the_paragraph
  • it_returns_a_tuple_not_a_list
  • it_keeps_runs_field_free_on_mixed_paragraphs (anti-regression)

What's next

Phase 5 closes out the epic with the HandoutMaster Python class + HandoutMasterPart plumbing, and the watermark helper. After that, issue #20 ships.

Refs #20.

Public Python API for the headers/footers/slide-numbers/dates epic (#20).
Phase 4 ships the small read-side companion to Phase 3's authoring API:
`_Paragraph.fields` returns the paragraph's `<a:fld>` children wrapped as
`_Field` instances in document order. Combined with Phase 3, a user can
now `add_field()` to write, save, re-open, and `paragraph.fields[0]` to
read or mutate.

Why this is small. The heavy lift landed in Phase 3 — `_Field` itself,
the OOXML primitives (Phase 1), and the field-aware `content_children`
+ `_Paragraph.text` (also Phase 3). Phase 4 just exposes the parallel-
to-`runs` discovery accessor that the existing `fld_lst` on
`CT_TextParagraph` made trivial.

Changes:
- pptx.text.text._Paragraph.fields — new `@property`, returns
  `tuple[_Field, ...]` built from `self._element.fld_lst`. Mirrors the
  shape of `_Paragraph.runs` exactly so the idiom is instantly familiar.

Out of scope for Phase 4 (deliberate):
- Interleaved ordered iterator combining `_Run` / `_Field` /
  `_LineBreak` in a single sequence. `content_children` already
  exposes this at the oxml layer; surfacing as public API can land
  later if real users ask.
- `Slide.has_auto_slide_number` / `has_auto_date` convenience flags —
  derive from `.fields` if useful; deferred.
- HandoutMaster class and watermark helper — Phase 5.

Anti-criteria upheld:
- `_Paragraph.runs` continues to yield only `_Run` instances. The new
  test `it_keeps_runs_field_free_on_mixed_paragraphs` regression-pins
  that on a mixed `(a:r, a:fld, a:r)` paragraph.
- `_Paragraph.text` semantics unchanged (still field-inclusive, as
  Phase 3 made it).
- `_Field` class itself is read-only here — no modifications.

Verification (local, CPython 3.14.4):
- python3 -m pytest tests/ -q                       → 3632 passed in 5.24s (+6 vs Phase 3 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_phase4.py         → PASS (opened uat/out_headers_footers_phase3.pptx,
  discovered fields[0] with type='slidenum' and id={2ED44585-...}, mutated text to "X" via the
  discovered handle, re-saved + re-opened, round-tripped clean)

Refs #20.
@MHoroszowski MHoroszowski merged commit 87b22c7 into master May 13, 2026
8 checks passed
@MHoroszowski MHoroszowski deleted the feature/headers-footers-phase4 branch May 13, 2026 23:34
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