Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
39 changes: 31 additions & 8 deletions .github/workflows/random_ci_pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,54 @@ on:
env:
PY_COLORS: 1
PYTEST_ADDOPTS: "--numprocesses=logical"
N_RUNS: 5

permissions:
contents: read

jobs:
generate:
runs-on: ubuntu-latest
outputs:
combos: ${{ steps.gen.outputs.combos }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.10"
- id: gen
name: generate-random-versions
run: |
python utils/generate_random_versions.py --num "$N_RUNS" --output combos.json
echo "combos=$(cat combos.json)" >> "$GITHUB_OUTPUT"

tox:
needs: generate
strategy:
fail-fast: false
matrix:
python-version: ["3.10"]
os: [ubuntu-latest]

runs-on: ${{ matrix.os }}
combo: ${{ fromJson(needs.generate.outputs.combos) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ matrix.python-version }}
python-version: "3.10"
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: "true"
cache-suffix: pytest-random-ci-${{ matrix.python-version }}
cache-suffix: pytest-random-ci-3.10
cache-dependency-glob: "pyproject.toml"
- name: generate-random-versions
run: python utils/generate_random_versions.py
- name: write-requirements
run: |
cat > random-requirements.txt <<EOF
pandas==${{ matrix.combo.pandas }}
numpy==${{ matrix.combo.numpy }}
polars==${{ matrix.combo.polars }}
pyarrow==${{ matrix.combo.pyarrow }}
EOF
cat random-requirements.txt
- name: install-random-versions
run: uv pip install -r random-requirements.txt --system
- name: install-narwhals
Expand Down
78 changes: 67 additions & 11 deletions utils/generate_random_versions.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from __future__ import annotations

import argparse
import json
import random
from pathlib import Path

PANDAS_AND_NUMPY_VERSION = [
PANDAS_AND_NUMPY_VERSION = (
("1.3.5", "1.21.6"),
("1.4.4", "1.22.4"),
("1.5.3", "1.23.5"),
("2.0.3", "1.24.4"),
("2.1.4", "1.25.2"),
("2.2.2", "1.26.4"),
]
POLARS_VERSION = [
)
POLARS_VERSION = (
"0.20.4",
"0.20.5",
"0.20.6",
Expand All @@ -35,8 +37,8 @@
"0.20.31",
"1.0.0",
"1.1.0",
]
PYARROW_VERSION = [
)
PYARROW_VERSION = (
"13.0.0",
"14.0.0",
"14.0.1",
Expand All @@ -49,11 +51,65 @@
"17.0.0",
"18.0.0",
"18.1.0",
]
)

pandas_version, numpy_version = random.choice(PANDAS_AND_NUMPY_VERSION)
polars_version = random.choice(POLARS_VERSION)
pyarrow_version = random.choice(PYARROW_VERSION)

reqs = f"pandas=={pandas_version}\nnumpy=={numpy_version}\npolars=={polars_version}\npyarrow=={pyarrow_version}\n"
Path("random-requirements.txt").write_text(reqs, "utf-8")
def all_combos() -> tuple[tuple[str, str, str, str], ...]:
return tuple(
(pd, np, pl, pa)
for pd, np in PANDAS_AND_NUMPY_VERSION
for pl in POLARS_VERSION
for pa in PYARROW_VERSION
)

@dangotbanned dangotbanned May 16, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove completely to have all different versions in the different samples.
Aside that, I am aware but this was 1 extra line of code if you consider imports 🀣



def sample_distinct(n: int) -> list[dict[str, str]]:
pool = all_combos()
if n > len(pool):
msg = f"Requested {n} distinct combos but only {len(pool)} exist."
raise ValueError(msg)
picks = random.sample(pool, n)
return [
{"pandas": pd, "numpy": np, "polars": pl, "pyarrow": pa}
for pd, np, pl, pa in picks
]


def to_requirements(combo: dict[str, str]) -> str:
return (
f"numpy=={combo['numpy']}\n"
f"pandas=={combo['pandas']}\n"
f"polars=={combo['polars']}\n"
f"pyarrow=={combo['pyarrow']}\n"
)


def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("-n", "--num", type=int, default=1)
parser.add_argument(
"-o",
"--output",
type=Path,
required=True,
help=(
"Path to write to. A `.json` extension writes a JSON array of combos; "
"any other extension writes a requirements.txt-style file (requires n=1)."
),
)
args = parser.parse_args()

combos = sample_distinct(args.num)

if args.output.suffix == ".json":
args.output.write_text(json.dumps(combos), "utf-8")
return

if args.num != 1:
msg = f"Non-JSON output ({args.output.suffix or 'no extension'}) requires --num=1"
raise ValueError(msg)
args.output.write_text(to_requirements(combos[0]), "utf-8")


if __name__ == "__main__":
main()
Loading