Skip to content

Commit 94965d7

Browse files
Matthew HoroszowskiMatthew Horoszowski
authored andcommitted
chore: pyright CI gate (public API) + Python 3.13 matrix + pyproject cleanup (Modernization Phase 3)
Issue: #29 (Phase 3 — dev-tooling) Phases 1 (#39) and 2 (#43) shipped the API ergonomics and bug fixes from issue #29. This PR covers the dev-tooling-modernization sub-bullets that were tractable in a single PR — `pyright` strict-mode CI gate on the public API, Python 3.13 in the test matrix, and cleanup of stale `pyproject.toml` config. CI changes - New `typecheck` job in `.github/workflows/ci.yml` runs `pyright` (installed alongside the editable package) on the public-API surface: `src/pptx/{__init__,api,presentation,util,exc,types}.py`. `pptx/__init__.py` is included because it's the literal entrypoint resolved by `from pptx import Presentation`. Pyright runs in strict mode (already configured in `pyproject.toml`'s `[tool.pyright]` section) and the gate fails on any error, satisfying issue #29's acceptance criterion of "zero errors on the public API". - Test matrix extended to include Python 3.13 (was 3.9 through 3.12). Public-API pyright fixes (zero errors after these) - `src/pptx/api.py` `Presentation()`: replaced `if hasattr(p, "__fspath__"): p = os.fspath(p)` (which doesn't narrow under pyright) with explicit `pkg_file: str | IO[bytes] = os.fspath(p) if isinstance(p, os.PathLike) else p`. Identical runtime behavior; fully narrowed for the type checker. - `src/pptx/presentation.py` `save()`: same shape change for the same reason. - `src/pptx/presentation.py`: added `# pyright: ignore[reportPrivateUsage]` on the deferred imports of `_Sections` (from `pptx.sections`) and `_PortContext` (from `pptx.parts.slide`) — both legitimately consumed at this seam by `Presentation.sections` and `Presentation.append_from`. The leading-underscore convention is documented intent ("internal"); pyright sees the rule and complains regardless. Suppression is the standard escape hatch. - `src/pptx/presentation.py`: dropped unused `duplicate_notes_slide_for` import from `append_from`. The `noqa: F401` was hiding an actually-unused symbol. `pyproject.toml` cleanup - Bumped `requires-python` from `>=3.8` to `>=3.9`. Python 3.8 reached end-of-life in 2024-10 and was never in the test matrix; this aligns the floor with what is actually exercised. PyPI users pinned to 3.8 will see a clean "no compatible version" message via wheel metadata (no runtime crash). Per Forge's NIT, the next release tag should bump the minor version (e.g. `1.0.x` → `1.1.0`) and call out the floor change in `HISTORY.rst`. - Dropped the `Python :: 3.8` classifier; added `Python :: 3.13`. - Removed the dead `[tool.black]` section. The fork standardized on `ruff format` in v1.2.0; black is no longer used anywhere in the toolchain (no `black` invocation in CI, in any Makefile, or in any developer doc). Skipped from issue #29 Phase 3 (deferred to separate PRs) - `uv` migration. Replacing the setuptools build backend, adding a uv lockfile, and reworking CI for uv is a significant standalone change worth its own PR. - Ruff selection strengthening (adding e.g. `B` flake8-bugbear or `RUF` Ruff-specific rules). Trial runs surface 49 + 82 findings respectively — most are real but each requires manual resolution. Defer to a follow-up PR that pairs the rule addition with the cleanup commit. - `pytest-syrupy` snapshot tests for XML fixtures (issue marks this optional). - `unittest`-style test conversion: already done in this fork. Verified by `grep -rln "import unittest|class.*TestCase" tests/` — empty result. Tests - Full pytest: `3456 passed in 4.99s` (no regressions; +0 vs Phase 2). - Full behave: `1041 scenarios passed, 0 failed` (no regressions). - Ruff: `ruff check src tests` → All checks passed; `ruff format --check` → no diff. - Pyright on public API: `0 errors, 0 warnings, 0 informations`. Refs #29
1 parent 5c26976 commit 94965d7

4 files changed

Lines changed: 43 additions & 19 deletions

File tree

.github/workflows/ci.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,37 @@ jobs:
3636
- name: ruff format check
3737
run: ruff format --check src tests
3838

39+
typecheck:
40+
name: Typecheck (pyright strict — public API)
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@v4
44+
- name: Set up Python
45+
uses: actions/setup-python@v5
46+
with:
47+
python-version: "3.12"
48+
- name: Install package and pyright
49+
run: |
50+
python -m pip install --upgrade pip pyright
51+
python -m pip install -e .
52+
# ---public-API surface: pptx/__init__.py is the literal entrypoint
53+
# ---resolved by `from pptx import Presentation`, plus the modules
54+
# ---it pulls in (api, presentation, util, exc, types).
55+
# ---Strict-mode pyright on the broader codebase still surfaces several
56+
# ---thousand findings (reportUnknownMemberType-heavy in chart and
57+
# ---oxml.simpletypes); those are tracked as future work, not this gate.
58+
- name: pyright (public-API entry points)
59+
run: |
60+
pyright src/pptx/__init__.py src/pptx/api.py src/pptx/presentation.py \
61+
src/pptx/util.py src/pptx/exc.py src/pptx/types.py
62+
3963
test:
4064
name: Test (Python ${{ matrix.python-version }})
4165
runs-on: ubuntu-latest
4266
strategy:
4367
fail-fast: false
4468
matrix:
45-
python-version: ["3.9", "3.10", "3.11", "3.12"]
69+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
4670
steps:
4771
- uses: actions/checkout@v4
4872
- name: Set up Python ${{ matrix.python-version }}

