Skip to content
Draft
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
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
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: 66 additions & 37 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,88 @@ 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: 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

- name: Sync Stubs Mypy
working-directory: python/lib
run: |
bash .githooks/pre-push-python/extras.sh
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: Pytest Unit Tests
- name: Pytest Unit Tests (Python ${{ matrix.python-version }})
# `--no-project --with-editable '.[dev-all]'` resolves the project +
# dev-all extra ad hoc under the matrix Python. Editable install so
# pytest collects from the working tree.
run: |
pytest -m "not integration"
uv run \
--python ${{ matrix.python-version }} \
--no-project \
--with-editable '.[dev-all]' \
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 +139,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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,12 @@ ipython_config.py
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock

# uv
# This project's direct dev deps are pinned exactly in pyproject.toml, so
# uv.lock would add transitive-dep pinning + supply-chain hashes without
# much marginal value for a library. Ignored to avoid PR churn.
uv.lock

# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
Expand Down
20 changes: 20 additions & 0 deletions python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,18 @@ all = ["openssl", "sift-stream", "file-imports", "data-review"]
dev-all = ["development", "all", "build"]
docs-build = ["dev-all", "docs"] # Note python 3.9+

[tool.uv]
# Restrict uv's resolver to Python 3.10+ for the dev environment. The
# `docs` extra requires `griffe-pydantic==1.3.1` (Python 3.10+), and uv
# resolves against the full `requires-python` range plus all extras by
# default. Without this, `uv run` / `uv sync` fail to find a solution
# that covers 3.8/3.9 with `docs` installed. The library still supports
# 3.8 at runtime (per `requires-python`); the dev env just doesn't need
# to. The `test-floor` and `test-ceiling` subcommands sidestep this via
# `uv run --no-project --python <ver>` which resolves ad hoc against
# just `dev-all` (no docs).
environments = ["python_version >= '3.10'"]

[tool.mypy]
python_version = "3.10" # Use the Python 3.10 type checker since we are using eval-type-backport and `from __future__ import annotations`

Expand Down Expand Up @@ -332,8 +344,16 @@ module = "nptdms"
ignore_missing_imports = true
ignore_errors = true

# alive-progress 3.3.0 ships py.typed but its `alive_it` signature is too
# tight (declares `Collection[Never]`), which breaks unpacking generators.
[[tool.mypy.overrides]]
module = "alive_progress"
follow_imports = "skip"
ignore_errors = true

[tool.setuptools.packages.find]
where = ["lib"]
exclude = ["sift_client._tests", "sift_client._tests.*"]

[tool.setuptools.package-data]
sift_grafana = ["py.typed"]
Expand Down
Loading
Loading