Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions features/modernization-phase1.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
Feature: Modernization Phase 1 — PathLike, PERCENT_40, slide.background.element
In order to use python-pptx with modern Python idioms
As a developer using python-pptx
I need pathlib.Path support, the PERCENT_40 enum typo fixed,
and slide.background.element to return the actual <p:bg> element


Scenario: Open a presentation from a pathlib.Path
Given a freshly-saved presentation at a Path
When I call Presentation(path) with the Path
Then I get a presentation back


Scenario: Save a presentation to a pathlib.Path
Given a fresh presentation
When I save it to a Path
Then a non-empty .pptx file exists at that Path


Scenario: PERCENT_40 enum is exposed with the correct name
Then MSO_PATTERN_TYPE.PERCENT_40 exists with xml_value pct40
And the broken name ERCENT_40 does not exist


Scenario: slide.background.element returns the <p:bg> element
Given a fresh slide on a fresh presentation
Then slide.background.element local-name is bg
82 changes: 82 additions & 0 deletions features/steps/modernization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Gherkin step implementations for Modernization Phase 1 (issue #29)."""

from __future__ import annotations

from pathlib import Path

from behave import given, then, when
from environment import scratch_dir # noqa: E402

from pptx import Presentation
from pptx.enum.dml import MSO_PATTERN_TYPE


# given ===================================================


@given("a freshly-saved presentation at a Path")
def given_freshly_saved_presentation(context):
target = Path(scratch_dir) / "modernization_seed.pptx"
target.parent.mkdir(parents=True, exist_ok=True)
Presentation().save(target)
context.path = target


@given("a fresh presentation")
def given_fresh_presentation(context):
context.prs = Presentation()


@given("a fresh slide on a fresh presentation")
def given_fresh_slide(context):
prs = Presentation()
context.slide = prs.slides.add_slide(prs.slide_layouts[6])


# when ====================================================


@when("I call Presentation(path) with the Path")
def when_call_presentation_with_path(context):
context.prs = Presentation(context.path)


@when("I save it to a Path")
def when_save_to_path(context):
target = Path(scratch_dir) / "modernization_out.pptx"
target.parent.mkdir(parents=True, exist_ok=True)
context.prs.save(target)
context.saved_path = target


# then ====================================================


@then("I get a presentation back")
def then_presentation_back(context):
assert context.prs is not None


@then("a non-empty .pptx file exists at that Path")
def then_non_empty_file_exists(context):
assert context.saved_path.exists()
assert context.saved_path.stat().st_size > 0


@then("MSO_PATTERN_TYPE.PERCENT_40 exists with xml_value pct40")
def then_percent_40_correct(context):
assert MSO_PATTERN_TYPE.PERCENT_40.xml_value == "pct40"


@then("the broken name ERCENT_40 does not exist")
def then_ercent_40_absent(context):
assert hasattr(MSO_PATTERN_TYPE, "ERCENT_40") is False


@then("slide.background.element local-name is bg")
def then_background_element_is_bg(context):
from lxml import etree

bg_elm = context.slide.background.element
local = etree.QName(bg_elm.tag).localname
assert local == "bg", f"expected 'bg', got '{local}'"
14 changes: 10 additions & 4 deletions src/pptx/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,22 @@
from pptx.parts.presentation import PresentationPart


def Presentation(pptx: str | IO[bytes] | None = None) -> presentation.Presentation:
def Presentation(
pptx: str | os.PathLike[str] | IO[bytes] | None = None,
) -> presentation.Presentation:
"""
Return a |Presentation| object loaded from *pptx*, where *pptx* can be
either a path to a ``.pptx`` file (a string) or a file-like object. If
*pptx* is missing or ``None``, the built-in default presentation
"template" is loaded.
a path to a ``.pptx`` file (a |str| or any |os.PathLike| object such as
|pathlib.Path|) or a file-like object. If *pptx* is missing or ``None``,
the built-in default presentation "template" is loaded.
"""
if pptx is None:
pptx = _default_pptx_path()

# ---accept os.PathLike (pathlib.Path, etc.) by coercing to str at the boundary---
if hasattr(pptx, "__fspath__"):
pptx = os.fspath(pptx)

presentation_part = Package.open(pptx).main_document_part

if not _is_pptx_package(presentation_part):
Expand Down
2 changes: 1 addition & 1 deletion src/pptx/enum/dml.py
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ class MSO_PATTERN_TYPE(BaseXmlEnum):
PERCENT_30 = (5, "pct30", "30% of the foreground color.")
"""30% of the foreground color."""

ERCENT_40 = (6, "pct40", "40% of the foreground color.")
PERCENT_40 = (6, "pct40", "40% of the foreground color.")
"""40% of the foreground color."""

PERCENT_5 = (1, "pct5", "5% of the foreground color.")
Expand Down
8 changes: 6 additions & 2 deletions src/pptx/parts/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,15 @@ def from_blob(cls, blob: bytes, filename: str | None = None) -> Image:
return cls(blob, filename)

@classmethod
def from_file(cls, image_file: str | IO[bytes]) -> Image:
def from_file(cls, image_file: str | os.PathLike[str] | IO[bytes]) -> Image:
"""Return a new |Image| object loaded from `image_file`.

`image_file` can be either a path (str) or a file-like object.
`image_file` can be a path (|str| or any |os.PathLike| object such as
|pathlib.Path|) or a file-like object.
"""
# ---accept os.PathLike (pathlib.Path etc.) by coercing to str---
if hasattr(image_file, "__fspath__"):
image_file = os.fspath(image_file)
if isinstance(image_file, str):
# treat image_file as a path
with open(image_file, "rb") as f:
Expand Down
9 changes: 7 additions & 2 deletions src/pptx/presentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import os
from typing import IO, TYPE_CHECKING, Iterable, cast

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

def save(self, file: str | IO[bytes]):
def save(self, file: str | os.PathLike[str] | IO[bytes]):
"""Writes this presentation to `file`.

`file` can be either a file-path or a file-like object open for writing bytes.
`file` can be a file-path (|str| or any |os.PathLike| object such as
|pathlib.Path|) or a file-like object open for writing bytes.
"""
# ---accept os.PathLike (pathlib.Path etc.) by coercing to str---
if hasattr(file, "__fspath__"):
file = os.fspath(file)
self.part.save(file)

@property
Expand Down
13 changes: 12 additions & 1 deletion src/pptx/slide.py
Original file line number Diff line number Diff line change
Expand Up @@ -595,10 +595,21 @@ class _Background(ElementProxy):
Note that the presence of this object does not by itself imply an
explicitly-defined background; a slide with an inherited background still
has a |_Background| object.

Closes upstream issue #1126: prior to this fix, ``slide.background.element``
returned the parent ``<p:cSld>`` element instead of the actual ``<p:bg>``
background element. Power users introspecting the XML now get the right
node. The ``<p:bg>`` element is materialized on construction (matching
the legacy destructive behavior of accessing ``.fill``) — and since
``slide.background`` is a |lazyproperty| upstream, this only fires on
first access of the slide's background, not on slide load.
"""

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

@lazyproperty
Expand Down
Loading
Loading