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
44 changes: 40 additions & 4 deletions .github/workflows/build-ultraplot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,15 @@ jobs:
with:
fetch-depth: 0

- name: Prepare compatible test environment
run: >-
python3 tools/ci/version_support.py
--matplotlib-version '${{ inputs.matplotlib-version }}'
--environment-output '${{ runner.temp }}/ultraplot-environment.yml'

- uses: mamba-org/setup-micromamba@v3.2.1
with:
environment-file: ./environment.yml
environment-file: ${{ runner.temp }}/ultraplot-environment.yml
init-shell: bash
condarc-file: ./.github/micromamba-condarc.yml
post-cleanup: none
Expand All @@ -69,6 +75,14 @@ jobs:
- name: Build Ultraplot
run: |
pip install --no-build-isolation --no-deps .
python - <<'PY'
import sys
import matplotlib, mpltern, pycirclize
assert sys.version_info[:2] == tuple(map(int, '${{ inputs.python-version }}'.split('.')))
assert matplotlib.__version__.startswith('${{ inputs.matplotlib-version }}.')
print('Python executable:', sys.executable)
print('Matplotlib:', matplotlib.__version__)
PY

compare-baseline:
name: Compare baseline Python ${{ inputs.python-version }} with MPL ${{ inputs.matplotlib-version }}
Expand Down Expand Up @@ -100,9 +114,15 @@ jobs:

- uses: actions/checkout@v7

- name: Prepare compatible test environment
run: >-
python3 tools/ci/version_support.py
--matplotlib-version '${{ inputs.matplotlib-version }}'
--environment-output '${{ runner.temp }}/ultraplot-environment.yml'

- uses: mamba-org/setup-micromamba@v3.2.1
with:
environment-file: ./environment.yml
environment-file: ${{ runner.temp }}/ultraplot-environment.yml
init-shell: bash
condarc-file: ./.github/micromamba-condarc.yml
post-cleanup: none
Expand All @@ -128,11 +148,26 @@ jobs:
echo "base_sha=${BASE_SHA}" >> "${GITHUB_OUTPUT}"
echo "Resolved baseline ref=${BASE_REF} sha=${BASE_SHA}"

- name: Check baseline version support
id: baseline-support
run: |
git show '${{ steps.baseline-ref.outputs.base_sha }}:pyproject.toml' > '${{ runner.temp }}/baseline-pyproject.toml'
python tools/ci/version_support.py \
--baseline-pyproject '${{ runner.temp }}/baseline-pyproject.toml' \
--python-version '${{ inputs.python-version }}' \
--matplotlib-version '${{ inputs.matplotlib-version }}' >> "$GITHUB_OUTPUT"

- name: Explain unavailable baseline
if: steps.baseline-support.outputs.baseline-supported != 'true'
run: |
echo '::notice::Image comparison skipped: the base commit does not support this Python/Matplotlib pair. PR tests still run in the coverage job.'
echo 'Image comparison skipped for Python ${{ inputs.python-version }} / Matplotlib ${{ inputs.matplotlib-version }}: base commit ${{ steps.baseline-ref.outputs.base_sha }} does not support this pair. The coverage job still tests the PR on this pair.' >> "$GITHUB_STEP_SUMMARY"

# Cache Baseline Figures (Restore step)
- name: Cache Baseline Figures
id: cache-baseline
uses: actions/cache@v6
if: ${{ env.IS_PR }}
if: env.IS_PR == 'true' && steps.baseline-support.outputs.baseline-supported == 'true'
with:
path: ./ultraplot/tests/baseline # The directory to cache
# Key is based on OS, Python/Matplotlib versions, and the base commit SHA
Expand All @@ -143,7 +178,7 @@ jobs:
# Conditional Baseline Generation (Only runs on cache miss)
- name: Generate baseline from main
# Skip this step if the cache was found (cache-hit is true)
if: steps.cache-baseline.outputs.cache-hit != 'true' || !env.IS_PR
if: steps.baseline-support.outputs.baseline-supported == 'true' && (steps.cache-baseline.outputs.cache-hit != 'true' || env.IS_PR != 'true')
run: |
mkdir -p ultraplot/tests/baseline
echo "TEST_MODE=${TEST_MODE}"
Expand Down Expand Up @@ -219,6 +254,7 @@ jobs:

# Image Comparison (Uses cached or newly generated baseline)
- name: Image Comparison Ultraplot
if: steps.baseline-support.outputs.baseline-supported == 'true'
run: |
set -uo pipefail
# This workflow runs in a login shell (bash -el), which executes
Expand Down
36 changes: 29 additions & 7 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,16 @@ jobs:
python tools/ci/version_support.py --format github-output >> $GITHUB_OUTPUT

