Skip to content

feat: Section break insert and delete API#44

Merged
citconv-agents[bot] merged 2 commits intomasterfrom
agent/issue-21
Apr 3, 2026
Merged

feat: Section break insert and delete API#44
citconv-agents[bot] merged 2 commits intomasterfrom
agent/issue-21

Conversation

@citconv-agents
Copy link
Copy Markdown

@citconv-agents citconv-agents bot commented Apr 3, 2026

Summary

Implements #21

This PR was automatically generated by the Developer Agent.

Original Issue

Feature Description

python-docx supports appending a new section at the end of a document via Document.add_section(), and reading/modifying section.start_type on existing sections. However, there is no support for inserting a section break at an arbitrary position within the document body, nor for deleting an existing section break. In Word, section breaks are stored as <w:sectPr> elements inside paragraph properties (<w:pPr>) — not at the document body level — which makes mid-document insertion and deletion non-trivial without a proper API.

This issue adds high-level methods for inserting a section break after any paragraph and deleting an existing one.

Acceptance Criteria

Insertion

  • paragraph.insert_section_break(start_type=WD_SECTION.NEW_PAGE) inserts a section break after the given paragraph by adding a <w:sectPr><w:type w:val="..."/></w:sectPr> inside that paragraph's <w:pPr>
  • Supported start_type values: NEW_PAGE, ODD_PAGE, EVEN_PAGE, CONTINUOUS, NEXT_PAGE (alias for NEW_PAGE)
  • The method returns the new Section object representing the inserted section
  • Inserting a section break into a paragraph that already has one replaces the existing sectPr type rather than duplicating it
  • Inserted section breaks round-trip correctly — save and reload produces the same structure and section count

Deletion

  • paragraph.remove_section_break() removes the <w:sectPr> from the paragraph's <w:pPr> if one exists, merging that section into the next
  • paragraph.has_section_break (bool property) returns True if the paragraph contains a section break sectPr
  • Calling remove_section_break() on a paragraph without a section break is a no-op (no exception raised)
  • Deleting the final document-level sectPr (in <w:body>) is explicitly disallowed and raises ValueError with a clear message — the document must always have at least one section

Section enumeration consistency

  • After inserting or deleting a section break, document.sections reflects the updated section count correctly

Suggested Implementation

docx/text/paragraph.py — add to Paragraph:

@property
def has_section_break(self) -> bool:
    """True if this paragraph's pPr contains a sectPr (section break)."""
    pPr = self._p.find(qn('w:pPr'))
    return pPr is not None and pPr.find(qn('w:sectPr')) is not None

def insert_section_break(self, start_type=WD_SECTION.NEW_PAGE) -> Section:
    """Insert a section break after this paragraph. Returns the new Section."""
    from docx.oxml.ns import qn
    from docx.oxml import OxmlElement
    pPr = self._p.get_or_add_pPr()
    sectPr = pPr.find(qn('w:sectPr'))
    if sectPr is None:
        sectPr = OxmlElement('w:sectPr')
        pPr.append(sectPr)
    type_elem = sectPr.find(qn('w:type'))
    if type_elem is None:
        type_elem = OxmlElement('w:type')
        sectPr.insert(0, type_elem)
    type_elem.set(qn('w:val'), start_type.lower())
    return Section(sectPr, self._p._element.getparent())

def remove_section_break(self) -> None:
    """Remove the section break from this paragraph's pPr, if present."""
    pPr = self._p.find(qn('w:pPr'))
    if pPr is None:
        return
    sectPr = pPr.find(qn('w:sectPr'))
    if sectPr is not None:
        pPr.remove(sectPr)

Tests — add to tests/unit/text/test_paragraph.py:

  • Test insert_section_break adds correct XML for each start_type
  • Test inserting on a paragraph that already has a sectPr replaces type, doesn't duplicate
  • Test has_section_break returns correct bool
  • Test remove_section_break removes sectPr, no-op when absent
  • Test document.sections count updates after insert/remove
  • Test that removing the body-level sectPr raises ValueError

Dependencies

None. (Builds on the existing Section and WD_SECTION infrastructure.)

Out of Scope

  • Copying page layout properties (margins, orientation, headers/footers) from adjacent sections when inserting — caller is responsible for configuring the new section
  • Inserting a section break by paragraph index on the Document object — can be added as a convenience wrapper in a follow-up
  • Moving content between sections

Generated by Developer Agent using Claude Code

Add `has_section_break` property, `insert_section_break()` method, and
`remove_section_break()` method to the Paragraph class. These enable
inserting a section break after any paragraph and removing an existing
one, with proper section enumeration consistency.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@citconv-agents citconv-agents bot added the agent-pr PR created by an agent label Apr 3, 2026
@citconv-agents
Copy link
Copy Markdown
Author

citconv-agents bot commented Apr 3, 2026

Security Agent Report

SECURITY_PASS

Security Review — PR #44

Branch: agent/issue-21
Date: 2026-04-03
Reviewer: Security Agent

Summary

No security issues found. The changes are minimal, well-scoped, and follow established safe patterns in the codebase.

Changed Files

src/docx/text/paragraph.py

Adds three new methods to the Paragraph class:

  • has_section_break (property)
  • insert_section_break(start_type)
  • remove_section_break()

