Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/basedpyright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: basedpyright

on:
pull_request:
push:
branches:
- main
- develop
- feature-*

jobs:
basedpyright:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
packages: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
version: 0.8.22
python-version: 3.11
enable-cache: true
cache-suffix: pre-commit
cache-dependency-glob: uv.lock
- name: Restore venv cache
uses: actions/cache@v4
with:
path: |
.venv
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }}
- name: Synchronize project dependencies
run: uv sync --group dev
- name: Run basedpyright
run: uv run basedpyright
9 changes: 3 additions & 6 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,6 @@ jobs:
path: |
.venv
key: ${{ runner.os }}-uv-${{ hashFiles('uv.lock') }}
- name: Restore mypy cache
uses: actions/cache@v4
with:
path: .mypy_cache
key: mypy_cache|${{ hashFiles('pyproject.toml') }}
- name: Restore pre-commit cache
uses: actions/cache@v4
with:
Expand All @@ -45,4 +40,6 @@ jobs:
- name: Synchronize project dependencies
run: uv sync --group dev
- name: Run pre-commit checks
run: uv run pre-commit run --show-diff-on-failure --all-files --hook-stage manual
run: uv run pre-commit run --show-diff-on-failure --all-files
env:
SKIP: basedpyright
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -451,3 +451,5 @@ $RECYCLE.BIN/
# Profiling
*.prof.txt
*.speedscope.json

!pyrightconfig.json
15 changes: 3 additions & 12 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,12 @@ repos:
rev: v0.24.3
hooks:
- id: toml-sort-fix
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.18.2"
hooks:
- id: mypy
stages: [pre-push, manual]
name: mypy
additional_dependencies:
# Need pydantic to load the pydantic.mypy plugin
- pydantic>=2.12.0
- repo: https://github.com/astral-sh/uv-pre-commit
# uv version.
rev: 0.8.24
hooks:
- id: uv-lock
- repo: https://github.com/RobertCraigie/pyright-python
rev: v1.1.406
- repo: https://github.com/DetachHead/basedpyright-pre-commit-mirror
rev: 1.34.0 # or whatever the latest version is at the time
hooks:
- id: pyright
- id: basedpyright
86 changes: 3 additions & 83 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,99 +22,17 @@ dev = [
"ruff>=0.6.9",
"pytest>=8.3.3",
"pytest-cov>=5.0.0",
"mypy>=1.18.2",
"pip-audit>=2.7.3",
"pyright>=1.1.406",
"pytest-md>=0.2.0",
"pytest-emoji>=0.2.0",
"pytest-dotenv>=0.5.2",
"pytest-asyncio>=1.2.0",
"pytest-rerunfailures>=16.0.1",
"pytest-xdist>=3.8.0",
"nox>=2025.5.1",
"basedpyright>=1.34.0",
]

[tool.mypy]
python_version = "3.10"
pretty = true
plugins = [
"pydantic.mypy",
]
# Discover modules that were installed in the virtualenv
python_executable = ".venv/bin/python"
fixed_format_cache = true
ignore_missing_imports = false
follow_imports = "silent"
# Balanced strictness (not “strict = true”); catches many bugs without being overbearing
no_implicit_optional = true
check_untyped_defs = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_return_any = true
warn_no_return = true
strict_equality = true
# Defaults here stay a bit lenient; ratchet up over time if desired
disallow_untyped_defs = false
disallow_incomplete_defs = false
disallow_untyped_calls = false
disallow_any_generics = false
implicit_reexport = true
namespace_packages = true
show_error_codes = true
# Typical project structure
packages = ["pdfrest"] # if using setuptools; or rely on src/ layout
exclude = '(build|dist|\.venv|scripts|examples|docs)'

# Example: tighten src/, loosen tests/
[[tool.mypy.overrides]]
module = ["pdfrest.*"]
disallow_untyped_defs = true
disallow_incomplete_defs = true

