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
285 changes: 285 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
name: tests

on:
push:
branches: [master, main]
pull_request:
# `labeled` is here so that adding the `test-source-build` label to an
# open pull request is enough to start the long job below, without
# needing a dummy commit to wake the workflow up.
types: [opened, synchronize, reopened, labeled]
workflow_dispatch:
# The from-source GDAL build takes about half an hour, so it runs on a
# schedule rather than on every push -- but it has to run *somewhere*,
# because it is the route that is hardest to keep working.
schedule:
- cron: '0 5 * * 1'

jobs:
# The main matrix: no GDAL, so it runs everywhere and proves that the
# library works without it.
test:
name: py${{ matrix.python }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python: ['3.8', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
with:
path: TopoAnalysis

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}

- name: Install
working-directory: TopoAnalysis
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[test,stats]"

- name: Report the active backend
working-directory: TopoAnalysis
run: python -c "import TopoAnalysis; print(TopoAnalysis.backend())"

- name: Fail if the C++ kernels did not build
working-directory: TopoAnalysis
run: python -c "import sys, TopoAnalysis; sys.exit(0 if TopoAnalysis.backend()=='c++' else 1)"

- name: Test
working-directory: TopoAnalysis
run: python -m pytest -q

- name: Test the pure-NumPy kernels
working-directory: TopoAnalysis
env:
TOPOANALYSIS_PURE_PYTHON: '1'
run: python -m pytest -q

# A second job with GDAL from conda-forge, which exercises the file I/O
# paths the matrix above skips.
test-with-gdal:
name: with GDAL (ubuntu)
runs-on: ubuntu-latest
defaults:
run:
shell: bash -el {0}
steps:
- uses: actions/checkout@v4
with:
path: TopoAnalysis

- uses: conda-incubator/setup-miniconda@v3
with:
environment-file: TopoAnalysis/environment.yml
activate-environment: topoanalysis
auto-activate-base: false

- name: Install
working-directory: TopoAnalysis
run: python -m pip install -e ".[test]"

- name: Test
working-directory: TopoAnalysis
run: python -m pytest -q

# Prove the package still installs when there is no compiler: the
# extension is optional and the NumPy kernels must take over.
test-without-compiler:
name: no compiler
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: TopoAnalysis

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Install with the compiler hidden
working-directory: TopoAnalysis
env:
CC: /bin/false
CXX: /bin/false
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[test]"

- name: The NumPy kernels must be active
working-directory: TopoAnalysis
run: python -c "import sys, TopoAnalysis; print(TopoAnalysis.backend()); sys.exit(0 if TopoAnalysis.backend()=='python' else 1)"

- name: Test
working-directory: TopoAnalysis
run: python -m pytest -q

# The advertised one-command install, on every platform. GDAL is skipped
# here so the job stays fast; the jobs below cover it.
install-script:
name: install.py on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
with:
path: TopoAnalysis
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install everything from a clean checkout
working-directory: TopoAnalysis
run: python install.py --no-gdal
- name: The installed environment must work
working-directory: TopoAnalysis
shell: bash
run: |
if [ -f .venv/bin/python ]; then PY=.venv/bin/python; else PY=.venv/Scripts/python.exe; fi
"$PY" -c "import sys, TopoAnalysis; print(TopoAnalysis.backend()); sys.exit(0 if TopoAnalysis.backend()=='c++' else 1)"
"$PY" -m TopoAnalysis.gdal_setup doctor || true

# Ubuntu's own GDAL is older than the version whose sdist declares numpy as
# a build dependency, so this job is the regression test for the
# gdal_array trap: installing the bindings the naive way here produces an
# osgeo that imports fine and cannot ReadAsArray.
gdal-from-system-packages:
name: GDAL from apt (the gdal_array trap)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: TopoAnalysis
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install the GDAL C library only
run: |
sudo apt-get update
sudo apt-get install -y gdal-bin libgdal-dev
gdal-config --version
- name: Install TopoAnalysis
working-directory: TopoAnalysis
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[test]"
- name: The bindings are missing, and the doctor should say so
working-directory: TopoAnalysis
run: |
python -m TopoAnalysis.gdal_setup doctor && exit 1 || true
- name: Install the bindings against the system library
working-directory: TopoAnalysis
run: python -m TopoAnalysis.gdal_setup install
- name: ReadAsArray must work, not merely import
run: |
python - <<'EOF'
import numpy as np
from osgeo import gdal, gdal_array # noqa: F401
ds = gdal.GetDriverByName('MEM').Create('', 4, 3, 1, gdal.GDT_Float32)
ds.GetRasterBand(1).WriteArray(np.arange(12, dtype='float32').reshape(3, 4))
assert ds.ReadAsArray().shape == (3, 4)
print('gdal', gdal.__version__, 'numpy bridge ok')
EOF
- name: Test, including the GDAL-marked cases
working-directory: TopoAnalysis
run: python -m pytest -q

# Windows has no gdal-config and no source build, so the only practical
# route is a wheel that bundles the C library. That makes this the one
# strategy with no fallback behind it, and worth testing on its own.
gdal-on-windows:
name: GDAL on Windows (prebuilt wheel)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with:
path: TopoAnalysis
- uses: actions/setup-python@v5
with:
# The wheels are published for recent CPythons only.
python-version: '3.12'
- name: Install TopoAnalysis
working-directory: TopoAnalysis
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[test]"
- name: Install GDAL
working-directory: TopoAnalysis
run: python -m TopoAnalysis.gdal_setup install
- name: The doctor must agree that it worked
working-directory: TopoAnalysis
run: python -m TopoAnalysis.gdal_setup doctor
- name: ReadAsArray must work, not merely import
shell: python
run: |
import numpy as np
from osgeo import gdal, gdal_array # noqa: F401
ds = gdal.GetDriverByName('MEM').Create('', 4, 3, 1, gdal.GDT_Float32)
ds.GetRasterBand(1).WriteArray(np.arange(12, dtype='float32').reshape(3, 4))
assert ds.ReadAsArray().shape == (3, 4)
print('gdal', gdal.__version__, 'numpy bridge ok')
- name: Test, including the GDAL-marked cases
working-directory: TopoAnalysis
run: python -m pytest -q

# The no-root route. Slow, so it is scheduled or triggered by hand.
gdal-from-source:
name: GDAL built from source
# Half an hour of compute, so not on every push. Runs on the weekly
# schedule, on demand, or on a pull request labelled `test-source-build`
# -- which is how to exercise it on a change that touches the build
# itself, rather than finding out the following Monday.
if: >-
github.event_name == 'schedule'
|| github.event_name == 'workflow_dispatch'
|| contains(github.event.pull_request.labels.*.name, 'test-source-build')
runs-on: ${{ matrix.os }}
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
with:
path: TopoAnalysis
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Cache the source tarballs
uses: actions/cache@v4
with:
path: ~/.cache/TopoAnalysis/downloads
key: gdal-sources-${{ hashFiles('TopoAnalysis/_gdal_source.py') }}
- name: Install everything, building GDAL from source
working-directory: TopoAnalysis
run: python install.py --gdal-strategy source
- name: Test against the GDAL we built
working-directory: TopoAnalysis
run: |
.venv/bin/python -m TopoAnalysis.gdal_setup doctor
.venv/bin/python -m pytest -q

sdist:
name: sdist rebuilds the extension
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
path: TopoAnalysis
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Build and install from an sdist
working-directory: TopoAnalysis
run: |
python -m pip install --upgrade pip build
python -m build --sdist
python -m pip install dist/*.tar.gz
cd /
python -c "import sys, TopoAnalysis; print(TopoAnalysis.backend()); sys.exit(0 if TopoAnalysis.backend()=='c++' else 1)"
38 changes: 35 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,36 @@
# Build artefacts
build/
dist/
*.egg-info/
*.so
*.pyd
*.o

# Python
__pycache__/
*.py[cod]
.pytest_cache/
.coverage
htmlcov/

# Environments. install.py creates .venv here, because this directory is
# itself the package.
.venv/
venv/

# Where a from-source GDAL build installs when it has nowhere better
share/topoanalysis/

# Docs
docs/_build/

# Editors
.idea/
.vscode/
.metadata
*.pyc
.idea
.idea/*
.project
.pydevproject
.settings/

# macOS
.DS_Store
Loading
Loading