coverage:
name: Coverage
name: Coverage Python ${{ matrix.python-version }} / MPL ${{ matrix.matplotlib-version }}
runs-on: ubuntu-latest
needs:
- run-if-changes
if: always() && needs.run-if-changes.outputs.run == 'true' && github.event_name == 'pull_request'
- get-versions
if: always() && needs.run-if-changes.outputs.run == 'true' && needs.get-versions.result == 'success' && github.event_name == 'pull_request'
strategy:
fail-fast: false
matrix:
include: ${{ fromJson(needs.get-versions.outputs.test-matrix) }}
defaults:
run:
shell: bash -el {0}
Expand All @@ -157,16 +162,22 @@ jobs:
with:
fetch-depth: 0

- name: Prepare compatible coverage environment
run: >-
python3 tools/ci/version_support.py
--matplotlib-version '${{ matrix.matplotlib-version }}'
--environment-output '${{ runner.temp }}/ultraplot-environment.yml'

- uses: mamba-org/setup-micromamba@v3.2.1
with:
environment-file: ./environment.yml
environment-file: ${{ runner.temp }}/ultraplot-environment.yml
init-shell: bash
condarc-file: ./.github/micromamba-condarc.yml
post-cleanup: none
create-args: >-
--verbose
python=3.10
matplotlib=3.9
python=${{ matrix.python-version }}
matplotlib=${{ matrix.matplotlib-version }}
cache-environment: true
cache-downloads: false

Expand All @@ -176,6 +187,16 @@ jobs:

- name: Run full coverage suite
run: |
python - <<'PY'
import sys
from importlib.metadata import version
import matplotlib, mpltern, pycirclize
assert sys.version_info[:2] == tuple(map(int, '${{ matrix.python-version }}'.split('.')))
assert matplotlib.__version__.startswith('${{ matrix.matplotlib-version }}.')
print('Python executable:', sys.executable)
for package in ('matplotlib', 'mpltern', 'pycirclize', 'cartopy'):
print(f'{package}: {version(package)}')
PY
pytest -q --tb=short --disable-warnings -n auto -p pytest_cov \
--cov=ultraplot --cov-branch --cov-context=test \
--cov-report=xml:coverage.xml --cov-report= \
Expand All @@ -186,7 +207,7 @@ jobs:
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
name: codecov-pr-python3.10-mpl3.9
name: codecov-pr-python${{ matrix.python-version }}-mpl${{ matrix.matplotlib-version }}