XML Injection / XXE: No risk. The start_type parameter is strongly typed as WD_SECTION_START (an enum). No raw string interpolation into XML. Element creation goes through get_or_add_pPr() / get_or_add_sectPr() — established lxml-backed helpers that produce well-formed elements only. No external XML is parsed.

Path Traversal: Not applicable. No file paths involved.

Data Exposure: No sensitive data is read, written, or transmitted.

Secrets: None present.

tests/text/test_paragraph.py

Adds unit tests for the new paragraph methods.

No security concerns. Test fixtures use cxml compact XML expressions (safe, internal DSL). No external files, credentials, or sensitive data referenced.

Dependency Changes

None. No new packages or version changes introduced.

Conclusion

All changes are read/write operations on in-memory lxml elements, constrained by typed enum inputs. The implementation follows the project's established three-layer pattern and introduces no new attack surface.

@citconv-agents
Copy link
Copy Markdown
Author

citconv-agents bot commented Apr 3, 2026

Security Agent Report

SECURITY_PASS

Security Review — PR #44 (Section Break Insert/Delete API)

Reviewed: 2026-04-03
Branch: agent/issue-21 vs origin/master
Files Changed:

  • src/docx/text/paragraph.py
  • tests/text/test_paragraph.py

Summary

No security issues found. The changes are limited to adding has_section_break, insert_section_break(), and remove_section_break() to the Paragraph class, plus corresponding tests.


Checks Performed

XML Injection / XXE

  • insert_section_break() sets sectPr.start_type using a WD_SECTION_START enum value — not a raw user string. The enum acts as a strict allow-list; values flow through the existing OOXML type layer which controls serialization. No unsanitized string is interpolated into XML.
  • No parse_xml() calls on user-supplied input.
  • No external entity declarations or DTD processing introduced.

Path Traversal

  • No file paths constructed or accessed.

New Dependencies

  • No new packages added. Imports are internal (docx.enum.section, docx.section), both already present in the codebase.

Sensitive Data / Secrets

  • No credentials, tokens, or API keys present.
  • No logging of document content.

Unsafe File Handling

  • No file I/O in the changed code.

Notes

The implementation correctly delegates mutation to the existing get_or_add_pPr() / get_or_add_sectPr() / _remove_sectPr() helpers in the OOXML layer, keeping input handling consistent with the rest of the library. No special treatment is needed.

@citconv-agents citconv-agents bot added the security-passed Security agent passed label Apr 3, 2026
@citconv-agents
Copy link
Copy Markdown
Author

citconv-agents bot commented Apr 3, 2026

Review Agent

REVIEW_APPROVED

Summary

PR #21 adds has_section_break, insert_section_break(), and remove_section_break() to Paragraph. The implementation is correct, follows project conventions, and is well-tested. No blocking issues found.

Positive Notes

  • Correct use of get_or_add_pPr() / get_or_add_sectPr() / _remove_sectPr() — all generated by ZeroOrOne on CT_PPr, no manual XML manipulation.
  • The circular-import pattern (from docx.section import Section as SectionCls inside the method body, with from docx.section import Section under TYPE_CHECKING) is the established idiom in this codebase and is correctly applied.
  • Parametrized tests cover the important cases: no pPr, pPr without sectPr, pPr with sectPr, and replacing an existing sectPr type.
  • Integration-style test it_updates_section_count_on_insert_and_remove verifies CT_Document.sectPr_lst changes correctly as breaks are added and removed.
  • remove_section_break() is a correct no-op when no break is present.

Minor Issues (non-blocking)

1. Docstring typo — src/docx/text/paragraph.py, insert_section_break

The docstring says:

defaults to WD_SECTION.NEW_PAGE

but the correct name is WD_SECTION_START.NEW_PAGE (the enum class is WD_SECTION_START, not WD_SECTION). Trivial to fix.

2. Missing test case: replace existing sectPr type with NEW_PAGE

it_can_insert_a_section_break tests replacing a CONTINUOUS sectPr with ODD_PAGE, but not with NEW_PAGE. When start_type=NEW_PAGE the setter calls _remove_type() rather than setting a value, so the resulting XML differs. Adding one parametrized case:

(
    "w:p/w:pPr/w:sectPr/w:type{w:val=continuous}",
    WD_SECTION_START.NEW_PAGE,
    WD_SECTION_START.NEW_PAGE,
),

would close that gap. Not a blocking concern because the underlying CT_SectPr.start_type setter is already tested in the oxml layer, but it would make the Paragraph-level tests more complete.

3. Redundant guard in remove_section_break (cosmetic)

if pPr.sectPr is not None:
    pPr._remove_sectPr()

_remove_sectPr() (generated by ZeroOrOne) is already a no-op when the element is absent, so the guard is redundant. Not wrong — just slightly defensive. Leave it or drop it; either is fine.

@citconv-agents citconv-agents bot added the review-approved Review agent approved label Apr 3, 2026
@citconv-agents citconv-agents bot merged commit 2c5d7c5 into master Apr 3, 2026
7 of 8 checks passed
@citconv-agents citconv-agents bot deleted the agent/issue-21 branch April 3, 2026 12:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-pr PR created by an agent review-approved Review agent approved security-passed Security agent passed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant