Skip to content
Open
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
13 changes: 8 additions & 5 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ echo "→ Python files changed:"
if [[ -n "$python_changed_files" ]]; then
echo " ${python_changed_files[*]}"
echo
echo " [1/3] Formatting and linting..."
echo " [1/4] Formatting and linting..."
bash "$GITHOOKS_DIR/pre-push-python/fmt-lint.sh" || FAILED=1
echo " [2/3] Stub generation..."

echo " [2/4] Stub generation..."
bash "$GITHOOKS_DIR/pre-push-python/stubs.sh" || FAILED=1
echo " [3/3] Extras validation..."

echo " [3/4] Extras validation..."
bash "$GITHOOKS_DIR/pre-push-python/extras.sh" || FAILED=1

echo " [4/4] Lockfile check..."
bash "$GITHOOKS_DIR/pre-push-python/lock.sh" || FAILED=1
echo
else
echo " (none)"
Expand Down
14 changes: 7 additions & 7 deletions .githooks/pre-push-python/extras.sh
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
# ensure generated pyproject.toml extras are up-to-date

# Clear git env vars set by the parent hook so git commands resolve the work tree normally
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX

# Store the root directory of the repository
REPO_ROOT="$(git rev-parse --show-toplevel)"
PYTHON_DIR="$REPO_ROOT/python"
PYPROJECT_FILE="$PYTHON_DIR/pyproject.toml"

# Function to check if pyproject.toml has changed
check_extras_changes() {
local target_path="$1"
local changed_files=$(git status --porcelain "$target_path" || true)
local changed_files=$(git status --porcelain "$PYPROJECT_FILE" || true)

if [ -n "$changed_files" ]; then
echo " ❌ ERROR: Generated extras are not up-to-date:"
Expand All @@ -23,13 +25,11 @@ generate_python_extras() {
echo " → Generating extras..."
cd "$PYTHON_DIR"

if [[ ! -d "$PYTHON_DIR/venv" ]]; then
echo " → Running bootstrap script..."
bash ./scripts/dev bootstrap
fi
# Idempotent: fast on a warm cache, recreates .venv on a cold checkout.
uv sync --extra dev-all --quiet

bash ./scripts/dev gen-extras
check_extras_changes "$PYPROJECT_FILE"
check_extras_changes
}

generate_python_extras
Expand Down
8 changes: 8 additions & 0 deletions .githooks/pre-push-python/fmt-lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@

set -e

# Clear git env vars set by the parent hook so git commands resolve the work tree normally
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX

# Store the root directory of the repository
REPO_ROOT="$(git rev-parse --show-toplevel)"
PYTHON_DIR="$REPO_ROOT/python"

# Change to Python directory
cd "$PYTHON_DIR"

# Ensure the dev environment is in place. Idempotent: fast on a warm
# cache, recreates .venv on a cold checkout. Without this, a fresh
# clone's first push fails with "ruff: command not found".
uv sync --extra dev-all --quiet

# Run ruff format (formatter)
echo " → Running ruff format..."
bash ./scripts/dev fmt
Expand Down
18 changes: 18 additions & 0 deletions .githooks/pre-push-python/lock.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# ensure uv.lock is up-to-date with pyproject.toml

# Clear git env vars set by the parent hook so git commands resolve the work tree normally
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX

REPO_ROOT="$(git rev-parse --show-toplevel)"
PYTHON_DIR="$REPO_ROOT/python"

echo " → Checking uv.lock..."
cd "$PYTHON_DIR"

if ! uv lock --check; then
echo " ❌ ERROR: uv.lock is out of date with pyproject.toml."
echo " Run 'uv lock' and commit the result before pushing."
exit 1
fi

echo " ✓ uv.lock is up-to-date"
9 changes: 5 additions & 4 deletions .githooks/pre-push-python/stubs.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# ensure generated python stubs are up-to-date, from sync clients

# Clear git env vars set by the parent hook so git commands resolve the work tree normally
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX

# Store the root directory of the repository
REPO_ROOT="$(git rev-parse --show-toplevel)"
PYTHON_DIR="$REPO_ROOT/python"
Expand All @@ -23,10 +26,8 @@ generate_python_stubs() {
echo " → Generating stubs..."
cd "$PYTHON_DIR"

if [[ ! -d "$PYTHON_DIR/venv" ]]; then
echo " → Running bootstrap script..."
bash ./scripts/dev bootstrap
fi
# Idempotent: fast on a warm cache, recreates .venv on a cold checkout.
uv sync --extra dev-all --quiet

bash ./scripts/dev gen-stubs
check_stub_changes "$STUBS_DIR"
Expand Down
103 changes: 65 additions & 38 deletions .github/workflows/python_ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- 'rust/crates/sift_stream_bindings/**'
- '.github/workflows/python_ci.yaml'

test-python:
static-checks:
needs: [changes]
if: |
always() &&
Expand All @@ -49,53 +49,86 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Set up Python
uses: actions/setup-python@v2
- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
python-version: "3.8"
enable-cache: true

- name: Pip install
id: install
run: |
python -m pip install --upgrade pip
pip install '.[dev-all]'
- name: Verify lockfile is up to date
run: uv lock --check

- name: Install dependencies
run: uv sync --extra dev-all

- name: Lint
run: |
ruff check
run: uv run ruff check

- name: Format
run: |
ruff format --check
run: uv run ruff format --check

- name: MyPy
run: |
mypy lib
run: uv run mypy lib

# Re-run mypy with --platform=win32 so typeshed evaluates platform-gated
# stubs (e.g. fcntl) as if we were on Windows. Catches imports that
# would only fail at runtime on Windows.
- name: MyPy (Windows platform)
run: |
mypy --platform=win32 lib
run: uv run mypy --platform=win32 lib

- name: Pyright
run: |
pyright lib
run: uv run pyright lib

- name: Check Stubs Generation
working-directory: .
run: |
bash .githooks/pre-push-python/stubs.sh
run: bash .githooks/pre-push-python/stubs.sh

- name: Check Extras Generation
working-directory: .
run: |
bash .githooks/pre-push-python/extras.sh
run: bash .githooks/pre-push-python/extras.sh

- name: Pytest Unit Tests
- name: Sync Stubs Mypy
working-directory: python/lib
run: |
pytest -m "not integration"
uv run stubtest \
--mypy-config-file ../pyproject.toml \
sift_client.resources.sync_stubs

test-python:
needs: [changes]
if: |
always() &&
(github.event_name != 'pull_request' || needs.changes.outputs.python == 'true')
runs-on: ubuntu-latest
defaults:
run:
working-directory: python
strategy:
fail-fast: false
matrix:
# Floor (3.8, per `requires-python`). This is the bug class local
# checks miss (devs run a newer Python; modern syntax slips into
# code that runs on 3.8). Ceiling testing is rarely useful in
# practice — Python's deprecation cycle is long and the project's
# stdlib usage is conservative — so it stays available locally as
# `./scripts/dev test-ceiling` rather than running on every PR.
# The full 3.8-3.14 install matrix lives in `python_build.yaml`.
python-version: ["3.8"]
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}

- name: Set up uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true

- name: Install dependencies (Python ${{ matrix.python-version }})
run: uv sync --python ${{ matrix.python-version }} --extra dev-all

- name: Pytest Unit Tests (Python ${{ matrix.python-version }})
run: uv run --python ${{ matrix.python-version }} pytest -m "not integration"

# Disabling integration tests that interact with Sift until a better solution is implemented
# - name: Pytest Integration Tests
Expand All @@ -104,26 +137,20 @@ jobs:
# SIFT_REST_URI: ${{ vars.SIFT_REST_URI }}
# SIFT_API_KEY: ${{ secrets.SIFT_API_KEY }}
# run: |
# pytest -m "integration"

- name: Sync Stubs Mypy
working-directory: python/lib
run: |
stubtest \
--mypy-config-file ../pyproject.toml \
sift_client.resources.sync_stubs
# uv run --python ${{ matrix.python-version }} --no-project --with-editable '.[dev-all]' pytest -m "integration"

python-ci-status:
if: always()
needs: [changes, test-python]
needs: [changes, static-checks, test-python]
runs-on: ubuntu-latest
steps:
- name: Check result
run: |
result="${{ needs.test-python.result }}"
if [[ "$result" == "success" || "$result" == "skipped" ]]; then
echo "python-ci passed (test-python: $result)"
static="${{ needs.static-checks.result }}"
tests="${{ needs.test-python.result }}"
if [[ ("$static" == "success" || "$static" == "skipped") && ("$tests" == "success" || "$tests" == "skipped") ]]; then
echo "python-ci passed (static-checks: $static, test-python: $tests)"
else
echo "python-ci failed (test-python: $result)"
echo "python-ci failed (static-checks: $static, test-python: $tests)"
exit 1
fi
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ ipython_config.py
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# uv
# uv.lock is committed for reproducible dev/test/CI runs.
#uv.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
Expand Down
Loading
Loading