diff --git a/.github/workflows/random_ci_pytest.yml b/.github/workflows/random_ci_pytest.yml index f50580afc1..11f07100f7 100644 --- a/.github/workflows/random_ci_pytest.yml +++ b/.github/workflows/random_ci_pytest.yml @@ -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 < list[dict[str, str]]: + """Return `n` combos where no version of any single library is reused.""" + n_max = min(len(PANDAS_AND_NUMPY_VERSION), len(POLARS_VERSION), len(PYARROW_VERSION)) + if n > n_max: + msg = ( + f"Requested {n} combos but at most {n_max} are possible " + "without reusing a version of any single library." + ) + raise ValueError(msg) + pandas_numpy = random.sample(PANDAS_AND_NUMPY_VERSION, n) + polars = random.sample(POLARS_VERSION, n) + pyarrow = random.sample(PYARROW_VERSION, n) + return [ + {"pandas": pd, "numpy": np, "polars": pl, "pyarrow": pa} + for (pd, np), pl, pa in zip(pandas_numpy, polars, pyarrow, strict=True) + ] + + +def to_requirements(combo: dict[str, str]) -> str: + """Render a single combo as the contents of a requirements.txt file.""" + return ( + f"numpy=={combo['numpy']}\n" + f"pandas=={combo['pandas']}\n" + f"polars=={combo['polars']}\n" + f"pyarrow=={combo['pyarrow']}\n" + ) + + +def main() -> None: + """Generate version combos and write them to the requested output path.""" + 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() + + num: int = args.num + output: Path = args.output + + combos = sample_distinct(n=num) + + if output.suffix == ".json": + output.write_text(json.dumps(combos), "utf-8") + return + + if num != 1: + msg = f"Non-JSON output ({output.suffix or 'no extension'}) requires --num=1" + raise ValueError(msg) + + output.write_text(to_requirements(combos[0]), "utf-8") + + +if __name__ == "__main__": + main()