pyproject.toml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ classifiers = [
1414
"Operating System :: OS Independent",
1515
"Programming Language :: Python",
1616
"Programming Language :: Python :: 3",
17-
"Programming Language :: Python :: 3.8",
1817
"Programming Language :: Python :: 3.9",
1918
"Programming Language :: Python :: 3.10",
2019
"Programming Language :: Python :: 3.11",
2120
"Programming Language :: Python :: 3.12",
21+
"Programming Language :: Python :: 3.13",
2222
"Topic :: Office/Business :: Office Suites",
2323
"Topic :: Software Development :: Libraries",
2424
]
@@ -33,7 +33,7 @@ dynamic = ["version"]
3333
keywords = ["powerpoint", "ppt", "pptx", "openxml", "office"]
3434
license = { text = "MIT" }
3535
readme = "README.rst"
36-
requires-python = ">=3.8"
36+
requires-python = ">=3.9"
3737

3838
[project.urls]
3939
Changelog = "https://github.com/MHoroszowski/python-pptx/blob/master/HISTORY.rst"
@@ -42,9 +42,6 @@ Homepage = "https://github.com/MHoroszowski/python-pptx"
4242
Repository = "https://github.com/MHoroszowski/python-pptx"
4343
Upstream = "https://github.com/scanny/python-pptx"
4444

45-
[tool.black]
46-
line-length = 100
47-
4845
[tool.pyright]
4946
exclude = [
5047
"**/__pycache__",

src/pptx/api.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,21 @@ def Presentation(
2727
|pathlib.Path|) or a file-like object. If *pptx* is missing or ``None``,
2828
the built-in default presentation "template" is loaded.
2929
"""
30+
# ---accept os.PathLike (pathlib.Path, etc.) by coercing to str at the
31+
# ---boundary; collapse the union to (str | IO[bytes]) for downstream---
32+
pkg_file: str | IO[bytes]
3033
if pptx is None:
31-
pptx = _default_pptx_path()
34+
pkg_file = _default_pptx_path()
35+
elif isinstance(pptx, os.PathLike):
36+
pkg_file = os.fspath(pptx)
37+
else:
38+
pkg_file = pptx
3239

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-
37-
presentation_part = Package.open(pptx).main_document_part
40+
presentation_part = Package.open(pkg_file).main_document_part
3841

3942
if not _is_pptx_package(presentation_part):
4043
tmpl = "file '%s' is not a PowerPoint file, content type is '%s'"
41-
raise ValueError(tmpl % (pptx, presentation_part.content_type))
44+
raise ValueError(tmpl % (pkg_file, presentation_part.content_type))
4245

4346
return presentation_part.presentation
4447

src/pptx/presentation.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ def save(self, file: str | os.PathLike[str] | IO[bytes]):
7373
`file` can be a file-path (|str| or any |os.PathLike| object such as
7474
|pathlib.Path|) or a file-like object open for writing bytes.
7575
"""
76-
# ---accept os.PathLike (pathlib.Path etc.) by coercing to str---
77-
if hasattr(file, "__fspath__"):
78-
file = os.fspath(file)
79-
self.part.save(file)
76+
# ---accept os.PathLike (pathlib.Path etc.) by coercing to str at the
77+
# ---boundary; collapse the union to (str | IO[bytes]) for downstream---
78+
pkg_file: str | IO[bytes] = os.fspath(file) if isinstance(file, os.PathLike) else file
79+
self.part.save(pkg_file)
8080

8181
@property
8282
def slide_height(self) -> Length | None:
@@ -157,7 +157,7 @@ def sections(self):
157157
elements into existence; the wrapping XML is created on the first
158158
``add_section`` call.
159159
"""
160-
from pptx.sections import _Sections
160+
from pptx.sections import _Sections # pyright: ignore[reportPrivateUsage]
161161

162162
return _Sections(self)
163163

@@ -202,7 +202,7 @@ def append_from(
202202
Raises |IndexError| if any value in `slide_indexes` is out of
203203
range for ``other_pres.slides``.
204204
"""
205-
from pptx.parts.slide import _PortContext, duplicate_notes_slide_for # noqa: F401
205+
from pptx.parts.slide import _PortContext # pyright: ignore[reportPrivateUsage]
206206

207207
if slide_indexes is None:
208208
source_slides = list(other_pres.slides)

0 commit comments

Comments
 (0)