[[tool.mypy.overrides]]
module = ["tests.*"]
disallow_untyped_defs = false
allow_redefinition = true

[tool.pyright]
venvPath = "."
venv = ".venv"
typeCheckingMode = "standard"
pythonVersion = "3.10"
reportMissingTypeStubs = true
# mypy catches this, so catch it in pyright too
reportOptionalMemberAccess = true
# Sensible signal without being punitive; tune severities as needed
reportUnusedImport = "error"
reportUnusedVariable = "error"
reportUnknownMemberType = "warning"
reportUnknownArgumentType = "warning"
reportUnknownVariableType = "warning"
reportPrivateUsage = "error"
# Keep false positives low in typical library code
useLibraryCodeForTypes = true
# Project layout
include = ["src", "tests"]
exclude = [
"**/.venv",
"build",
"dist",
"scripts",
"examples",
"docs",
]

[[tool.pyright.executionEnvironments]]
root = "src"

[[tool.pyright.executionEnvironments]]
root = "tests"
typeCheckingMode = "basic"
reportUnknownMemberType = "none"
reportUnknownArgumentType = "none"
reportUnknownVariableType = "none"
reportPrivateUsage = "none"

[tool.pytest.ini_options]
minversion = "7.4"
testpaths = ["tests"]
Expand Down Expand Up @@ -185,6 +103,8 @@ trailing_comma_inline_array = true

[tool.uv]
keyring-provider = "subprocess"
no-build = true
no-binary-package = ["pdfrest"]

[[tool.uv.index]]
name = "cit-pypi"
Expand Down
34 changes: 34 additions & 0 deletions pyrightconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"include": ["src", "tests"],
"exclude": [
"**/.venv",
"build",
"dist",
"scripts",
"examples",
"docs",
"**/node_modules",
"**/__pycache__"
],
"pythonVersion": "3.10",
"reportImplicitStringConcatenation": "none",
"reportAny": "none",
"reportExplicitAny": "none",
"reportUnannotatedClassAttribute": "none",
"executionEnvironments": [
{
"root": "src"
},
{
"root": "tests",
"reportUnknownLambdaType": "none",
"reportUnknownMemberType": "none",
"reportArgumentType": "none",
"reportUnknownArgumentType": "none",
"reportUnknownVariableType": "none",
"reportPrivateUsage": "none",
"reportUnusedCallResult": "none",
"reportPrivateLocalImportUsage": "none"
}
]
}
31 changes: 14 additions & 17 deletions src/pdfrest/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
ValidationError,
field_validator,
)
from typing_extensions import override

