Skip to content

Commit f4ec56a

Browse files
authored
Merge pull request #39 from MHoroszowski:feature/modernization-phase1
feat(ergonomics): PathLike + PERCENT_40 typo + Slide.background.element fix (modernization Phase 1)
2 parents 76291a2 + ad7edb8 commit f4ec56a

8 files changed

Lines changed: 336 additions & 10 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Feature: Modernization Phase 1 — PathLike, PERCENT_40, slide.background.element
2+
In order to use python-pptx with modern Python idioms
3+
As a developer using python-pptx
4+
I need pathlib.Path support, the PERCENT_40 enum typo fixed,
5+
and slide.background.element to return the actual <p:bg> element
6+
7+
8+
Scenario: Open a presentation from a pathlib.Path
9+
Given a freshly-saved presentation at a Path
10+
When I call Presentation(path) with the Path
11+
Then I get a presentation back
12+
13+
14+
Scenario: Save a presentation to a pathlib.Path
15+
Given a fresh presentation
16+
When I save it to a Path
17+
Then a non-empty .pptx file exists at that Path
18+
19+
20+
Scenario: PERCENT_40 enum is exposed with the correct name
21+
Then MSO_PATTERN_TYPE.PERCENT_40 exists with xml_value pct40
22+
And the broken name ERCENT_40 does not exist
23+
24+
25+
Scenario: slide.background.element returns the <p:bg> element
26+
Given a fresh slide on a fresh presentation
27+
Then slide.background.element local-name is bg

