Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .github/scripts/ci_changes.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ def classify(paths):
selected.update(("cpp", "python_quality"))
elif path == "tests/python/test_benchmark_protocol.py":
selected.add("python_quality")
elif path == "tests/test_pyright.py":
selected.update(("python_quality", "python_build"))
elif path.startswith("tests/python/"):
selected.update(("python_build", "wheels"))
if path == "tests/python/conftest.py":
Expand Down
44 changes: 43 additions & 1 deletion .github/scripts/test_ci_workflow_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def test_installed_typing_checks_are_required(self):
build = workflow.split(" python-build:\n", 1)[1].split(
" python-result:\n", 1
)[0]
self.assertIn('pip install ".[test]"', build)
self.assertIn('pip install ".[test,typing]"', build)
self.assertIn("python -m pytest tests/python/test_editor_support.py", build)
self.assertNotIn("continue-on-error", build)
self.assertIn(
Expand Down Expand Up @@ -73,6 +73,48 @@ def test_installed_typing_checks_are_required(self):
)
self.assertEqual(checked.returncode == 0, matrix_result == "success")

def test_pyright_checks_are_required_and_scoped(self):
config = tomllib.loads((ROOT / "pyproject.toml").read_text())
self.assertEqual(
set(config["tool"]["pyright"]["include"]),
{
"tests/python/typing/public_api.py",
"tests/python/test_editor_support.py",
"tests/test_pyright.py",
},
)
self.assertEqual(config["tool"]["pyright"]["typeCheckingMode"], "standard")
self.assertTrue(
any(
dependency.startswith("pyright==")
for dependency in config["project"]["optional-dependencies"]["typing"]
)
)
self.assertFalse(
any(
dependency.startswith("pyright")
for dependency in config["project"]["dependencies"]
)
)
self.assertFalse(
any(
dependency.startswith("pyright")
for dependency in config["tool"]["cibuildwheel"]["test-requires"]
)
)
workflow = (ROOT / ".github/workflows/python.yml").read_text()
build = workflow.split(" python-build:\n", 1)[1].split(
" python-result:\n", 1
)[0]
self.assertIn("actions/setup-node@v7", build)
self.assertIn('node-version: "24"', build)
self.assertIn('pip install ".[test,typing]"', build)
self.assertIn("python -m pytest tests/test_pyright.py", build)
self.assertNotIn("continue-on-error", build)
self.assertEqual(
classify(["tests/test_pyright.py"]), {"python_quality", "python_build"}
)

def test_tidy_rejects_empty_scope_and_propagates_analysis_failure(self):
with tempfile.TemporaryDirectory() as directory:
work = Path(directory)
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,28 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: pip

- uses: actions/setup-node@v7
with:
node-version: "24"
package-manager-cache: false

- name: Verify runner CMake
run: cmake --version

- name: Upgrade pip
run: pip install --upgrade pip

- name: Build and install wheel
run: pip install ".[test]" -v
run: pip install ".[test,typing]" -v

- name: Check installed Python types and editor metadata
env:
OMP_NUM_THREADS: "2"
run: python -m pytest tests/python/test_editor_support.py --tb=short -q

- name: Check Pyright public API and test code
run: python -m pytest tests/test_pyright.py --tb=short -q

# Repaired-wheel jobs run the full Python suite.
- name: Smoke-test source installation
run: >-
Expand Down
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Paper = "https://doi.org/10.1145/3725413"

[project.optional-dependencies]
test = ["pytest>=7", "numpy", "mypy==2.4.0"]
typing = ["pyright==1.1.414"]

[tool.scikit-build]
# Disable directory auto-discovery so we can map it via CMake's install target
Expand Down Expand Up @@ -76,3 +77,11 @@ target-version = "py311"

[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I"]

[tool.pyright]
include = [
"tests/python/typing/public_api.py",
"tests/python/test_editor_support.py",
"tests/test_pyright.py",
]
typeCheckingMode = "standard"
2 changes: 1 addition & 1 deletion scripts/check-python.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ if [[ "$($ruff --version)" != "ruff 0.16.1" ]]; then
fi

cd "$repo_root"
paths=(python_bindings sample/python tests/python benchmarks)
paths=(python_bindings sample/python tests/python tests/test_pyright.py benchmarks)
"$ruff" format --check "${paths[@]}"
"$ruff" check "${paths[@]}"
10 changes: 10 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,13 @@ python -m pytest tests/python/test_editor_support.py -q
The suite checks installed declarations, signatures, documentation, examples, and
valid/invalid calls with strict mypy. CI requires it; missing mypy fails the test.
Keep the mypy pins in the `test` extra and wheel test requirements synchronized.

CI also checks the public type examples and test code with Pyright. With Node 24
and the `test,typing` extras installed in the same environment, run:

```bash
python -m pytest tests/test_pyright.py -q
```

This requires clean diagnostics and verifies that every intentional invalid call
is rejected. Pyright is not required for the platform wheel test suite.
63 changes: 63 additions & 0 deletions tests/test_pyright.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Explicit Pyright checks; run with the test and typing extras plus Node 24."""

import json
import re
import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "tests/python/typing/public_api.py"


def run_pyright(*paths: Path) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[
sys.executable,
"-m",
"pyright",
"--project",
str(ROOT / "pyproject.toml"),
"--pythonpath",
sys.executable,
"--warnings",
"--outputjson",
*(str(path) for path in paths),
],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)


def test_pyright_public_api_and_tests():
result = run_pyright()
assert result.returncode == 0, result.stdout + result.stderr
report = json.loads(result.stdout)
assert report["summary"]["filesAnalyzed"] == 3, result.stdout
assert report["summary"]["errorCount"] == 0, result.stdout
assert report["summary"]["warningCount"] == 0, result.stdout


def test_pyright_rejects_invalid_calls(tmp_path: Path):
source = FIXTURE.read_text()
expected_lines = {
number
for number, line in enumerate(source.splitlines(), start=1)
if "# type: ignore[" in line
}
assert expected_lines
invalid = tmp_path / "invalid.py"
invalid.write_text(re.sub(r" # type: ignore\[[^]]+\]", "", source))
result = run_pyright(invalid)
assert result.returncode == 1, result.stdout + result.stderr
report = json.loads(result.stdout)
assert report["summary"]["filesAnalyzed"] == 1, result.stdout
assert report["summary"]["warningCount"] == 0, result.stdout
actual_lines = set()
for diagnostic in report["generalDiagnostics"]:
assert diagnostic["severity"] == "error", diagnostic
assert Path(diagnostic["file"]).resolve() == invalid.resolve(), diagnostic
actual_lines.add(diagnostic["range"]["start"]["line"] + 1)
assert actual_lines == expected_lines, result.stdout
Loading