From 14b48944c157b301fc0d29588d5dde0264140752 Mon Sep 17 00:00:00 2001 From: janosh Date: Thu, 11 Jun 2026 11:53:44 +0200 Subject: [PATCH 1/3] Adopt official ty pre-commit hook --- .pre-commit-config.yaml | 16 ++++++++-------- pdf_compressor/ilovepdf.py | 22 +++++++++++++++++++--- pdf_compressor/main.py | 6 +++--- pdf_compressor/utils.py | 13 +++++++++---- pyproject.toml | 14 +++----------- tests/test_main.py | 2 +- 6 files changed, 43 insertions(+), 30 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 304c511..c6ec66c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ default_install_hook_types: [pre-commit, commit-msg] repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.10 + rev: v0.15.16 hooks: - id: ruff-check args: [--fix] @@ -26,15 +26,15 @@ repos: args: [--check-filenames] - repo: https://github.com/crate-ci/typos - rev: v1.45.0 + rev: v1.47.2 hooks: - id: typos - - repo: local + - repo: https://github.com/astral-sh/ty-pre-commit + rev: v0.0.48 hooks: - id: ty - name: ty check - entry: ty check - language: python - types: [python, jupyter] - additional_dependencies: [ty, pytest, pandas, requests] + # --isolated: don't create/update uv.lock or the local .venv + # --all-extras: make optional deps (pytest, pandas, ...) resolvable by ty + args: [--isolated, --all-extras] + env: { UV_FROZEN: "0" } # prek hook: override global UV_FROZEN=1 which would require a uv.lock diff --git a/pdf_compressor/ilovepdf.py b/pdf_compressor/ilovepdf.py index 5351f15..8f4e4a5 100644 --- a/pdf_compressor/ilovepdf.py +++ b/pdf_compressor/ilovepdf.py @@ -8,7 +8,7 @@ import json import os import tempfile -from typing import Any, BinaryIO, Literal, TypedDict +from typing import Any, BinaryIO, Literal, TypedDict, Unpack import requests from requests import Response @@ -26,6 +26,19 @@ class ProcessResponse(TypedDict): output_extensions: list[str] +class ILovePDFOptions(TypedDict, total=False): + """Optional keyword arguments accepted by ILovePDF.""" + + debug: bool + + +class TaskOptions(ILovePDFOptions, total=False): + """Optional keyword arguments accepted by Task.""" + + verbose: bool + password: str + + class ILovePDF: """Communicates with the iLovePDF API.""" @@ -120,7 +133,7 @@ def __init__( *, verbose: bool = False, password: str = "", - **kwargs: Any, + **kwargs: Unpack[ILovePDFOptions], ) -> None: """Creates a new task object to interact with the API. @@ -326,7 +339,10 @@ class Compress(Task): """ def __init__( - self, public_key: str, compression_level: str = "recommended", **kwargs: Any + self, + public_key: str, + compression_level: str = "recommended", + **kwargs: Unpack[TaskOptions], ) -> None: """Subclass of Task for using the iLovePDF compression tool. diff --git a/pdf_compressor/main.py b/pdf_compressor/main.py index f32ec1e..c58cc6b 100644 --- a/pdf_compressor/main.py +++ b/pdf_compressor/main.py @@ -7,7 +7,7 @@ from argparse import ArgumentParser from glob import glob from importlib.metadata import version -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING from pdf_compressor.ilovepdf import Compress, ILovePDF from pdf_compressor.utils import ROOT, del_or_keep_compressed, load_dotenv @@ -178,7 +178,7 @@ def main(argv: Sequence[str] | None = None) -> int: return 0 - return compress(**vars(args)) # ty: ignore[missing-argument] + return compress(**vars(args)) def compress( @@ -195,7 +195,7 @@ def compress( on_bad_files: str = "error", write_stats_path: str = "", password: str = "", - **kwargs: Any, # noqa: ARG001 + **kwargs: object, # noqa: ARG001 ) -> int: """Compress PDFs using iLovePDF's API. diff --git a/pdf_compressor/utils.py b/pdf_compressor/utils.py index 1ed8577..7dd99da 100644 --- a/pdf_compressor/utils.py +++ b/pdf_compressor/utils.py @@ -12,6 +12,8 @@ if TYPE_CHECKING: from collections.abc import Sequence +type CompressionStats = dict[str, dict[str, object]] + ROOT = dirname(dirname(abspath(__file__))) @@ -83,7 +85,7 @@ def del_or_keep_compressed( suffix: str, min_size_reduction: int, verbose: bool = False, -) -> dict[str, dict[str, object]]: +) -> CompressionStats: """Check whether compressed PDFs are smaller than original and relocate if so. Relocates each compressed file to same directory as the original either with suffix @@ -103,8 +105,11 @@ def del_or_keep_compressed( False. Returns: - pd.DataFrame: Table with original and compressed file sizes. - """ # noqa: DOC501 + CompressionStats: Original and compressed file size stats. + + Raises: + RuntimeError: If called without in-place mode or suffix after validation. + """ if (n_files := len(pdfs)) == 1: compressed_files = [downloaded_file] else: # if multiple files were uploaded, downloaded_file is a ZIP archive @@ -115,7 +120,7 @@ def del_or_keep_compressed( total_orig_size = total_compressed_size = 0 - stats = {} + stats: CompressionStats = {} for idx, orig_path in enumerate(pdfs): if n_files > 1: diff --git a/pyproject.toml b/pyproject.toml index bb7d0ae..953c7b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["uv_build>=0.7.19"] +requires = ["uv_build>=0.11.20"] build-backend = "uv_build" [project] @@ -13,12 +13,11 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", ] -requires-python = ">=3.11" +requires-python = ">=3.12" dependencies = ["requests >= 2.25.0"] license = { file = "license" } @@ -42,22 +41,16 @@ testpaths = ["tests"] addopts = "-p no:warnings" [tool.ruff] -target-version = "py311" +target-version = "py312" [tool.ruff.lint] select = ["ALL"] ignore = [ - "ANN001", # missing type annotation for self - "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `**kwargs "C901", # Function is too complex "COM", - "CPY001", # missing copyright "EM", # Exception must not use a string literal - "FURB103", # open and write should be replaced by Path(file_path).write_bytes() - "PLC1901", # x == "" can be simplified to `not x` "PLR0912", # too many branches "PLR0913", # too many arguments - "PLR0914", # too many local variables "PLR0915", # too many statements "PTH", # Prefer pathlib over os.path "SIM105", # Use contextlib.suppress(FileNotFoundError) instead of try-except-pass @@ -66,7 +59,6 @@ ignore = [ ] pydocstyle.convention = "google" isort.split-on-trailing-comma = false -preview = true [tool.ruff.lint.per-file-ignores] "__init__.py" = ["F401"] diff --git a/tests/test_main.py b/tests/test_main.py index c888f2f..0ff84e3 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -199,7 +199,7 @@ def mock_send_request( method: str, endpoint: str, payload: dict[str, Any] | None = None, - **_kwargs: Any, + **_kwargs: object, ) -> MagicMock: if method == "post" and endpoint == "process" and payload is not None: # Check that each file in the payload has the correct password From 78a92eeadb85db6092e2ace464d4ef94d630282b Mon Sep 17 00:00:00 2001 From: janosh Date: Thu, 11 Jun 2026 11:56:05 +0200 Subject: [PATCH 2/3] Run CI tests on Python 3.12 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5e1f16..ea25501 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Set up python uses: astral-sh/setup-uv@v6 with: - python-version: 3.11 + python-version: 3.12 - name: Run tests run: uv run --with '.[test,stats]' pytest --durations 0 --cov . From 1c3f45a24a61cb8c30b1958ac023804cfd391724 Mon Sep 17 00:00:00 2001 From: janosh Date: Thu, 11 Jun 2026 12:02:01 +0200 Subject: [PATCH 3/3] Simplify ty hook typing cleanup --- .pre-commit-config.yaml | 5 ++--- pdf_compressor/ilovepdf.py | 38 ++++++++++++++++++-------------------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c6ec66c..32f844f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,6 @@ repos: rev: v0.0.48 hooks: - id: ty - # --isolated: don't create/update uv.lock or the local .venv - # --all-extras: make optional deps (pytest, pandas, ...) resolvable by ty args: [--isolated, --all-extras] - env: { UV_FROZEN: "0" } # prek hook: override global UV_FROZEN=1 which would require a uv.lock + # pre-commit hook: override global UV_FROZEN=1, which would otherwise require uv.lock. + env: { UV_FROZEN: "0" } diff --git a/pdf_compressor/ilovepdf.py b/pdf_compressor/ilovepdf.py index 8f4e4a5..ce2d44e 100644 --- a/pdf_compressor/ilovepdf.py +++ b/pdf_compressor/ilovepdf.py @@ -8,7 +8,7 @@ import json import os import tempfile -from typing import Any, BinaryIO, Literal, TypedDict, Unpack +from typing import Any, BinaryIO, Literal, TypedDict import requests from requests import Response @@ -26,19 +26,6 @@ class ProcessResponse(TypedDict): output_extensions: list[str] -class ILovePDFOptions(TypedDict, total=False): - """Optional keyword arguments accepted by ILovePDF.""" - - debug: bool - - -class TaskOptions(ILovePDFOptions, total=False): - """Optional keyword arguments accepted by Task.""" - - verbose: bool - password: str - - class ILovePDF: """Communicates with the iLovePDF API.""" @@ -133,7 +120,7 @@ def __init__( *, verbose: bool = False, password: str = "", - **kwargs: Unpack[ILovePDFOptions], + debug: bool = False, ) -> None: """Creates a new task object to interact with the API. @@ -148,9 +135,10 @@ def __init__( and processing files. Defaults to False. password (str, optional): Password to open PDFs in case they have one. Defaults to "". - **kwargs: Additional keyword arguments to pass to ILovePDF.__init__(). + debug (bool, optional): Whether to ask iLovePDF for debug responses. + Defaults to False. """ - super().__init__(public_key, **kwargs) + super().__init__(public_key, debug=debug) self.files: dict[str, str] = {} self._task_id = "" @@ -342,7 +330,10 @@ def __init__( self, public_key: str, compression_level: str = "recommended", - **kwargs: Unpack[TaskOptions], + *, + verbose: bool = False, + password: str = "", + debug: bool = False, ) -> None: """Subclass of Task for using the iLovePDF compression tool. @@ -351,12 +342,19 @@ def __init__( https://developer.ilovepdf.com/signup. compression_level (str, optional): How hard to squeeze the file size. 'extreme' noticeably degrades image quality. Defaults to 'recommended'. - **kwargs: Additional keyword arguments to pass to Task.__init__(). + verbose (bool, optional): Whether to print progress messages while uploading + and processing files. Defaults to False. + password (str, optional): Password to open PDFs in case they have one. + Defaults to "". + debug (bool, optional): Whether to ask iLovePDF for debug responses. + Defaults to False. Raises: ValueError: If the compression level is invalid. """ - super().__init__(public_key, tool="compress", **kwargs) + super().__init__( + public_key, tool="compress", verbose=verbose, password=password, debug=debug + ) if compression_level not in (valid_levels := ("low", "recommended", "extreme")): raise ValueError(