from .exceptions import (
PdfRestApiError,
Expand Down Expand Up @@ -258,9 +259,6 @@ def _normalize_file_type(file_value: FileTypes) -> NormalizedFileTypes:
normalized_content_type = (
str(content_type) if content_type is not None else None
)
if not isinstance(headers, Mapping):
msg = "Headers must be provided as a mapping of str keys to str values."
raise TypeError(msg)
normalized_headers = _normalize_headers(headers)
return (
normalized_filename,
Expand Down Expand Up @@ -303,9 +301,6 @@ def _parse_path_spec(spec: FilePathTypes) -> tuple[Path, str | None, Mapping[str
headers: Mapping[str, str] = {}
elif length == 3:
raw_path, content_type, headers = cast(FilePathTuple3, spec)
if not isinstance(headers, Mapping):
msg = "Headers must be provided as a mapping of str keys to str values."
raise TypeError(msg)
else:
msg = "File path tuples must contain a path plus optional content type and headers."
raise TypeError(msg)
Expand Down Expand Up @@ -441,8 +436,6 @@ class _BaseApiClient(Generic[ClientType]):
"""Shared logic between sync and async client variants."""

_config: _ClientConfig
_client: ClientType
_owns_http_client: bool

def __init__(
self,
Expand All @@ -454,7 +447,7 @@ def __init__(
max_retries: int = DEFAULT_MAX_RETRIES,
) -> None:
self._logger = LOGGER
if not isinstance(max_retries, int) or max_retries < 0:
if max_retries < 0:
msg = "max_retries must be a non-negative integer."
raise PdfRestConfigurationError(msg)
self._max_retries = max_retries
Expand Down Expand Up @@ -585,7 +578,7 @@ def _validate_pdfrest_api_key(api_key: str, url: URL) -> None:
msg = "pdfRest API keys must be 36 characters (UUID format)."
raise PdfRestConfigurationError(msg)
try:
uuid.UUID(api_key)
_ = uuid.UUID(api_key)
except ValueError:
msg = "pdfRest API keys must be valid UUID strings."
raise PdfRestConfigurationError(msg) from None
Expand Down Expand Up @@ -740,8 +733,6 @@ def _handle_response(self, response: httpx.Response) -> Any:
request = response.request
request_label = (
f"{getattr(request, 'method', 'UNKNOWN')} {getattr(request, 'url', '')}"
if request is not None
else "UNKNOWN"
)
if response.is_success:
if self._logger.isEnabledFor(logging.DEBUG):
Expand Down Expand Up @@ -811,6 +802,7 @@ class _SyncApiClient(_BaseApiClient[httpx.Client]):
"""Internal synchronous client implementation."""

_client: httpx.Client
_owns_http_client: bool

def __init__(
self,
Expand Down Expand Up @@ -1072,6 +1064,7 @@ class _AsyncApiClient(_BaseApiClient[httpx.AsyncClient]):
"""Internal asynchronous client implementation."""

_client: httpx.AsyncClient
_owns_http_client: bool

def __init__(
self,
Expand Down Expand Up @@ -1523,7 +1516,7 @@ def create_from_urls(
) -> list[PdfRestFile]:
"""Upload one or more files by providing remote URLs."""

normalized_urls = UploadURLs.model_validate({"url": urls}) # pyright: ignore[reportPrivateUsage]
normalized_urls = UploadURLs.model_validate({"url": urls})
request = self._client.prepare_request(
"POST",
"/upload",
Expand Down Expand Up @@ -1627,7 +1620,7 @@ def write_bytes(
try:
with path.open("wb") as file_handle:
for chunk in response.iter_bytes():
file_handle.write(chunk)
_ = file_handle.write(chunk)
finally:
response.close()
return path
Expand Down Expand Up @@ -1901,7 +1894,7 @@ async def write_bytes(
try:
with path.open("wb") as file_handle:
async for chunk in response.aiter_bytes():
file_handle.write(chunk)
_ = file_handle.write(chunk)
finally:
await response.aclose()
return path
Expand Down Expand Up @@ -1950,10 +1943,12 @@ def __init__(
)
self._files_client = _FilesClient(self)

@override
def __enter__(self) -> PdfRestClient:
super().__enter__()
_ = super().__enter__()
return self

@override
def __exit__(self, exc_type: Any, exc: Any, traceback: Any) -> None:
super().__exit__(exc_type, exc, traceback)

Expand Down Expand Up @@ -2379,10 +2374,12 @@ def __init__(
)
self._files_client = _AsyncFilesClient(self)

@override
async def __aenter__(self) -> AsyncPdfRestClient:
await super().__aenter__()
_ = await super().__aenter__()
return self

@override
async def __aexit__(self, exc_type: Any, exc: Any, traceback: Any) -> None:
await super().__aexit__(exc_type, exc, traceback)

Expand Down
2 changes: 2 additions & 0 deletions src/pdfrest/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any

import httpx
from typing_extensions import override

__all__ = (
"PdfRestApiError",
Expand Down Expand Up @@ -65,6 +66,7 @@ def __init__(
detail = message or f"pdfRest API returned status code {status_code}"
super().__init__(detail)

@override
def __str__(self) -> str: # pragma: no cover - mirrors Exception.__str__
base = super().__str__()
if self.response_content is None:
Expand Down
Loading