Skip to content

chore: pyright CI gate (public API) + Python 3.13 matrix + pyproject cleanup (Modernization Phase 3)#44

Merged
MHoroszowski merged 1 commit intomasterfrom
feature/modernization-phase3
May 8, 2026
Merged

chore: pyright CI gate (public API) + Python 3.13 matrix + pyproject cleanup (Modernization Phase 3)#44
MHoroszowski merged 1 commit intomasterfrom
feature/modernization-phase3

Conversation

@MHoroszowski
Copy link
Copy Markdown
Owner

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

pyright strict-mode CI gate (new typecheck job in .github/workflows/ci.yml). Strict-mode pyright (already configured in pyproject.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 by from pptx import Presentation
  • src/pptx/api.pyPresentation() factory
  • src/pptx/presentation.pyPresentation class
  • src/pptx/util.py, src/pptx/exc.py, src/pptx/types.py — public helpers

Public-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() and Presentation.save(): 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. Forge audit confirmed empirically: isinstance(x, os.PathLike) is semantically identical to hasattr(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).
  • Added line-local # 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.
  • Removed unused duplicate_notes_slide_for import from presentation.append_from. The noqa: F401 was hiding an actually-unused symbol; the function is still used in slide.py, just not in this seam.

Test matrix update: added Python 3.13 to the CI matrix (was 3.9-3.12).

pyproject.toml cleanup:

  • Bumped requires-python from >=3.8 to >=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).
  • 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).

Versioning note (per Forge NIT)

Bumping requires-python is technically a breaking change for any 3.8 holdouts on PyPI. Recommend bumping the minor version (e.g. 1.0.x1.1.0) on the next release tag, with a HISTORY.rst callout. 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)

  • uv migration — replacing the setuptools build backend, adding a uv.lock, and reworking CI for uv is a significant standalone change worth its own PR.
  • Ruff selection strengthening (e.g. adding B flake8-bugbear or RUF Ruff-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)

$ python3 -m pytest tests/ -q | tail -3
........................................................................ [ 97%]
........................................................................ [100%]
3456 passed in 4.99s

$ python3 -m ruff check src tests | tail -3
All checks passed!

$ python3 -m behave features/ --no-color 2>&1 | tail -3
1041 scenarios passed, 0 failed, 0 skipped
3134 steps passed, 0 failed, 0 skipped
Took 0min 1.653s

$ pyright src/pptx/{__init__,api,presentation,util,exc,types}.py
0 errors, 0 warnings, 0 informations

UAT

No .pptx UAT — Phase 3 is pure dev-tooling. The diff IS the UAT: CI yaml, pyproject changes, type-narrowing fixes in api.py / presentation.py. Maintainer reviewed and signed off.

Refs #29

…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
@MHoroszowski MHoroszowski merged commit a6cb787 into master May 8, 2026
14 checks passed
@MHoroszowski MHoroszowski deleted the feature/modernization-phase3 branch May 8, 2026 18:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant