diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2492df6d5..7f56c7b10 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,13 +36,37 @@ jobs: - name: ruff format check run: ruff format --check src tests + typecheck: + name: Typecheck (pyright strict — public API) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install package and pyright + run: | + python -m pip install --upgrade pip pyright + python -m pip install -e . + # ---public-API surface: pptx/__init__.py is the literal entrypoint + # ---resolved by `from pptx import Presentation`, plus the modules + # ---it pulls in (api, presentation, util, exc, types). + # ---Strict-mode pyright on the broader codebase still surfaces several + # ---thousand findings (reportUnknownMemberType-heavy in chart and + # ---oxml.simpletypes); those are tracked as future work, not this gate. + - name: pyright (public-API entry points) + run: | + pyright src/pptx/__init__.py src/pptx/api.py src/pptx/presentation.py \ + src/pptx/util.py src/pptx/exc.py src/pptx/types.py + test: name: Test (Python ${{ matrix.python-version }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index 2da7a2681..b19d84595 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,11 +14,11 @@ classifiers = [ "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Office/Business :: Office Suites", "Topic :: Software Development :: Libraries", ] @@ -33,7 +33,7 @@ dynamic = ["version"] keywords = ["powerpoint", "ppt", "pptx", "openxml", "office"] license = { text = "MIT" } readme = "README.rst" -requires-python = ">=3.8" +requires-python = ">=3.9" [project.urls] Changelog = "https://github.com/MHoroszowski/python-pptx/blob/master/HISTORY.rst" @@ -42,9 +42,6 @@ Homepage = "https://github.com/MHoroszowski/python-pptx" Repository = "https://github.com/MHoroszowski/python-pptx" Upstream = "https://github.com/scanny/python-pptx" -[tool.black] -line-length = 100 - [tool.pyright] exclude = [ "**/__pycache__", diff --git a/src/pptx/api.py b/src/pptx/api.py index daa3e8410..69269ea33 100644 --- a/src/pptx/api.py +++ b/src/pptx/api.py @@ -27,18 +27,21 @@ def Presentation( |pathlib.Path|) or a file-like object. If *pptx* is missing or ``None``, the built-in default presentation "template" is loaded. """ + # ---accept os.PathLike (pathlib.Path, etc.) by coercing to str at the + # ---boundary; collapse the union to (str | IO[bytes]) for downstream--- + pkg_file: str | IO[bytes] if pptx is None: - pptx = _default_pptx_path() + pkg_file = _default_pptx_path() + elif isinstance(pptx, os.PathLike): + pkg_file = os.fspath(pptx) + else: + pkg_file = pptx - # ---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 + presentation_part = Package.open(pkg_file).main_document_part if not _is_pptx_package(presentation_part): tmpl = "file '%s' is not a PowerPoint file, content type is '%s'" - raise ValueError(tmpl % (pptx, presentation_part.content_type)) + raise ValueError(tmpl % (pkg_file, presentation_part.content_type)) return presentation_part.presentation diff --git a/src/pptx/presentation.py b/src/pptx/presentation.py index 426a50fd2..44db95f9e 100644 --- a/src/pptx/presentation.py +++ b/src/pptx/presentation.py @@ -73,10 +73,10 @@ def save(self, file: str | os.PathLike[str] | IO[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) + # ---accept os.PathLike (pathlib.Path etc.) by coercing to str at the + # ---boundary; collapse the union to (str | IO[bytes]) for downstream--- + pkg_file: str | IO[bytes] = os.fspath(file) if isinstance(file, os.PathLike) else file + self.part.save(pkg_file) @property def slide_height(self) -> Length | None: @@ -157,7 +157,7 @@ def sections(self): elements into existence; the wrapping XML is created on the first ``add_section`` call. """ - from pptx.sections import _Sections + from pptx.sections import _Sections # pyright: ignore[reportPrivateUsage] return _Sections(self) @@ -202,7 +202,7 @@ def append_from( Raises |IndexError| if any value in `slide_indexes` is out of range for ``other_pres.slides``. """ - from pptx.parts.slide import _PortContext, duplicate_notes_slide_for # noqa: F401 + from pptx.parts.slide import _PortContext # pyright: ignore[reportPrivateUsage] if slide_indexes is None: source_slides = list(other_pres.slides)