build:
needs:
Expand All @@ -212,6 +233,7 @@ jobs:
build-success:
needs:
- build
- coverage
- run-if-changes
if: always()
runs-on: ubuntu-latest
Expand All @@ -220,7 +242,7 @@ jobs:
if [[ '${{ needs.run-if-changes.outputs.run }}' == 'false' ]]; then
echo "No changes detected, tests skipped."
else
if [[ '${{ needs.build.result }}' == 'success' ]]; then
if [[ '${{ needs.build.result }}' == 'success' && ( '${{ needs.coverage.result }}' == 'success' || '${{ needs.coverage.result }}' == 'skipped' ) ]]; then
echo "All tests passed successfully!"
else
echo "Tests failed!"
Expand Down
2 changes: 2 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ dependencies:
- numpy
- matplotlib>=3.9
- ffmpeg # animation encoder for ultraplot.animation
# CI/Nox omit Basemap for Matplotlib >=3.11; retain legacy/docs coverage here.
- basemap >=1.4.1
- cartopy
- xarray
Expand All @@ -24,4 +25,5 @@ dependencies:
- markdown
- requests
- pip:
- mpltern
- pycirclize
35 changes: 23 additions & 12 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ def _mamba_env_name(python_version: str, matplotlib_version: str) -> str:
def _ensure_mamba_env(
session: nox.Session, python_version: str, matplotlib_version: str
) -> str:
if tuple(map(int, matplotlib_version.split("."))) >= (3, 11) and tuple(
map(int, python_version.split("."))
) < (3, 11):
session.skip("Matplotlib 3.11 requires Python 3.11 or newer.")
root = _mamba_root()
env_name = _mamba_env_name(python_version, matplotlib_version)
env_path = root / "envs" / env_name
Expand All @@ -79,19 +83,26 @@ def _ensure_mamba_env(
exe = _mamba_exe(session)
env = os.environ.copy()
env["MAMBA_ROOT_PREFIX"] = str(root)
session.run(
exe,
"create",
"-y",
"-n",
env_name,
"-f",
str(PROJECT_ROOT / "environment.yml"),
f"python={python_version}",
f"matplotlib={matplotlib_version}",
external=True,
env=env,
environment = _load_version_support().environment_for_matplotlib(
(PROJECT_ROOT / "environment.yml").read_text(encoding="utf-8"),
matplotlib_version,
)
with tempfile.TemporaryDirectory() as tmpdir:
environment_path = Path(tmpdir) / "environment.yml"
environment_path.write_text(environment, encoding="utf-8")
session.run(
exe,
"create",
"-y",
"-n",
env_name,
"-f",
str(environment_path),
f"python={python_version}",
f"matplotlib={matplotlib_version}",
external=True,
env=env,
)
return env_name


Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description = "A succinct matplotlib wrapper for making beautiful, publication-q
readme = "README.rst"
requires-python = ">=3.10,<3.15"
license = "MIT"
license-files = ["LICENSE"]
license-files = ["LICENSE.txt"]
authors = [
{ name = "Casper van Elteren", email = "caspervanelteren@gmail.com" },
{ name = "Luke Davis", email = "lukelbd@gmail.com" },
Expand All @@ -25,7 +25,7 @@ classifiers = [
"Programming Language :: Python :: 3.14",
]
dependencies = [
"matplotlib>=3.9,<3.11",
"matplotlib>=3.9,<3.12",
"numpy>=1.26.0",
"typing-extensions; python_version < '3.12'",
]
Expand All @@ -49,6 +49,7 @@ docs = [
"lxml-html-clean",
"markdown",
"mpltern",
"pycirclize>=1.10.1",
# Floors below are the versions that work with Sphinx 9, which removed
# sphinx.ext.autosummary.get_documenter. Without them pip is free to pair a
# current Sphinx with an extension that cannot import it, and the build
Expand Down Expand Up @@ -100,4 +101,4 @@ version_file_template = "__version__ = '{version}'\n"

[tool.ultraplot.core_versions]
python = ["3.10", "3.11", "3.12", "3.13", "3.14"]
matplotlib = ["3.9", "3.10"]
matplotlib = ["3.9", "3.10", "3.11"]
2 changes: 1 addition & 1 deletion requirements-minimal.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
numpy>=1.26.0
matplotlib>=3.9,<3.11
matplotlib>=3.9,<3.12
typing-extensions; python_version < "3.12"
40 changes: 40 additions & 0 deletions tools/ci/version_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,21 @@ def build_version_payload(pyproject: dict | None = None) -> dict:
}


def environment_for_matplotlib(environment: str, matplotlib_version: str) -> str:
"""Omit Basemap's incompatible Matplotlib constraint for 3.11+ jobs.

Keep the source YAML intact apart from the standalone Basemap dependency.
This runs before the conda environment exists, without a YAML dependency.
"""
if tuple(map(int, matplotlib_version.split("."))) < (3, 11):
return environment
return "".join(
line
for line in environment.splitlines(keepends=True)
if not re.match(r"^\s*-\s+basemap(?:\s|[<>=!~]|$)", line)
)


def _emit_github_output(payload: dict) -> str:
"""
Format the derived version payload for ``$GITHUB_OUTPUT`` consumption.
Expand All @@ -226,13 +241,38 @@ def main() -> int:
CLI entry point used by GitHub Actions and local verification.
"""
parser = argparse.ArgumentParser()
parser.add_argument("--environment-output", type=Path)
parser.add_argument("--matplotlib-version")
parser.add_argument("--python-version")
parser.add_argument("--baseline-pyproject", type=Path)
parser.add_argument(
"--format",
choices=("json", "github-output"),
default="json",
)
args = parser.parse_args()

if args.baseline_pyproject is not None:
if args.python_version is None or args.matplotlib_version is None:
parser.error(
"--baseline-pyproject requires --python-version and --matplotlib-version"
)
baseline = load_pyproject(args.baseline_pyproject)
supported = args.python_version in supported_python_versions(
baseline
) and args.matplotlib_version in supported_matplotlib_versions(baseline)
print(f"baseline-supported={str(supported).lower()}")
return 0

if args.environment_output is not None:
if args.matplotlib_version is None:
parser.error("--environment-output requires --matplotlib-version")
environment = (ROOT / "environment.yml").read_text(encoding="utf-8")
environment = environment_for_matplotlib(environment, args.matplotlib_version)
args.environment_output.parent.mkdir(parents=True, exist_ok=True)
args.environment_output.write_text(environment, encoding="utf-8")
return 0

payload = build_version_payload()
if args.format == "github-output":
print(_emit_github_output(payload))
Expand Down
1 change: 1 addition & 0 deletions ultraplot/.opencode/goals/state.json.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"protocol":2,"sentinel":true,"token":"opencode-goal-plugin-immutable-claims-v2","pid":1,"hostname":"opencode-goal-plugin-v2.invalid","createdAt":1786452102710}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": 1,
"migratedAt": 1786452102728
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": 1,
"goals": [],
"results": [],
"archives": [],
"orderedSessions": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"protocol":2,"sentinel":true,"token":"opencode-goal-plugin-immutable-claims-v2","pid":1,"hostname":"opencode-goal-plugin-v2.invalid","createdAt":1786452102644}
5 changes: 4 additions & 1 deletion ultraplot/_interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,10 @@ class _SurfaceProxyRecipe:


def _surface_geometry_signature(surface):
vector = getattr(surface, "_vec", None)
# Matplotlib 3.11 stores polygons in _faces instead of the flattened _vec.
vector = getattr(surface, "_faces", None)
if vector is None:
vector = getattr(surface, "_vec", None)
return (id(vector), getattr(vector, "shape", None))


Expand Down
Loading
Loading