Skip to content

Commit 87b22c7

Browse files
authored
Merge pull request #51 from MHoroszowski/feature/headers-footers-phase4
feat(text): _Paragraph.fields field-discovery accessor (Phase 4)
2 parents 3bd4216 + f0fc1f8 commit 87b22c7

2 files changed

Lines changed: 58 additions & 0 deletions

File tree

src/pptx/text/text.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,17 @@ def runs(self) -> tuple[_Run, ...]:
778778
"""Sequence of runs in this paragraph."""
779779
return tuple(_Run(r, self) for r in self._element.r_lst)
780780

781+
@property
782+
def fields(self) -> tuple[_Field, ...]:
783+
"""Sequence of fields in this paragraph in document order.
784+
785+
Mirrors :attr:`runs` but yields :class:`_Field` instances wrapping each
786+
``<a:fld>`` child element. Useful for discovering existing slide-number,
787+
date, and other PowerPoint-resolved fields in a deck — `.runs` deliberately
788+
excludes fields so that pre-existing iteration semantics stay intact.
789+
"""
790+
return tuple(_Field(f, self) for f in self._element.fld_lst)
791+
781792
@property
782793
def space_after(self) -> Length | None:
783794
"""The spacing to appear between this paragraph and the subsequent paragraph.

tests/text/test_text.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,53 @@ def it_provides_access_to_its_runs(self, runs_fixture):
902902
assert isinstance(r, _Run)
903903
assert r._parent == paragraph
904904

905+
def it_returns_an_empty_tuple_of_fields_when_paragraph_has_none(self):
906+
paragraph = _Paragraph(element("a:p"), None)
907+
assert paragraph.fields == ()
908+
909+
def it_provides_access_to_a_single_field(self):
910+
paragraph = _Paragraph(element('a:p/a:fld{id=fld-1,type=slidenum}/a:t"sn"'), None)
911+
fields = paragraph.fields
912+
assert len(fields) == 1
913+
assert isinstance(fields[0], _Field)
914+
assert fields[0].type == "slidenum"
915+
assert fields[0].text == "sn"
916+
917+
def it_yields_multiple_fields_in_document_order(self):
918+
paragraph = _Paragraph(
919+
element(
920+
'a:p/(a:fld{id=fld-1,type=slidenum}/a:t"sn",a:fld{id=fld-2,type=datetime1}/a:t"date")'
921+
),
922+
None,
923+
)
924+
fields = paragraph.fields
925+
assert tuple(f.type for f in fields) == ("slidenum", "datetime1")
926+
assert tuple(f.text for f in fields) == ("sn", "date")
927+
928+
def it_chains_each_field_parent_back_to_the_paragraph(self):
929+
paragraph = _Paragraph(element('a:p/(a:fld{id=fld-1}/a:t"a",a:fld{id=fld-2}/a:t"b")'), None)
930+
for f in paragraph.fields:
931+
assert f._parent is paragraph
932+
933+
def it_returns_a_tuple_not_a_list(self):
934+
paragraph = _Paragraph(element("a:p/a:fld{id=fld-1}"), None)
935+
assert isinstance(paragraph.fields, tuple)
936+
937+
def it_keeps_runs_field_free_on_mixed_paragraphs(self):
938+
# ---a:r and a:fld interleaved: .runs yields only _Run, .fields yields only _Field
939+
paragraph = _Paragraph(
940+
element('a:p/(a:r/a:t"head",a:fld{id=fld-1,type=slidenum}/a:t"sn",a:r/a:t"tail")'),
941+
None,
942+
)
943+
runs = paragraph.runs
944+
fields = paragraph.fields
945+
assert len(runs) == 2
946+
assert tuple(r.text for r in runs) == ("head", "tail")
947+
assert all(isinstance(r, _Run) for r in runs)
948+
assert len(fields) == 1
949+
assert fields[0].type == "slidenum"
950+
assert isinstance(fields[0], _Field)
951+
905952
def it_knows_its_space_after(self, after_get_fixture):
906953
paragraph, expected_value = after_get_fixture
907954
assert paragraph.space_after == expected_value

0 commit comments

Comments
 (0)