chore: pyright CI gate (public API) + Python 3.13 matrix + pyproject cleanup (Modernization Phase 3)#44
Merged
MHoroszowski merged 1 commit intomasterfrom May 8, 2026
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 3 of issue #29 (Modernization & Ergonomics): dev-tooling
Phases 1 (#39) and 2 (#43) shipped the API ergonomics and bug fixes from issue #29. This PR closes the dev-tooling sub-bullets that were tractable in a single bundle.
What it adds
pyrightstrict-mode CI gate (newtypecheckjob in.github/workflows/ci.yml). Strict-mode pyright (already configured inpyproject.toml's[tool.pyright], was just never run in CI) now runs against the public-API surface:src/pptx/__init__.py— the literal entrypoint resolved byfrom pptx import Presentationsrc/pptx/api.py—Presentation()factorysrc/pptx/presentation.py—Presentationclasssrc/pptx/util.py,src/pptx/exc.py,src/pptx/types.py— public helpersPublic-API has zero strict-mode errors after the fixes below. Strict mode on the broader codebase (chart/, oxml/simpletypes/, etc.) still surfaces ~4200 findings — those are tracked as future work and deliberately excluded from this gate.
Public-API pyright fixes:
Presentation()andPresentation.save(): replacedif hasattr(p, "__fspath__"): p = os.fspath(p)(which doesn't narrow under pyright) with explicitpkg_file: str | IO[bytes] = os.fspath(p) if isinstance(p, os.PathLike) else p. Forge audit confirmed empirically:isinstance(x, os.PathLike)is semantically identical tohasattr(x, "__fspath__")for all real-world inputs (str/bytes excluded by both; any__fspath__implementer matched by both via the ABC's virtual-subclass machinery).# pyright: ignore[reportPrivateUsage]on the deferred imports of_Sections(frompptx.sections) and_PortContext(frompptx.parts.slide) — both legitimately consumed at this seam byPresentation.sectionsandPresentation.append_from. The leading-underscore convention is documented intent ("internal"); pyright sees the rule and complains regardless. Suppression is the standard escape hatch.duplicate_notes_slide_forimport frompresentation.append_from. Thenoqa: F401was hiding an actually-unused symbol; the function is still used inslide.py, just not in this seam.Test matrix update: added Python 3.13 to the CI matrix (was 3.9-3.12).
pyproject.tomlcleanup:requires-pythonfrom>=3.8to>=3.9. Python 3.8 reached end-of-life 2024-10 and was never in the test matrix. PyPI users pinned to 3.8 will see a clean "no compatible version" message via wheel metadata (no runtime crash).Python :: 3.8classifier; addedPython :: 3.13.[tool.black]section. The fork standardized onruff formatin v1.2.0; black is no longer used anywhere in the toolchain (noblackinvocation in CI, in any Makefile, or in any developer doc).Versioning note (per Forge NIT)
Bumping
requires-pythonis technically a breaking change for any 3.8 holdouts on PyPI. Recommend bumping the minor version (e.g.1.0.x→1.1.0) on the next release tag, with aHISTORY.rstcallout. Pip's resolver handles the floor cleanly — 3.8 users get a clear "no compatible version" message rather than an install-then-crash.Skipped from issue #29 Phase 3 (deferred to separate PRs)
uvmigration — replacing the setuptools build backend, adding auv.lock, and reworking CI for uv is a significant standalone change worth its own PR.Bflake8-bugbear orRUFRuff-specific rules). Trial runs surface 49 and 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— the issue marks this optional.unittest-style test conversion — already done in this fork. Verified:grep -rln "import unittest|class.*TestCase" tests/returns empty.Reporting contract (CLAUDE.md §7)
UAT
No
.pptxUAT — Phase 3 is pure dev-tooling. The diff IS the UAT: CI yaml, pyproject changes, type-narrowing fixes inapi.py/presentation.py. Maintainer reviewed and signed off.Refs #29