~sara/pdm-matrix

A PDM plugin to generate virtual environments, and scripts in them, based on a matrix of variable permutations
67004ff0 — sarayourfriend 10 months ago
More testing
35b322dc — sarayourfriend 10 months ago
Stub out tests
bcbd9fbc — sarayourfriend 10 months ago
Fix cyclical dependencies

refs

main
browse  log 

clone

read-only
https://git.sr.ht/~sara/pdm-matrix
read/write
git@git.sr.ht:~sara/pdm-matrix

You can also use your local clone with git send-email.

#pdm-matrix

PDM plugin to enable generating virtual environments, and running scripts inside them, based on a matrix of variables defined in pyproject.toml.

This plugin is conceptually inspired by a similar feature in hatch.

#Installation

Add pdm-matrix to the PDM plugins in pyproject.toml:

# pyproject.toml
[tool.pdm]
plugins = [
    "pdm-matrix",
]

Then run:

pdm install --plugins

It is not recommended to install pdm-matrix into your global PDM installation, as it is useless without additional project-specific matrix definitions.

#Usage

Define one or more matrix in pyproject.toml, then use pdm matrix to interact. The command requires a matrix name to be specified, but beyond that behaves almost identically to pdm run, except that it runs the command or script in each virtual environment: pdm matrix ci pytest will run pdm run pytest in each of the virtual environments defined for the matrix ci. Matrices may also declare themselves as an extension of another matrix.

Usage: pdm matrix [-h] [-v | -q] [-g] [-p PROJECT_PATH] [--list]
 [--format {table,json}] [--venv MATRIX_VENV | --filter FILTER]
 [--filter-mode {and,or,not}] [--fast-fail]
 [matrix] [script] [args ...]

Generate and run scripts in virtual environments based on project defined variable matrices

Options:
  -h, --help            Show this help message and exit.
  -v, --verbose         Use `-v` for detailed output and `-vv` for more detailed
  -q, --quiet           Suppress output
  -g, --global          Use the global project, supply the project root with `-p`
                        option
  -p PROJECT_PATH, --project PROJECT_PATH
                        Specify another path as the project root, which changes the
                        base of pyproject.toml and __pypackages__ [env var:
                        PDM_PROJECT]

Matrix Parameters:
  --list                List all matricies or, if a matrix is specified, the virtual
                        environments created by the matrix
  --format {table,json}
                        Format in which to output --list result (default 'table')
  --venv MATRIX_VENV    Specify a virtual environment by name
  --filter FILTER       Include (or exclude) virtual environments matching the
                        filters. e.g., --filter python=3.10 --filter
                        library===0.12.0
  --filter-mode {and,or,not}
                        Specify how to combine --filter arguments.
  --fast-fail           Stop after the first virtual environment with a non-zero
                        exit code (overrides 'keep_going' on the matrix, if present)
  matrix                The configuration matrix to use
  script                The command or script to run (passed to 'pdm run')
  args                  Arguments to pass with the command

#Virtual environment filtering

The --venv, --filter, and --filter-mode CLI arguments can be used to filter matrix permutations. In a CI environment, this can be used to split runs along meaningful lines to run test environment in parallel. When debugging a particular configuration, filtering allows you to save time by bypassing irrelevant conditions.

Consider this basic example matrix:

[tool.pdm.matrix.ci.variables] python = ["3.10", "3.11", "3.12"]


Running `pdm matrix ci pytest` will run `pytest` three times, in series. Doing so in CI make the outcome harder to read and wastes time when the runs could be parallelised. Doing so locally when you are only debugging an issue with Python 3.10 wastes time.

To target only the Python 3.10 environment, run:

pdm matrix ci --filter python=3.10 pytest


To retrieve the list of configured Python versions to programmatically generate the filters needed in a CI environment, use `--list --format json` and parse the result as needed. Using `jq` for example:

pdm matrix -v --list --format json | jq '.[] | select(.name == "ci").variables.python'
[ "3.10", "3.11", "3.12" ]


In GitHub Actions, the output can be used to program a job matrix, and then passed as `pdm-matrix` filters:

jobs: get-test-matrix: name: Get PDM matrix variables runs-on: ubuntu-latest

steps:
  - uses: actions/checkout@v4
  - uses: pdm-project/setup-pdm@v4

  - name: Create the test matrix output
    shell: bash
    run: |
      variables=$(pdm matrix -v --list --format json | jq '.[] | select(.name == "ci").variables | pick(.python)')
      echo "pdm_matrix_variables=$variables" >> "$GITHUB_OUTPUT"

test: name: Run tests with Python ${{ matrix.python }} runs-on: ubuntu-latest needs: - get-test-matrix matrix: ${{ fromJson(needs.get-test-matrix.pdm_matrix_variables) }}

steps:
  - uses: actions/checkout@v4
  - uses: pdm-project/setup-pdm@v4

  - name: Run tests
    shell: bash
    run: pdm matrix ci --filter python=${{ matrix.python }} pytest

### Matrix definition

Each matrix is defined in `pyproject.toml` as table under `tool.pdm.matrix`. The matrix must define a `variables` table, where each entry is a list of permutations for a given variable.

**Options**:
- `venv_name_pattern`: Customise the [generated virtual environment names](#environment-names)
- `overlay_dependencies`: Specify dependencies with variables defining versions to [overlay into each virtual environment](#dependency-overlay)
- `keep_going` (default: `true`): Similar to PDM's composite script's option by the same name. When `true`, `pdm-matrix` will run the specified command in all environments regardless of individual failures. When `false`, it will exit after the first failed environment. Use the `--fail-fast` CLI argument to override this for a particular run.
- `scripts`: Define [matrix-specific scripts](#scripts), which support [variable interpolation](#variable-interpolation)
- `variables` (required): Define [variables and permutations used to generate each virtual environment](#variables)

Refer to [the example matrices for various recipes and use-cases](#example-matrices).

#### Variables

A matrix must define at least one set of variable permutations. Each entry into the matrix's `variables` table must be a list of strings, corresponding to the "permutations" for that variable. The set of virtual environments for the matrix is created as a product of all permutations of all variables. Variables may have different numbers of permutations.

The following is an example of the bare-minimum `variables` definition to run a script using multiple Python versions:

[tool.pdm.matrix.demo.variables] python = ["3.11", "3.12"]


With the matrix above, `pdm matrix demo pytest` will run `pytest` twice, each time in a unique virtual environment for each of the defined Python versions.

Variables must not define multiple identical permutations.

##### Special `python` variable

The `python` variable is special. When included, it will cause the Python version to change for each generated virtual environment.

If the particular Python version is unavailable on the host system, `pdm python install` is automatically used to download and install it. Configure `pdm python`'s behaviour in the usual way, via PDM's configuration options.

#### Scripts

A matrix may define scripts to overlay onto the project's default scripts. The `scripts` table has an identical layout, options, and API as `tool.pdm.scripts`, with the exception that it also supports [variable interpolation](#variable-interpolation) in the script contents.

This means, for example, that you can modify the command a script runs based on the matrix's variables:

[tool.pdm.matrix.demo.variables] python = ["3.11", "3.12"] flag = ["-V", "-VV"]

[tool.pdm.matrix.demo.scripts] version = "python {flag}"


`pdm-matrix` will interpolate all variables into each script and make it available in the virtual environment before running the specified command. Scripts defined in the matrix will overwrite project-level scripts by the same name. Additionally, [matrix scripts can refer to project-level scripts](#matrix-scripts-using-project-level-scripts).

#### Variable interpolation

Each variable is available to interpolate into the matrix-specific scripts, or the matrix's `venv_name_pattern`. Use f-string style interpolations. Given a variable named `flag`, interpolate it with `{flag}`.

#### Dependency overlay

Matrices may define a top-level setting named `overlay_dependencies`, which must be a list of dependency names. Each overlaid dependency must have a corresponding matrix variable named after the dependency. Overlaid dependencies are force installed into each virtual environment.

Variables specifying dependency overlay permutations must use one of the following:

- a specific version (e.g., `2.30.0`)
- `project`, to use the project's default version of the package
- `exclude`, to exclude the package from the virtual environment

#### Environment names

Each combination of variable permutations in the matrix creates a unique virtual environment. Environments _must_ have a unique name in order to avoid name collisions. `pdm-matrix` will generate a name based on the variables in order, separated by underscores. Overlay dependencies will have the dependency name prefixed to the variable permutation, and the Python version will include the interpreter.

For greater control over the name of each virtual environment, define a `venv_name_pattern` at the top level of the matrix. Use [variable interpolation](#variable-interpolation) to create a pattern based on the variables for each permutation. _You must interpolate each variable to ensure a unique name is generated_. `pdm-matrix` will raise an exception if it finds non-unique environment names.

`pdm-matrix` generates each environment's name using the optional pattern, each of the variables, and with the matrix's name followed by a dot used as a prefix.

Virtual environments _must_ have unique names. To avoid collision with other matrices in the same project, the matrix name is always prefixed to the virtual environment name, with a dot separating the two.

Consider the example:

[tool.pdm.matrix.demo] overlay_dependencies = ["requests"]

[tool.pdm.matrix.demo.variables] python = ["3.11", "3.12"] flag = ["-V", "-VV"] requests = ["project", "2.30.0"]


For this matrix, the default naming pattern is equivalent to `flag={flag}_requests={requests}_cpython@{python}`. Note that variables are sorted alphabetically, except the Python interpreter which will always come last. This will generate the following default environment names, retrievable using `pdm matrix demo --list`:

- `demo.flag=-V_requests=project_cpython@3.11`
- `demo.flag=-V_requests=project_cpython@3.12`
- `demo.flag=-V_requests=2.30.0_cpython@3.11`
- `demo.flag=-V_requests=2.30.0_cpython@3.12`
- `demo.flag=-VV_requests=project_cpython@3.11`
- `demo.flag=-VV_requests=project_cpython@3.12`
- `demo.flag=-VV_requests=2.30.0_cpython@3.11`
- `demo.flag=-VV_requests=2.30.0_cpython@3.12`

## Example matrices

### Run tests using multiple Python versions

`pdm matrix ci pytest` with this example will run `pytest` with each of the defined versions of Python.

[tool.pdm.matrix.ci.variables] python = ["3.10", "3.11", "3.12"]


### Run tests with different dependency versions

`pdm matrix ci pytest` with this example will run `pytest` with the different versions of `requests` installed into each virtual environment.

This can be used, for example, to automate tests against multiple divergent versions of a dependency, to enforce backwards and forwards compatibility.

[tool.pdm.matrix.ci] overlay_dependencies = ["requests"]

[tool.pdm.matrix.ci.variables] requests = ["2.31.0", "2.30.0"]


### Extend and layer matrices

For managing tests in different scenarios, it can be useful to define separate matrices, rather than needing to rely on [environment filtering](#virtual-environment-filtering), especially if the filtering happens regularly. While you can define a regular PDM script to encode the default filters, it may be easier to reason about and maintain layered matrices.

[tool.pdm.matrix.test] overlay_dependencies = ["requests"]

[tool.pdm.matrix.ci.variables] requests = ["project", "exclude", "2.10.0"]

[tool.pdm.matrix.ci] extends = "test"

variables = { python = ["3.10", "3.11", "3.12"] }


You can use the `test` matrix locally for regular test invocations, and then use `ci` for CI environments, or special test runs (e.g., long-running nightly integration tests).

The same idea can be achieved imperatively if using a single matrix, and using `--filter python=<project's default python>`. The layered matrix approach allows falling back to project defaults without needing to encode them in multiple places.

Matrices only extend `overlay_dependencies`, `variables`, and `scripts`. Extending matrices _overwrite_ rather than merge into properties declared on both. For example, if the base matrix defines a variable `python = ["3.12"]` and the extending matrix defines `python = ["3.10", "3.11"]`, only 3.10 and 3.11 will be present in the final matrix. In other words, the lists are not merged. Nested structures (like composite scripts) are likewise overwritten, rather than merged.

Extending matrices have access to all the variables and scripts of the base matrix. When declaring a custom `venv_name_pattern`, all variables from both matrices must be used to prevent environment name collisions.
Do not follow this link