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
43 changes: 43 additions & 0 deletions .github/actions/setup-pysits/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Set up pysits
description: R with the sits stack, and uv-managed Python

inputs:
python-version:
description: Python version to run pysits on
required: true

runs:
using: composite

steps:
- uses: r-lib/actions/setup-r@v2
with:
Ncpus: '4'

# R is installed under `/opt/R`, which the dynamic loader does not search.
# `rpy2` links against `libR.so` at import, so without this it falls back
# to ABI mode and then fails to load R's own base packages
- name: Put libR on the loader path
shell: bash
run: |
echo "LD_LIBRARY_PATH=$(R RHOME)/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" >> "$GITHUB_ENV"

- uses: r-lib/actions/setup-r-dependencies@v2
with:
packages: |
github::e-sensing/sits@dev
any::arrow
any::earthdatalogin

# The `torch` R package ships without libtorch, and pysits disables the
# implicit download (`TORCH_INSTALL=0`), so without this operation
# the deep learning models would fail only once a test train one
- name: Install libtorch
shell: bash
run: Rscript -e 'torch::install_torch()'

# After R: `rpy2` has no Linux wheel and is compiled against it
- uses: astral-sh/setup-uv@v9.0.0
with:
python-version: ${{ inputs.python-version }}
enable-cache: true
30 changes: 10 additions & 20 deletions .github/workflows/ruff.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,27 @@ on:
pull_request:
branches: [main]

# Run every Saturday at 3:00 AM
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '0 3 * * 6'

workflow_dispatch:
inputs:
reason:
required: false
description: 'Reason'
default: 'Manual trigger'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

jobs:
lint:
name: Run ruff
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up R
uses: r-lib/actions/setup-r@v2

- name: Set up Python (Dev version)
uses: actions/setup-python@v4
- uses: actions/checkout@v7
- uses: astral-sh/ruff-action@v4.1.0
with:
python-version: '3.11-dev'

- name: Install dependencies
run: pip install .[dev]

- name: Check
run: ruff check .
version-file: uv.lock
122 changes: 53 additions & 69 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
@@ -1,96 +1,80 @@
name: Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

# Run every Saturday at 3:00 AM
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '0 3 * * 6'

workflow_dispatch:
inputs:
reason:
required: false
description: 'Reason'
default: 'Manual trigger'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

env:
# pak resolves `sits` from GitHub; without a token it shares the anonymous
# rate limit with every other runner
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}

jobs:
test:
name: Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
timeout-minutes: 120

strategy:
# Report every version, so a single break does not hide the others.
# Report every version, so a single break does not hide the others
fail-fast: false
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']

env:
# Fail the build on R warnings that would otherwise pass unnoticed
R_KEEP_PKG_SOURCE: yes
# The suite trains various models on CPU. To reduce execution time,
# a change is only checked against floor and ceiling versions.
# The weekly test covers all other versions
python-version: ${{ fromJSON((github.event_name == 'push' || github.event_name == 'pull_request') && '["3.10", "3.14"]' || '["3.10", "3.11", "3.12", "3.13", "3.14"]') }}

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up R
uses: r-lib/actions/setup-r@v2
with:
use-public-rspm: true
- uses: actions/checkout@v7

- name: Install R dependencies
uses: r-lib/actions/setup-r-dependencies@v2
with:
# `arrow` and `kohonen` are required by pysits but are not part of a
# default `install.packages("sits")`, so they are listed explicitly.
packages: |
any::sits
any::arrow
any::kohonen
any::jsonlite

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
- uses: ./.github/actions/setup-pysits
with:
python-version: ${{ matrix.python-version }}

- name: Create a clean virtual environment
# Mirrors how users install pysits, so a broken dependency resolution
# is caught here rather than by them.
run: python -m venv .venv

- name: Install pysits
# `--only-binary` makes pip fail instead of falling back to a source
# build when no wheel matches this interpreter. Those builds have
# shipped binaries that segfault. Only pysits itself is built here.
run: |
.venv/bin/python -m pip install --upgrade pip
.venv/bin/python -m pip install \
--only-binary=:all: --no-binary pysits \
".[dev]"

- name: Report resolved dependency versions
run: .venv/bin/python -m pip list

- name: Verify Arrow interoperability between R and Python
# pyarrow and R arrow package each bundle libarrow. If they stop
# sharing an allocator, transfers are silently corrupted rather than
# failing, so this asserts a known value survives a round-trip.
run: |
.venv/bin/python - <<'EOF'
import pyarrow as pa

import pysits
from pysits.conversions.tibble import (
pandas_sits_to_tibble,
tibble_sits_to_pandas,
)

probe = [0.5, 1.5, 2.5]
assert pa.array(probe).to_pylist() == probe, "pyarrow returns corrupted data"

samples = pysits.samples_modis_ndvi
expected = samples["time_series"][0]["NDVI"].tolist()
actual = tibble_sits_to_pandas(pandas_sits_to_tibble(samples.head(1)))
actual = actual["time_series"][0]["NDVI"].tolist()

assert actual == expected, f"round-trip corrupted data: {actual} != {expected}"
print("Arrow round-trip is lossless.")
EOF
run: uv sync --locked --extra dev

- name: Run tests
run: uv run pytest

latest-dependencies:
name: Latest dependencies
# `uv.lock` pins what `test` runs against, so a release that breaks pysits
# stays invisible there until someone relocks. This resolves from scratch.
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 120

steps:
- uses: actions/checkout@v7

- uses: ./.github/actions/setup-pysits
with:
python-version: '3.14'

- name: Install pysits, ignoring the lockfile
run: uv sync --upgrade --extra dev

- name: Run tests
run: .venv/bin/python -m pytest
run: uv run pytest
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

[project]
name = "pysits"
version = "2.0.0.dev2"
version = "2.0.0.dev3"
description = "Python wrapper for the sits R package"
readme = "README.md"
requires-python = ">=3.10,<4"
Expand Down
Loading
Loading