Skip to content

Commit 9bc7c59

Browse files
fix: landscape orientation (#75)
* fix: landscape mode * version bump * remove com * fix: bad comparison operator
1 parent 4bd54a2 commit 9bc7c59

3 files changed

Lines changed: 188 additions & 15 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "cmi-docx"
3-
version = "0.6.6"
3+
version = "0.6.7"
44
description = "Additional tooling for Python-docx."
55
readme = "README.md"
66
requires-python = ">=3.12"

src/cmi_docx/declarative/document.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -464,34 +464,54 @@ def _pack_section( # noqa: C901, PLR0912
464464
if child.condition() and isinstance(child, paragraph.Paragraph): # ty:ignore[unresolved-attribute] already awaited.
465465
paragraphs_inserted += 1
466466

467-
docx_section = docx_doc.add_section()
467+
current_section: docx_section.Section = docx_doc.sections[-1]
468468

469469
if sec.properties:
470470
props = sec.properties
471-
if props.page_size:
472-
if "width" in props.page_size:
473-
docx_section.page_width = props.page_size["width"]
474-
if "height" in props.page_size:
475-
docx_section.page_height = props.page_size["height"]
476-
477-
if props.page_orientation:
478-
if props.page_orientation.lower() == "landscape":
479-
docx_section.orientation = docx_enum_section.WD_ORIENTATION.LANDSCAPE
480-
elif props.page_orientation.lower() == "portrait":
481-
docx_section.orientation = docx_enum_section.WD_ORIENTATION.PORTRAIT
471+
if props.page_size or props.page_orientation:
472+
if props.page_size:
473+
width = props.page_size.get("width") or current_section.page_width
474+
height = props.page_size.get("height") or current_section.page_height
475+
else:
476+
width = current_section.page_width
477+
height = current_section.page_height
478+
479+
if props.page_orientation:
480+
orientation = props.page_orientation.lower()
481+
if orientation == "landscape":
482+
if width < height: # ty:ignore[unsupported-operator]
483+
width, height = height, width
484+
current_section.page_width = width # ty:ignore[invalid-assignment]
485+
current_section.page_height = height # ty:ignore[invalid-assignment]
486+
current_section.orientation = (
487+
docx_enum_section.WD_ORIENTATION.LANDSCAPE
488+
)
489+
elif orientation == "portrait":
490+
if width > height: # ty:ignore[unsupported-operator]
491+
width, height = height, width
492+
current_section.page_width = width # ty:ignore[invalid-assignment]
493+
current_section.page_height = height # ty:ignore[invalid-assignment]
494+
current_section.orientation = (
495+
docx_enum_section.WD_ORIENTATION.PORTRAIT
496+
)
497+
else:
498+
current_section.page_width = width # ty:ignore[invalid-assignment]
499+
current_section.page_height = height # ty:ignore[invalid-assignment]
482500

483501
if sec.headers:
484502
for header_type, header in sec.headers.items():
485503
_pack_header(
486-
docx_doc, docx_section, header_type, header, default_comment_author
504+
docx_doc, current_section, header_type, header, default_comment_author
487505
)
488506

489507
if sec.footers:
490508
for footer_type, footer in sec.footers.items():
491509
_pack_footer(
492-
docx_doc, docx_section, footer_type, footer, default_comment_author
510+
docx_doc, current_section, footer_type, footer, default_comment_author
493511
)
494512

513+
docx_doc.add_section()
514+
495515
return paragraphs_inserted
496516

497517

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
"""Tests for SectionProperties page orientation in the declarative API."""
2+
3+
import pytest
4+
from docx.shared import Inches
5+
6+
from cmi_docx import declarative
7+
8+
9+
@pytest.mark.asyncio
10+
async def test_landscape_no_page_size() -> None:
11+
"""Test landscape orientation without explicit page_size.
12+
13+
When page_orientation is 'landscape' and no page_size is provided, the
14+
resulting section should have page_width greater than page_height.
15+
"""
16+
doc = declarative.Document(
17+
sections=[
18+
declarative.Section(
19+
children=[declarative.Paragraph(text="Landscape page")],
20+
properties=declarative.SectionProperties(
21+
page_orientation="landscape",
22+
),
23+
),
24+
],
25+
)
26+
27+
docx_doc = await doc.to_docx()
28+
section = docx_doc.sections[-2]
29+
assert section.page_width is not None
30+
assert section.page_height is not None
31+
assert section.page_width > section.page_height
32+
33+
34+
@pytest.mark.asyncio
35+
async def test_landscape_with_page_size() -> None:
36+
"""Test landscape orientation with portrait-sized page_size dimensions.
37+
38+
When page_orientation is 'landscape' and page_size provides portrait
39+
dimensions (width < height), the dimensions should be swapped so that
40+
the resulting section has page_width greater than page_height.
41+
"""
42+
doc = declarative.Document(
43+
sections=[
44+
declarative.Section(
45+
children=[declarative.Paragraph(text="Landscape with size")],
46+
properties=declarative.SectionProperties(
47+
page_orientation="landscape",
48+
page_size={"width": Inches(8.5), "height": Inches(11)},
49+
),
50+
),
51+
],
52+
)
53+
54+
docx_doc = await doc.to_docx()
55+
section = docx_doc.sections[-2]
56+
assert section.page_width is not None
57+
assert section.page_height is not None
58+
assert section.page_width > section.page_height
59+
60+
61+
@pytest.mark.asyncio
62+
async def test_portrait_no_page_size() -> None:
63+
"""Test portrait orientation without explicit page_size.
64+
65+
When page_orientation is 'portrait' and no page_size is provided, the
66+
resulting section should have page_width less than page_height.
67+
"""
68+
doc = declarative.Document(
69+
sections=[
70+
declarative.Section(
71+
children=[declarative.Paragraph(text="Portrait page")],
72+
properties=declarative.SectionProperties(
73+
page_orientation="portrait",
74+
),
75+
),
76+
],
77+
)
78+
79+
docx_doc = await doc.to_docx()
80+
section = docx_doc.sections[-2]
81+
assert section.page_width is not None
82+
assert section.page_height is not None
83+
assert section.page_width < section.page_height
84+
85+
86+
@pytest.mark.asyncio
87+
async def test_no_orientation() -> None:
88+
"""Test section without any page_orientation set.
89+
90+
When no page_orientation is specified, the resulting section should still
91+
have valid (non-None) page_width and page_height values.
92+
"""
93+
doc = declarative.Document(
94+
sections=[
95+
declarative.Section(
96+
children=[declarative.Paragraph(text="Default orientation")],
97+
),
98+
],
99+
)
100+
101+
docx_doc = await doc.to_docx()
102+
section = docx_doc.sections[-2]
103+
assert section.page_width is not None
104+
assert section.page_height is not None
105+
106+
107+
@pytest.mark.asyncio
108+
async def test_landscape_multi_section() -> None:
109+
"""Test that landscape properties apply to the correct section index.
110+
111+
With 2 declarative sections (portrait then landscape), the resulting
112+
docx_doc.sections list has 3 entries: sections[0] is portrait, sections[1]
113+
is landscape, and sections[2] is the trailing empty section added by the
114+
final add_section() call.
115+
116+
This test would have failed with the old off-by-one bug, where landscape
117+
properties were applied to sections[2] (the trailing sentinel) instead of
118+
sections[1] (the second declarative section).
119+
"""
120+
doc = declarative.Document(
121+
sections=[
122+
declarative.Section(
123+
children=[declarative.Paragraph(text="Portrait page")],
124+
properties=declarative.SectionProperties(
125+
page_orientation="portrait",
126+
),
127+
),
128+
declarative.Section(
129+
children=[declarative.Paragraph(text="Landscape page")],
130+
properties=declarative.SectionProperties(
131+
page_orientation="landscape",
132+
),
133+
),
134+
],
135+
)
136+
137+
docx_doc = await doc.to_docx()
138+
sections = list(docx_doc.sections)
139+
140+
# 2 declarative sections produce 3 total sections (trailing sentinel at [-1]).
141+
expected_section_count = 3
142+
assert len(sections) == expected_section_count
143+
144+
portrait_section = sections[0]
145+
landscape_section = sections[1]
146+
147+
assert portrait_section.page_width is not None
148+
assert portrait_section.page_height is not None
149+
assert portrait_section.page_width < portrait_section.page_height
150+
151+
assert landscape_section.page_width is not None
152+
assert landscape_section.page_height is not None
153+
assert landscape_section.page_width > landscape_section.page_height

0 commit comments

Comments
 (0)