Skip to content

Commit

Permalink
section: Section.iter_inner_content()
Browse files Browse the repository at this point in the history
* clean up section (more needs doing in feature/steps and tests)
* acceptance test
* unit test
  • Loading branch information
scanny committed Oct 10, 2023
1 parent 12a6bb8 commit 9c9106f
Show file tree
Hide file tree
Showing 11 changed files with 295 additions and 48 deletions.
5 changes: 5 additions & 0 deletions features/sct-section.feature
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ Feature: Access and change section properties
Then section.header is a _Header object


Scenario: Section.iter_inner_content()
Given a Section object of a multi-section document as section
Then section.iter_inner_content() produces the paragraphs and tables in section


Scenario Outline: Get section start type
Given a section having start type <start-type>
Then the reported section start type is <start-type>
Expand Down
79 changes: 50 additions & 29 deletions features/steps/section.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Step implementations for section-related features."""

from behave import given, then, when
from behave.runner import Context

from docx import Document
from docx.enum.section import WD_ORIENT, WD_SECTION
Expand All @@ -13,36 +14,43 @@


@given("a Section object as section")
def given_a_Section_object_as_section(context):
def given_a_Section_object_as_section(context: Context):
context.section = Document(test_docx("sct-section-props")).sections[-1]


@given("a Section object of a multi-section document as section")
def given_a_Section_object_of_a_multi_section_document_as_section(context: Context):
context.section = Document(test_docx("sct-inner-content")).sections[1]


@given("a Section object {with_or_without} a distinct first-page header as section")
def given_a_Section_object_with_or_without_first_page_header(context, with_or_without):
def given_a_Section_object_with_or_without_first_page_header(
context: Context, with_or_without: str
):
section_idx = {"with": 1, "without": 0}[with_or_without]
context.section = Document(test_docx("sct-first-page-hdrftr")).sections[section_idx]


@given("a section collection containing 3 sections")
def given_a_section_collection_containing_3_sections(context):
def given_a_section_collection_containing_3_sections(context: Context):
document = Document(test_docx("doc-access-sections"))
context.sections = document.sections


@given("a section having known page dimension")
def given_a_section_having_known_page_dimension(context):
def given_a_section_having_known_page_dimension(context: Context):
document = Document(test_docx("sct-section-props"))
context.section = document.sections[-1]


@given("a section having known page margins")
def given_a_section_having_known_page_margins(context):
def given_a_section_having_known_page_margins(context: Context):
document = Document(test_docx("sct-section-props"))
context.section = document.sections[0]


@given("a section having start type {start_type}")
def given_a_section_having_start_type(context, start_type):
def given_a_section_having_start_type(context: Context, start_type: str):
section_idx = {
"CONTINUOUS": 0,
"NEW_PAGE": 1,
Expand All @@ -55,7 +63,7 @@ def given_a_section_having_start_type(context, start_type):


@given("a section known to have {orientation} orientation")
def given_a_section_having_known_orientation(context, orientation):
def given_a_section_having_known_orientation(context: Context, orientation: str):
section_idx = {"landscape": 0, "portrait": 1}[orientation]
document = Document(test_docx("sct-section-props"))
context.section = document.sections[section_idx]
Expand All @@ -65,12 +73,14 @@ def given_a_section_having_known_orientation(context, orientation):


@when("I assign {bool_val} to section.different_first_page_header_footer")
def when_I_assign_value_to_section_different_first_page_hdrftr(context, bool_val):
def when_I_assign_value_to_section_different_first_page_hdrftr(
context: Context, bool_val: str
):
context.section.different_first_page_header_footer = eval(bool_val)


@when("I set the {margin_side} margin to {inches} inches")
def when_I_set_the_margin_side_length(context, margin_side, inches):
def when_I_set_the_margin_side_length(context: Context, margin_side: str, inches: str):
prop_name = {
"left": "left_margin",
"right": "right_margin",
Expand All @@ -85,7 +95,7 @@ def when_I_set_the_margin_side_length(context, margin_side, inches):


@when("I set the section orientation to {orientation}")
def when_I_set_the_section_orientation(context, orientation):
def when_I_set_the_section_orientation(context: Context, orientation: str):
new_orientation = {
"WD_ORIENT.PORTRAIT": WD_ORIENT.PORTRAIT,
"WD_ORIENT.LANDSCAPE": WD_ORIENT.LANDSCAPE,
Expand All @@ -95,17 +105,17 @@ def when_I_set_the_section_orientation(context, orientation):


@when("I set the section page height to {y} inches")
def when_I_set_the_section_page_height_to_y_inches(context, y):
def when_I_set_the_section_page_height_to_y_inches(context: Context, y: str):
context.section.page_height = Inches(float(y))


@when("I set the section page width to {x} inches")
def when_I_set_the_section_page_width_to_x_inches(context, x):
def when_I_set_the_section_page_width_to_x_inches(context: Context, x: str):
context.section.page_width = Inches(float(x))


@when("I set the section start type to {start_type}")
def when_I_set_the_section_start_type_to_start_type(context, start_type):
def when_I_set_the_section_start_type_to_start_type(context: Context, start_type: str):
new_start_type = {
"None": None,
"CONTINUOUS": WD_SECTION.CONTINUOUS,
Expand All @@ -121,15 +131,15 @@ def when_I_set_the_section_start_type_to_start_type(context, start_type):


@then("I can access a section by index")
def then_I_can_access_a_section_by_index(context):
def then_I_can_access_a_section_by_index(context: Context):
sections = context.sections
for idx in range(3):
section = sections[idx]
assert isinstance(section, Section)


@then("I can iterate over the sections")
def then_I_can_iterate_over_the_sections(context):
def then_I_can_iterate_over_the_sections(context: Context):
sections = context.sections
actual_count = 0
for section in sections:
Expand All @@ -139,13 +149,13 @@ def then_I_can_iterate_over_the_sections(context):


@then("len(sections) is 3")
def then_len_sections_is_3(context):
def then_len_sections_is_3(context: Context):
sections = context.sections
assert len(sections) == 3, "expected len(sections) of 3, got %s" % len(sections)


@then("section.different_first_page_header_footer is {bool_val}")
def then_section_different_first_page_header_footer_is(context, bool_val):
def then_section_different_first_page_header_footer_is(context: Context, bool_val: str):
actual = context.section.different_first_page_header_footer
expected = eval(bool_val)
assert actual == expected, (
Expand All @@ -154,49 +164,58 @@ def then_section_different_first_page_header_footer_is(context, bool_val):


@then("section.even_page_footer is a _Footer object")
def then_section_even_page_footer_is_a_Footer_object(context):
def then_section_even_page_footer_is_a_Footer_object(context: Context):
actual = type(context.section.even_page_footer).__name__
expected = "_Footer"
assert actual == expected, "section.even_page_footer is a %s object" % actual


@then("section.even_page_header is a _Header object")
def then_section_even_page_header_is_a_Header_object(context):
def then_section_even_page_header_is_a_Header_object(context: Context):
actual = type(context.section.even_page_header).__name__
expected = "_Header"
assert actual == expected, "section.even_page_header is a %s object" % actual


@then("section.first_page_footer is a _Footer object")
def then_section_first_page_footer_is_a_Footer_object(context):
def then_section_first_page_footer_is_a_Footer_object(context: Context):
actual = type(context.section.first_page_footer).__name__
expected = "_Footer"
assert actual == expected, "section.first_page_footer is a %s object" % actual


@then("section.first_page_header is a _Header object")
def then_section_first_page_header_is_a_Header_object(context):
def then_section_first_page_header_is_a_Header_object(context: Context):
actual = type(context.section.first_page_header).__name__
expected = "_Header"
assert actual == expected, "section.first_page_header is a %s object" % actual


@then("section.footer is a _Footer object")
def then_section_footer_is_a_Footer_object(context):
def then_section_footer_is_a_Footer_object(context: Context):
actual = type(context.section.footer).__name__
expected = "_Footer"
assert actual == expected, "section.footer is a %s object" % actual


@then("section.header is a _Header object")
def then_section_header_is_a_Header_object(context):
def then_section_header_is_a_Header_object(context: Context):
actual = type(context.section.header).__name__
expected = "_Header"
assert actual == expected, "section.header is a %s object" % actual


@then("section.iter_inner_content() produces the paragraphs and tables in section")
def step_impl(context: Context):
actual = [type(item).__name__ for item in context.section.iter_inner_content()]
expected = ["Table", "Paragraph", "Paragraph"]
assert actual == expected, f"expected: {expected}, got: {actual}"


@then("section.{propname}.is_linked_to_previous is True")
def then_section_hdrftr_prop_is_linked_to_previous_is_True(context, propname):
def then_section_hdrftr_prop_is_linked_to_previous_is_True(
context: Context, propname: str
):
actual = getattr(context.section, propname).is_linked_to_previous
expected = True
assert actual == expected, "section.%s.is_linked_to_previous is %s" % (
Expand All @@ -206,7 +225,7 @@ def then_section_hdrftr_prop_is_linked_to_previous_is_True(context, propname):


@then("the reported {margin_side} margin is {inches} inches")
def then_the_reported_margin_is_inches(context, margin_side, inches):
def then_the_reported_margin_is_inches(context: Context, margin_side: str, inches: str):
prop_name = {
"left": "left_margin",
"right": "right_margin",
Expand All @@ -222,7 +241,9 @@ def then_the_reported_margin_is_inches(context, margin_side, inches):


@then("the reported page orientation is {orientation}")
def then_the_reported_page_orientation_is_orientation(context, orientation):
def then_the_reported_page_orientation_is_orientation(
context: Context, orientation: str
):
expected_value = {
"WD_ORIENT.LANDSCAPE": WD_ORIENT.LANDSCAPE,
"WD_ORIENT.PORTRAIT": WD_ORIENT.PORTRAIT,
Expand All @@ -231,17 +252,17 @@ def then_the_reported_page_orientation_is_orientation(context, orientation):


@then("the reported page width is {x} inches")
def then_the_reported_page_width_is_width(context, x):
def then_the_reported_page_width_is_width(context: Context, x: str):
assert context.section.page_width == Inches(float(x))


@then("the reported page height is {y} inches")
def then_the_reported_page_height_is_11_inches(context, y):
def then_the_reported_page_height_is_11_inches(context: Context, y: str):
assert context.section.page_height == Inches(float(y))


@then("the reported section start type is {start_type}")
def then_the_reported_section_start_type_is_type(context, start_type):
def then_the_reported_section_start_type_is_type(context: Context, start_type: str):
expected_start_type = {
"CONTINUOUS": WD_SECTION.CONTINUOUS,
"EVEN_PAGE": WD_SECTION.EVEN_PAGE,
Expand Down
Binary file added features/steps/test_files/sct-inner-content.docx
Binary file not shown.
2 changes: 1 addition & 1 deletion pyrightconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
"reportUnnecessaryTypeIgnoreComment": true,
"stubPath": "./typings",
"typeCheckingMode": "strict",
"useLibraryCodeForTypes": false,
"useLibraryCodeForTypes": true,
"verboseOutput": true
}
21 changes: 17 additions & 4 deletions src/docx/oxml/document.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
"""Custom element classes that correspond to the document part, e.g. <w:document>."""

from __future__ import annotations

from typing import List

from docx.oxml.section import CT_SectPr
from docx.oxml.xmlchemy import BaseOxmlElement, ZeroOrMore, ZeroOrOne


Expand All @@ -9,10 +14,18 @@ class CT_Document(BaseOxmlElement):
body = ZeroOrOne("w:body")

@property
def sectPr_lst(self):
"""Return a list containing a reference to each ``<w:sectPr>`` element in the
document, in the order encountered."""
return self.xpath(".//w:sectPr")
def sectPr_lst(self) -> List[CT_SectPr]:
"""All `w:sectPr` elements directly accessible from document element.
Note this does not include a `sectPr` child in a paragraphs wrapped in
revision marks or other intervening layer, perhaps `w:sdt` or customXml
elements.
`w:sectPr` elements appear in document order. The last one is always
`w:body/w:sectPr`, all preceding are `w:p/w:pPr/w:sectPr`.
"""
xpath = "./w:body/w:p/w:pPr/w:sectPr | ./w:body/w:sectPr"
return self.xpath(xpath)


class CT_Body(BaseOxmlElement):
Expand Down
Loading

0 comments on commit 9c9106f

Please sign in to comment.