features/steps/modernization.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Gherkin step implementations for Modernization Phase 1 (issue #29)."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
7+
from behave import given, then, when
8+
from environment import scratch_dir # noqa: E402
9+
10+
from pptx import Presentation
11+
from pptx.enum.dml import MSO_PATTERN_TYPE
12+
13+
14+
# given ===================================================
15+
16+
17+
@given("a freshly-saved presentation at a Path")
18+
def given_freshly_saved_presentation(context):
19+
target = Path(scratch_dir) / "modernization_seed.pptx"
20+
target.parent.mkdir(parents=True, exist_ok=True)
21+
Presentation().save(target)
22+
context.path = target
23+
24+
25+
@given("a fresh presentation")
26+
def given_fresh_presentation(context):
27+
context.prs = Presentation()
28+
29+
30+
@given("a fresh slide on a fresh presentation")
31+
def given_fresh_slide(context):
32+
prs = Presentation()
33+
context.slide = prs.slides.add_slide(prs.slide_layouts[6])
34+
35+
36+
# when ====================================================
37+
38+
39+
@when("I call Presentation(path) with the Path")
40+
def when_call_presentation_with_path(context):
41+
context.prs = Presentation(context.path)
42+
43+
44+
@when("I save it to a Path")
45+
def when_save_to_path(context):
46+
target = Path(scratch_dir) / "modernization_out.pptx"
47+
target.parent.mkdir(parents=True, exist_ok=True)
48+
context.prs.save(target)
49+
context.saved_path = target
50+
51+
52+
# then ====================================================
53+
54+
55+
@then("I get a presentation back")
56+
def then_presentation_back(context):
57+
assert context.prs is not None
58+
59+
60+
@then("a non-empty .pptx file exists at that Path")
61+
def then_non_empty_file_exists(context):
62+
assert context.saved_path.exists()
63+
assert context.saved_path.stat().st_size > 0
64+
65+
66+
@then("MSO_PATTERN_TYPE.PERCENT_40 exists with xml_value pct40")
67+
def then_percent_40_correct(context):
68+
assert MSO_PATTERN_TYPE.PERCENT_40.xml_value == "pct40"
69+
70+
71+
@then("the broken name ERCENT_40 does not exist")
72+
def then_ercent_40_absent(context):
73+
assert hasattr(MSO_PATTERN_TYPE, "ERCENT_40") is False
74+
75+
76+
@then("slide.background.element local-name is bg")
77+
def then_background_element_is_bg(context):
78+
from lxml import etree
79+
80+
bg_elm = context.slide.background.element
81+
local = etree.QName(bg_elm.tag).localname
82+
assert local == "bg", f"expected 'bg', got '{local}'"

src/pptx/api.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,22 @@
1818
from pptx.parts.presentation import PresentationPart
1919

2020

21-
def Presentation(pptx: str | IO[bytes] | None = None) -> presentation.Presentation:
21+
def Presentation(
22+
pptx: str | os.PathLike[str] | IO[bytes] | None = None,
23+
) -> presentation.Presentation:
2224
"""
2325
Return a |Presentation| object loaded from *pptx*, where *pptx* can be
24-
either a path to a ``.pptx`` file (a string) or a file-like object. If
25-
*pptx* is missing or ``None``, the built-in default presentation
26-
"template" is loaded.
26+
a path to a ``.pptx`` file (a |str| or any |os.PathLike| object such as
27+
|pathlib.Path|) or a file-like object. If *pptx* is missing or ``None``,
28+
the built-in default presentation "template" is loaded.
2729
"""
2830
if pptx is None:
2931
pptx = _default_pptx_path()
3032

33+
# ---accept os.PathLike (pathlib.Path, etc.) by coercing to str at the boundary---
34+
if hasattr(pptx, "__fspath__"):
35+
pptx = os.fspath(pptx)
36+
3137
presentation_part = Package.open(pptx).main_document_part
3238

3339
if not _is_pptx_package(presentation_part):

src/pptx/enum/dml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ class MSO_PATTERN_TYPE(BaseXmlEnum):
351351
PERCENT_30 = (5, "pct30", "30% of the foreground color.")
352352
"""30% of the foreground color."""
353353

354-
ERCENT_40 = (6, "pct40", "40% of the foreground color.")
354+
PERCENT_40 = (6, "pct40", "40% of the foreground color.")
355355
"""40% of the foreground color."""
356356

357357
PERCENT_5 = (1, "pct5", "5% of the foreground color.")

src/pptx/parts/image.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,11 +153,15 @@ def from_blob(cls, blob: bytes, filename: str | None = None) -> Image:
153153
return cls(blob, filename)
154154

155155
@classmethod
156-
def from_file(cls, image_file: str | IO[bytes]) -> Image:
156+
def from_file(cls, image_file: str | os.PathLike[str] | IO[bytes]) -> Image:
157157
"""Return a new |Image| object loaded from `image_file`.
158158
159-
`image_file` can be either a path (str) or a file-like object.
159+
`image_file` can be a path (|str| or any |os.PathLike| object such as
160+
|pathlib.Path|) or a file-like object.
160161
"""
162+
# ---accept os.PathLike (pathlib.Path etc.) by coercing to str---
163+
if hasattr(image_file, "__fspath__"):
164+
image_file = os.fspath(image_file)
161165
if isinstance(image_file, str):
162166
# treat image_file as a path
163167
with open(image_file, "rb") as f:

src/pptx/presentation.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import os
56
from typing import IO, TYPE_CHECKING, Iterable, cast
67

78
from pptx.opc.constants import RELATIONSHIP_TYPE as RT
@@ -66,11 +67,15 @@ def notes_master(self) -> NotesMaster:
6667
"""
6768
return self.part.notes_master
6869

69-
def save(self, file: str | IO[bytes]):
70+
def save(self, file: str | os.PathLike[str] | IO[bytes]):
7071
"""Writes this presentation to `file`.
7172
72-
`file` can be either a file-path or a file-like object open for writing bytes.
73+
`file` can be a file-path (|str| or any |os.PathLike| object such as
74+
|pathlib.Path|) or a file-like object open for writing bytes.
7375
"""
76+
# ---accept os.PathLike (pathlib.Path etc.) by coercing to str---
77+
if hasattr(file, "__fspath__"):
78+
file = os.fspath(file)
7479
self.part.save(file)
7580

7681
@property

src/pptx/slide.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,10 +595,21 @@ class _Background(ElementProxy):
595595
Note that the presence of this object does not by itself imply an
596596
explicitly-defined background; a slide with an inherited background still
597597
has a |_Background| object.
598+
599+
Closes upstream issue #1126: prior to this fix, ``slide.background.element``
600+
returned the parent ``<p:cSld>`` element instead of the actual ``<p:bg>``
601+
background element. Power users introspecting the XML now get the right
602+
node. The ``<p:bg>`` element is materialized on construction (matching
603+
the legacy destructive behavior of accessing ``.fill``) — and since
604+
``slide.background`` is a |lazyproperty| upstream, this only fires on
605+
first access of the slide's background, not on slide load.
598606
"""
599607

600608
def __init__(self, cSld: CT_CommonSlideData):
601-
super(_Background, self).__init__(cSld)
609+
# ---resolve to the <p:bg> element (creating if needed) so that
610+
# `_element` and `.element` point at the right node per issue #1126
611+
bg = cSld.get_or_add_bg()
612+
super(_Background, self).__init__(bg)
602613
self._cSld = cSld
603614

604615
@lazyproperty

0 commit comments

Comments
 (0)