From 0d53db061a1cead0bbaaa51f9a53354d51158135 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Thu, 14 May 2026 23:03:57 +0000 Subject: [PATCH] Generalize package: configurable core package and flat column list - packages.yaml: replace the python_versions list with a core_package block (pypi_name, module, repo_url, optional dev_index_urls), a dashboard_title, and a flat columns list of {python, variant} pairs - config.py: new module that loads and validates the config, shared by run and dashboard - run.py: resolve_specs takes the core block instead of literal "astropy" and the nightly index constants; iterate the columns list instead of nesting python and variant loops; --variant/--python become filters over the columns; result JSON key astropy -> core - dashboard.py: read column order, title, and core name from config; group consecutive columns that share a python version under a spanning header so the classic layout still renders as before - integration.yml: a setup job parses columns from packages.yaml and emits the job matrix, so there is one source of truth --- .github/workflows/integration.yml | 34 +++- README.md | 113 ++++++++----- astropy_integration/config.py | 69 ++++++++ astropy_integration/dashboard.py | 67 +++++--- astropy_integration/run.py | 193 ++++++++++++----------- astropy_integration/status.py | 3 +- astropy_integration/templates/index.html | 14 +- packages.yaml | 61 ++++--- 8 files changed, 374 insertions(+), 180 deletions(-) create mode 100644 astropy_integration/config.py diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index ac12697..ffb429c 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -17,14 +17,40 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + # Read the `columns` list from packages.yaml and emit it as the job + # matrix, so the (python, variant) combinations have a single source + # of truth and never need hand-syncing into this workflow. + setup: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + matrix: ${{ steps.columns.outputs.matrix }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.12' + + - name: Build matrix from packages.yaml + id: columns + run: | + pip install pyyaml + python -c ' + import json, yaml + cols = yaml.safe_load(open("packages.yaml"))["columns"] + matrix = [{"python": str(c["python"]), "variant": c["variant"]} for c in cols] + print("matrix=" + json.dumps(matrix)) + ' >> "$GITHUB_OUTPUT" + variant: + needs: setup strategy: fail-fast: false matrix: - # Keep in sync with `python_versions` in packages.yaml. - # (free-threaded builds: use the "t" suffix, e.g. "3.14t".) - python: ["3.12", "3.14"] - variant: [stable, pre, dev] + # Generated by the `setup` job from `columns:` in packages.yaml; + # each entry is one {python, variant} object. + include: ${{ fromJson(needs.setup.outputs.matrix) }} runs-on: ubuntu-latest timeout-minutes: 240 steps: diff --git a/README.md b/README.md index b1a9335..a97f2e8 100644 --- a/README.md +++ b/README.md @@ -12,43 +12,54 @@ The dashboard is published to [astropy.github.io/astropy-integration-testing](https://astropy.github.io/astropy-integration-testing/) after each scheduled run. +The harness itself is not astropy-specific: `core_package` in +`packages.yaml` defines the ecosystem's core package, so the same code +can drive integration testing for another ecosystem (e.g. sunpy) just +by pointing the config at a different core package and package list. + How it works ------------ The `variant` job runs on a schedule (and on `workflow_dispatch`) as a -matrix over astropy variant and Python version. The three variants are: - -| Variant | Astropy | Each package | -|----------|-----------------------------------------------------|----------------------------------------| -| `stable` | Latest non-pre-release on PyPI | Latest non-pre-release on PyPI | -| `pre` | Latest including pre-releases (`--prerelease=allow`)| Latest including pre-releases | -| `dev` | Latest dev wheel from the astropy/simple channel | `git+` (HEAD of main branch) | - -Within each matrix job, a single shared venv is built and packages are -installed one at a time in a deterministic order (coordinated first, -alphabetical within each tier). If a package can't be installed -alongside the existing venv (e.g., it pins `astropy<7` but we already -installed astropy 8), it's skipped and recorded; the rest of the venv -is untouched. After installs, `pytest --pyargs ` runs for each -package that installed successfully. - -The `dashboard` job then downloads the per-matrix-job result JSONs and -publishes the dashboard to `gh-pages`. +matrix over the *columns* configured in `packages.yaml`. Each column is +an independent (Python version, variant) pair — the two axes are not a +cross-product, so you can test e.g. `3.12 + stable`, `3.13 + pre` and +`3.14 + dev` if you want. The three variants are: + +| Variant | Core package | Each package | +|----------|-------------------------------------------------------|----------------------------------------| +| `stable` | Latest non-pre-release on PyPI | Latest non-pre-release on PyPI | +| `pre` | Latest including pre-releases (`--prerelease=allow`) | Latest including pre-releases | +| `dev` | Latest dev wheel from `core_package.dev_index_urls` (or `git+repo_url` if unset) | `git+` (HEAD of main branch) | + +Within each matrix job, a single shared venv is built: the core package +is installed first, then each package one at a time in a deterministic +order (coordinated first, alphabetical within each tier). If a package +can't be installed alongside the existing venv (e.g., it pins +`astropy<7` but we already installed astropy 8), it's skipped and +recorded; the rest of the venv is untouched. After installs, `pytest +--pyargs ` runs for each package that installed successfully. + +A small `setup` job parses `columns:` from `packages.yaml` and emits it +as the workflow matrix, so the column list has a single source of +truth. The `dashboard` job then downloads the per-matrix-job result +JSONs and publishes the dashboard to `gh-pages`. What's in the repo ------------------ | File | Purpose | |---------------------------------------|------------------------------------------------------| -| `packages.yaml` | The list of packages tested + `python_versions` to test against. | -| `astropy_integration/run.py` | Runs one or more (variant, python) combos: resolve specs, install, test, write `results/__.json`. | +| `packages.yaml` | The config: `core_package`, the `columns` to test, and the `packages` list. | +| `astropy_integration/config.py` | Loads and validates `packages.yaml` (shared by `run` and `dashboard`). | +| `astropy_integration/run.py` | Runs one or more columns: resolve specs, install, test, write `results/__.json`. | | `astropy_integration/dashboard.py` | Reads `results/*.json`, renders `site/index.html` (single self-contained page). | | `astropy_integration/cli.py` | Console entry point that dispatches the `run` and `dashboard` subcommands. | | `astropy_integration/status.py` | Shared status vocabulary (used by both `run` and `dashboard`). | | `astropy_integration/templates/` | HTML/CSS for the dashboard (loaded as package data). | | `pyproject.toml` | Package metadata; declares the `astropy-integration` console script. | | `conftest.py` | Repo-root pytest plugin that caps each package to the first `PYTEST_LIMIT_N` tests for PR previews. | -| `.github/workflows/integration.yml` | The matrix workflow (variant x python + dashboard). | +| `.github/workflows/integration.yml` | The matrix workflow (`setup` builds the matrix, `variant` runs each column, `dashboard` publishes). | | `.github/workflows/preview-link.yml` | Companion that posts the "View dashboard preview" status check on PRs. | | `sunpy_pytest.ini` | Custom pytest config referenced by sunpy's `pytest_args` (sunpy's own config requires plugins we don't install). | @@ -78,23 +89,49 @@ python -m http.server -d site 8000 Results land in `results/__.json`; the dashboard in `site/`. Both directories are gitignored. -Python versions ---------------- +Core package +------------ + +`packages.yaml` has a top-level `core_package` block — the package +installed into the shared venv before everything else: + +```yaml +core_package: + pypi_name: astropy + module: astropy + repo_url: https://github.com/astropy/astropy.git + # dev variant: install nightly wheels from these indexes. + # omit to install the dev version from git+repo_url instead. + dev_index_urls: + - https://pypi.anaconda.org/astropy/simple + - https://pypi.anaconda.org/liberfa/simple +``` + +To retarget the harness at a different ecosystem, point `core_package` +at that ecosystem's core package and replace the `packages` list. +`dashboard_title` (also top-level) sets the dashboard's heading. + +Columns +------- -`packages.yaml` has a top-level `python_versions` list (uv notation, -so `"3.14t"` means the free-threaded 3.14 build): +`packages.yaml` has a top-level `columns` list. Each column is one +(Python version, variant) pair and becomes one dashboard column; +Python version and variant are independent, so list whatever +combinations you want (uv notation for Python, so `"3.14t"` is the +free-threaded 3.14 build): ```yaml -python_versions: - - "3.12" - - "3.14t" +columns: + - {python: "3.12", variant: stable} + - {python: "3.13", variant: pre} + - {python: "3.14t", variant: dev} ``` -The runner tests every (variant x python_version) combination. The -dashboard renders Python versions as grouped header columns above the -three variants. **Keep the `matrix.python` list in -`.github/workflows/integration.yml` in sync** with this — the CI uses -its own matrix because GitHub Actions can't read it from YAML directly. +The runner tests every column; `--variant` / `--python` narrow that to +a subset. The dashboard groups consecutive columns that share a Python +version under a spanning header, so a classic `python x variant` +layout still renders as grouped columns. The CI matrix is generated +from this list by the `setup` job, so there's nothing to keep in sync. Adding or disabling a package ----------------------------- @@ -118,15 +155,15 @@ Triggering a run from GitHub 1. Actions tab -> `integration-matrix` workflow. 2. "Run workflow" dropdown -> green button. -3. The matrix expands to `len(variants) x len(python_versions)` - parallel jobs; the `dashboard` job waits for them and publishes - to `gh-pages`. +3. The `setup` job reads `columns:` from `packages.yaml` and the + matrix expands to one parallel `variant` job per column; the + `dashboard` job waits for them and publishes to `gh-pages`. PR previews ----------- -`integration-matrix` also runs on pull requests. Same three-variant -matrix as the scheduled run, just with a different final step: the +`integration-matrix` also runs on pull requests. Same column matrix +as the scheduled run, just with a different final step: the `dashboard` job uploads `site/index.html` as a non-zipped artifact (`actions/upload-artifact@v7` with `archive: false`) instead of publishing to gh-pages. The companion `preview-link` workflow diff --git a/astropy_integration/config.py b/astropy_integration/config.py new file mode 100644 index 0000000..3ea100b --- /dev/null +++ b/astropy_integration/config.py @@ -0,0 +1,69 @@ +"""Load and validate the integration-testing config (``packages.yaml``). + +The config has three top-level keys: + + - ``core_package``: the ecosystem's core package (astropy here, but + swap it to retarget the whole harness at another ecosystem). It is + installed into the shared venv before anything else. + - ``columns``: a flat list of ``{python, variant}`` pairs. Each one + is a single run and a single dashboard column; Python version and + variant are fully decoupled. + - ``packages``: the per-package list (one entry = one dashboard row). + +Both ``run`` and ``dashboard`` read this file, so the parsing and +validation live here in one place. +""" + +from pathlib import Path + +import yaml + +from . import status + + +def _load(path): + return yaml.safe_load(Path(path).read_text()) or {} + + +def load_packages(path): + return list(_load(path).get("packages", [])) + + +def load_core_package(path): + """Return the validated ``core_package`` block. + + ``module`` defaults to ``pypi_name`` when not given. + """ + core = _load(path).get("core_package") or {} + if not core.get("pypi_name"): + raise ValueError(f"{path}: 'core_package.pypi_name' is required") + core.setdefault("module", core["pypi_name"]) + return core + + +def load_columns(path): + """Return the validated ``columns`` list as ``[{python, variant}, ...]``. + + Order is preserved: it drives both the run order and the dashboard + column order. + """ + raw = _load(path).get("columns") or [] + columns = [] + for i, col in enumerate(raw): + python = col.get("python") + variant = col.get("variant") + if not python or not variant: + raise ValueError(f"{path}: columns[{i}] needs both 'python' and 'variant'") + if variant not in status.VARIANTS: + raise ValueError( + f"{path}: columns[{i}] has variant '{variant}'; " + f"expected one of {', '.join(status.VARIANTS)}" + ) + columns.append({"python": str(python), "variant": variant}) + if not columns: + raise ValueError(f"{path}: at least one entry under 'columns' is required") + return columns + + +def load_dashboard_title(path): + return _load(path).get("dashboard_title") or "Ecosystem integration matrix" diff --git a/astropy_integration/dashboard.py b/astropy_integration/dashboard.py index 7775edb..55b0a2c 100644 --- a/astropy_integration/dashboard.py +++ b/astropy_integration/dashboard.py @@ -4,8 +4,9 @@ wrote and emits a single self-contained `site/index.html` with: - one row per package - - one column group per Python version, subdivided into the three - astropy variants (stable / pre / dev) + - one column per configured (python, variant) pair, in config + order; consecutive columns that share a Python version are + grouped under a spanning header - a "Failure logs" section at the bottom with collapsible
for every non-passing cell, anchored from the matching badge """ @@ -18,7 +19,7 @@ from jinja2 import Environment, PackageLoader, select_autoescape -from . import status +from . import config, status def _anchor_id(name, variant, python): @@ -43,17 +44,37 @@ def _load_results(results_dir): return by_combo -def _column_groups(by_combo): - """Discover Python versions and order columns. +def _column_groups(by_combo, config_columns): + """Order the columns that produced results and group them for the header. + + Columns are ordered to match the `columns:` list in the config; any + result not listed there (e.g. a leftover JSON from an old config) is + appended afterwards, sorted. The header groups *consecutive* columns + that share a Python version, so a config that pairs each Python with + a single variant renders as plain columns while the classic + python x variant layout still renders as spanning groups. Returns: - pythons: sorted list of Python version strings (preserves config-style - ordering: shorter strings first, then alphabetical). columns: flat list of (python, variant) tuples in display order. + groups: list of {"python", "span"} dicts for the top header row. """ - pythons = sorted({p for _, p in by_combo}, key=lambda s: (len(s), s)) - columns = [(p, v) for p in pythons for v in status.VARIANTS] - return pythons, columns + present = set(by_combo) # {(variant, python), ...} + columns = [] + for col in config_columns: + key = (col["variant"], col["python"]) + if key in present: + columns.append((col["python"], col["variant"])) + present.discard(key) + for variant, python in sorted(present): + columns.append((python, variant)) + + groups = [] + for python, _variant in columns: + if groups and groups[-1]["python"] == python: + groups[-1]["span"] += 1 + else: + groups.append({"python": python, "span": 1}) + return columns, groups def _variant_meta(variant, python, data): @@ -63,8 +84,8 @@ def _variant_meta(variant, python, data): "variant": variant, "python": python, "has_data": True, - "astropy_version": data["astropy"]["version"], - "extra_index_urls": data["astropy"].get("extra_index_urls") or [], + "core_version": data["core"]["version"], + "extra_index_urls": data["core"].get("extra_index_urls") or [], "python_version": data.get("python_version") or python, "started_at": data["started_at"], "finished_at": data["finished_at"], @@ -156,7 +177,7 @@ def _failures(by_combo, columns): return out -def build(results_dir, output_dir): +def build(results_dir, output_dir, config_path): results_dir = Path(results_dir) output_dir = Path(output_dir) if output_dir.exists(): @@ -168,16 +189,19 @@ def build(results_dir, output_dir): autoescape=select_autoescape(["html"]), ) + config_columns = config.load_columns(config_path) + core = config.load_core_package(config_path) + title = config.load_dashboard_title(config_path) + by_combo = _load_results(results_dir) - pythons, columns = _column_groups(by_combo) + columns, column_groups = _column_groups(by_combo, config_columns) names = _ordered_packages(by_combo) rows = _make_rows(by_combo, names, columns) tier_groups = _group_rows_by_tier(rows) failures = _failures(by_combo, columns) variants_meta = [ - _variant_meta(v, p, by_combo.get((v, p))) - for p in pythons - for v in status.VARIANTS + _variant_meta(variant, python, by_combo.get((variant, python))) + for python, variant in columns ] # If any variant ran with a per-package test limit, surface it in @@ -191,8 +215,10 @@ def build(results_dir, output_dir): (output_dir / "index.html").write_text( env.get_template("index.html").render( - pythons=pythons, - variants=list(status.VARIANTS), + title=title, + core_name=core["pypi_name"], + column_groups=column_groups, + columns=columns, variants_meta=variants_meta, tier_groups=tier_groups, failures=failures, @@ -203,9 +229,10 @@ def build(results_dir, output_dir): def add_arguments(ap): + ap.add_argument("--config", default="packages.yaml") ap.add_argument("--results-dir", default="results") ap.add_argument("--output", default="site") def run(args): - build(args.results_dir, args.output) + build(args.results_dir, args.output, args.config) diff --git a/astropy_integration/run.py b/astropy_integration/run.py index 49cc0d2..843e37e 100644 --- a/astropy_integration/run.py +++ b/astropy_integration/run.py @@ -1,19 +1,23 @@ -"""Run one variant of the astropy ecosystem integration matrix. +"""Run one or more columns of the ecosystem integration matrix. -Creates a single shared venv, installs astropy first then each package -from packages.yaml in order, recording per-package install outcome -(installed / skipped / install-fail / no-spec). Each install is -constrained so it can never downgrade a package already in the venv; -a package needing an older version is reported as skipped. Then runs -`pytest --pyargs ` for each successfully-installed package. -Writes results/__.json with the full venv freeze and -per-package data. +Creates a single shared venv, installs the core package first then +each package from packages.yaml in order, recording per-package +install outcome (installed / skipped / install-fail / no-spec). Each +install is constrained so it can never downgrade a package already in +the venv; a package needing an older version is reported as skipped. +Then runs `pytest --pyargs ` for each successfully-installed +package. Writes results/__.json with the full venv +freeze and per-package data. + +A "column" is one (python, variant) pair from the config; the two +axes are independent (see `columns:` in packages.yaml). `--variant` +and `--python` narrow the configured columns down to a subset. Usage: - astropy-integration run # full matrix (all variants x all Python versions from config) - astropy-integration run --variant stable # one variant, all configured Pythons - astropy-integration run --variant stable --python 3.12 # one combo - astropy-integration run --python 3.14t # all variants on free-threaded 3.14 + astropy-integration run # every configured column + astropy-integration run --variant stable # configured columns with variant=stable + astropy-integration run --variant stable --python 3.12 # one specific column + astropy-integration run --python 3.14t # configured columns with python=3.14t astropy-integration run --variant stable --packages reproject,sunpy astropy-integration run --variant stable --tiers coordinated """ @@ -30,16 +34,11 @@ from datetime import datetime, timezone from pathlib import Path -import yaml from packaging.version import InvalidVersion, Version -from . import status +from . import config, status PYPI_JSON_URL = "https://pypi.org/pypi/{name}/json" -ASTROPY_NIGHTLY_INDEX = "https://pypi.anaconda.org/astropy/simple" -LIBERFA_NIGHTLY_INDEX = ( - "https://pypi.anaconda.org/liberfa/simple" # for pyerfa dev wheels -) def _http_json(url, timeout=20): @@ -87,27 +86,41 @@ def _extras_suffix(pkg): return "[" + ",".join(extras) + "]" if extras else "" -def resolve_specs(packages, variant): - """Resolve the astropy spec and per-package install specs for the variant.""" +def resolve_specs(packages, variant, core): + """Resolve the core-package spec and per-package install specs for the variant.""" + core_name = core["pypi_name"] if variant == "dev": - # Let uv resolve the latest dev version from the astropy/simple - # channel; we read the installed version back after install. No - # explicit pin avoids the PEP 440 local-version segment headaches - # that astropy's nightly wheels have (e.g. 8.1.0.dev53+gabcdef). - # - # `--index-strategy unsafe-best-match` is required because uv's - # default ("first-index") only considers a single index per - # package; astropy/simple hosts astropy AND pyerfa (the channel's - # latest wheels sometimes ship only musllinux). unsafe-best-match - # lets uv fall back to PyPI when the channel's only wheels don't - # match the runner platform. - astropy = { - "install": "astropy", - "version": "", - "extra_index_urls": [ASTROPY_NIGHTLY_INDEX, LIBERFA_NIGHTLY_INDEX], - "prerelease_strategy": "allow", - "index_strategy": "unsafe-best-match", - } + dev_index_urls = core.get("dev_index_urls") or [] + if dev_index_urls: + # Let uv resolve the latest dev version from the core + # package's nightly channel; we read the installed version + # back after install. No explicit pin avoids the PEP 440 + # local-version segment headaches that nightly wheels have + # (e.g. 8.1.0.dev53+gabcdef). + # + # `--index-strategy unsafe-best-match` is required because + # uv's default ("first-index") only considers a single + # index per package; a nightly channel often hosts the core + # package AND its compiled deps, and its latest wheels + # sometimes don't match the runner platform. + # unsafe-best-match lets uv fall back to PyPI in that case. + core_spec = { + "install": core_name, + "version": "", + "extra_index_urls": dev_index_urls, + "prerelease_strategy": "allow", + "index_strategy": "unsafe-best-match", + } + else: + # No nightly channel configured: install the core package's + # dev version straight from the HEAD of its repository. + core_spec = { + "install": f"{core_name} @ git+{core['repo_url']}", + "version": "", + "extra_index_urls": [], + "prerelease_strategy": "allow", + "index_strategy": None, + } def pkg_spec(pkg): repo = pkg.get("repo_url") @@ -117,10 +130,10 @@ def pkg_spec(pkg): else: include_pre = variant == "pre" - astropy_ver = latest_pypi("astropy", include_prereleases=include_pre) - astropy = { - "install": f"astropy=={astropy_ver}", - "version": astropy_ver, + core_ver = latest_pypi(core_name, include_prereleases=include_pre) + core_spec = { + "install": f"{core_name}=={core_ver}", + "version": core_ver, "extra_index_urls": [], "prerelease_strategy": "allow" if include_pre @@ -138,7 +151,7 @@ def pkg_spec(pkg): for pkg in packages: spec, target = pkg_spec(pkg) pkg_specs.append((pkg, spec, target)) - return astropy, pkg_specs + return core_spec, pkg_specs def _resolver_conflict(stderr): @@ -248,19 +261,6 @@ def _write_no_downgrade_constraints(python, path): Path(path).write_text("\n".join(lines) + ("\n" if lines else "")) -def _load_packages(path): - raw = yaml.safe_load(Path(path).read_text()) or {} - return list(raw.get("packages", [])) - - -def _load_python_versions(path): - raw = yaml.safe_load(Path(path).read_text()) or {} - versions = raw.get("python_versions") or [] - if not versions: - versions = ["3.12"] - return [str(v) for v in versions] - - # Ordering of tiers when installing/displaying. Unknown tiers sort last. TIER_RANK = {"coordinated": 0, "affiliated": 1, "pyopensci": 2, "other": 3} @@ -287,10 +287,12 @@ def _run_install(install_cmd, timeout): return proc.returncode, (proc.stderr or proc.stdout) -def run_variant(variant, python_version, packages, repo_root, results_dir, timeouts): - astropy, pkg_specs = resolve_specs(packages, variant) +def run_variant( + variant, python_version, packages, repo_root, results_dir, timeouts, core +): + core_spec, pkg_specs = resolve_specs(packages, variant, core) print(f"\n=== Variant: {variant} (Python {python_version}) ===") - print(f" astropy: {astropy['install']}") + print(f" {core['pypi_name']}: {core_spec['install']}") for pkg, spec, target in pkg_specs: print(f" {pkg['pypi_name']:<20} {spec or '(no install spec)'}") @@ -298,11 +300,12 @@ def run_variant(variant, python_version, packages, repo_root, results_dir, timeo "variant": variant, "started_at": datetime.now(timezone.utc).isoformat(timespec="seconds"), "finished_at": "", - "astropy": { - "install_spec": astropy["install"], - "version": astropy["version"], - "extra_index_urls": astropy["extra_index_urls"], - "prerelease_strategy": astropy["prerelease_strategy"], + "core": { + "name": core["pypi_name"], + "install_spec": core_spec["install"], + "version": core_spec["version"], + "extra_index_urls": core_spec["extra_index_urls"], + "prerelease_strategy": core_spec["prerelease_strategy"], }, "python_requested": python_version, "python_version": "", @@ -341,29 +344,29 @@ def run_variant(variant, python_version, packages, repo_root, results_dir, timeo constraints_path = os.path.join(tmpdir, "no-downgrade-constraints.txt") common = ["uv", "pip", "install", "--python", python, "-q"] - for url in astropy["extra_index_urls"]: + for url in core_spec["extra_index_urls"]: common += ["--extra-index-url", url] - common += [f"--prerelease={astropy['prerelease_strategy']}"] - if astropy.get("index_strategy"): - common += [f"--index-strategy={astropy['index_strategy']}"] + common += [f"--prerelease={core_spec['prerelease_strategy']}"] + if core_spec.get("index_strategy"): + common += [f"--index-strategy={core_spec['index_strategy']}"] - print("\nInstalling astropy + pytest...") + print(f"\nInstalling {core['pypi_name']} + pytest...") # pytest-remotedata registers the `remote_data` marker many # astropy ecosystem packages use; with the plugin installed but # `--remote-data` not passed, those tests are skipped automatically # instead of running and timing out on network calls. rc, err = _run_install( common - + [astropy["install"], "pytest", "pytest-timeout", "pytest-remotedata"], + + [core_spec["install"], "pytest", "pytest-timeout", "pytest-remotedata"], timeouts["install"], ) if rc != 0: - print(" FATAL: astropy install failed") + print(f" FATAL: {core['pypi_name']} install failed") print(err) result["fatal_error"] = err return result, out_path - result["astropy"]["version"] = ( - _pkg_version(python, "astropy") or result["astropy"]["version"] + result["core"]["version"] = ( + _pkg_version(python, core["pypi_name"]) or result["core"]["version"] ) _write_no_downgrade_constraints(python, constraints_path) @@ -477,12 +480,13 @@ def add_arguments(ap): ap.add_argument( "--variant", choices=status.VARIANTS, - help="Variant to run; if omitted, runs all variants in sequence.", + help="Only run configured columns with this variant; " + "if omitted, every configured column runs.", ) ap.add_argument( "--python", - help="Python version to run against (e.g. '3.12', '3.14t'); " - "if omitted, runs every version listed in the config.", + help="Only run configured columns with this Python version " + "(e.g. '3.12', '3.14t'); if omitted, every configured column runs.", ) ap.add_argument("--packages", help="Comma-separated subset of package names to run") ap.add_argument( @@ -499,7 +503,8 @@ def run(args): # instead of buffering until the script exits. sys.stdout.reconfigure(line_buffering=True) - all_packages = _load_packages(args.config) + core = config.load_core_package(args.config) + all_packages = config.load_packages(args.config) packages = all_packages if args.tiers: wanted_tiers = {t.strip() for t in args.tiers.split(",") if t.strip()} @@ -521,21 +526,25 @@ def run(args): results_dir.mkdir(parents=True, exist_ok=True) timeouts = {"install": args.timeout_install, "test": args.timeout_test} - variants_to_run = [args.variant] if args.variant else list(status.VARIANTS) - pythons_to_run = ( - [args.python] if args.python else _load_python_versions(args.config) - ) + + columns = config.load_columns(args.config) + if args.variant: + columns = [c for c in columns if c["variant"] == args.variant] + if args.python: + columns = [c for c in columns if c["python"] == args.python] + if not columns: + sys.exit("No configured columns match the given --variant/--python filters.") fatal_combos = [] - for python_version in pythons_to_run: - for variant in variants_to_run: - result, out_path = run_variant( - variant, python_version, packages, repo_root, results_dir, timeouts - ) - print(f"\nDone {variant}/{python_version}: {_counts(result)}") - print(f"Wrote {out_path}") - if result.get("fatal_error"): - fatal_combos.append(f"{variant}/{python_version}") + for column in columns: + variant, python_version = column["variant"], column["python"] + result, out_path = run_variant( + variant, python_version, packages, repo_root, results_dir, timeouts, core + ) + print(f"\nDone {variant}/{python_version}: {_counts(result)}") + print(f"Wrote {out_path}") + if result.get("fatal_error"): + fatal_combos.append(f"{variant}/{python_version}") if fatal_combos: - sys.exit(f"\nAstropy install failed for: {', '.join(fatal_combos)}") + sys.exit(f"\n{core['pypi_name']} install failed for: {', '.join(fatal_combos)}") diff --git a/astropy_integration/status.py b/astropy_integration/status.py index 47d8ac5..3c86e0f 100644 --- a/astropy_integration/status.py +++ b/astropy_integration/status.py @@ -6,7 +6,8 @@ in exactly one place. """ -# The three astropy variants. Listed once here so both scripts agree. +# The recognised core-package variants. Listed once here so both +# scripts (and config validation) agree. VARIANTS = ("stable", "pre", "dev") # install_status values, used in results/.json diff --git a/astropy_integration/templates/index.html b/astropy_integration/templates/index.html index de253e3..ad45ec6 100644 --- a/astropy_integration/templates/index.html +++ b/astropy_integration/templates/index.html @@ -1,7 +1,7 @@ {% extends "_base.html" %} -{% block title %}Astropy integration matrix{% endblock %} +{% block title %}{{ title }}{% endblock %} {% block body %} -

Astropy ecosystem integration matrix

+

{{ title }}

{% if pytest_limit %}