Skip to content

Commit ca1ff95

Browse files
feanilclaude
andcommitted
feat: update tox.ini to use tox-uv with locked dependency groups
Adds tox-uv>=1 to [tox].requires. Switches all testenv sections to uv-venv-lock-runner so every run installs exactly from uv.lock. For the Django version matrix, splits test-base (framework-agnostic pytest deps) from test (test-base + current Django 5.2). The django42 group holds the legacy Django 4.2 pin. Declaring these groups as conflicting in [tool.uv].conflicts lets uv produce a single uv.lock with a separate resolution per group. Tox factor conditionals select the right group per environment. Adds docs/how-tos/adding-a-matrix-dependency.rst explaining how to add or retire a version from any dependency matrix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3dd021c commit ca1ff95

4 files changed

Lines changed: 136 additions & 12 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
.. _how-to-matrix-dependency:
2+
3+
Adding or Updating a Matrix Dependency Version
4+
###############################################
5+
6+
This project tests against multiple versions of certain dependencies (e.g.
7+
Django). Each version in the matrix gets its own ``[dependency-groups]`` entry
8+
in ``pyproject.toml``, and ``uv`` produces a single ``uv.lock`` with a separate
9+
resolution per group via the ``[tool.uv].conflicts`` feature.
10+
11+
The ``test`` group always represents the **current default version** — the one
12+
used by quality checks, docs builds, and the primary CI matrix entry. Legacy
13+
versions get their own named groups (e.g. ``django42``).
14+
15+
How it works
16+
************
17+
18+
``uv`` normally requires all dependency groups to resolve to a compatible set of
19+
packages. Declaring groups as conflicting tells ``uv`` they are mutually
20+
exclusive — it produces a single ``uv.lock`` with independent resolutions for
21+
each group. ``tox`` then selects the right group per environment via factor
22+
conditionals, and ``uv-venv-lock-runner`` installs from the lockfile so every
23+
run is fully reproducible.
24+
25+
Adding support for a new version
26+
*********************************
27+
28+
Use this process when you want to **add a new version** to the test matrix
29+
(e.g. start testing against Django 6.0 while still supporting Django 5.2).
30+
31+
1. **Update the default group** in ``pyproject.toml`` to the new version:
32+
33+
.. code-block:: toml
34+
35+
# Before
36+
test = [
37+
{include-group = "test-base"},
38+
"Django>=5.0,<6.0",
39+
]
40+
41+
# After
42+
test = [
43+
{include-group = "test-base"},
44+
"Django>=6.0,<7.0",
45+
]
46+
47+
2. **Add a legacy group** for the version being retained:
48+
49+
.. code-block:: toml
50+
51+
django52 = [
52+
{include-group = "test-base"},
53+
"Django>=5.0,<6.0",
54+
]
55+
56+
3. **Register the conflict** in ``[tool.uv]`` so ``uv`` knows the groups are
57+
mutually exclusive:
58+
59+
.. code-block:: toml
60+
61+
[tool.uv]
62+
conflicts = [
63+
[{group = "test"}, {group = "django42"}, {group = "django52"}],
64+
]
65+
66+
4. **Add a tox factor** for the new legacy version. Update ``envlist`` and add
67+
a factor conditional to ``dependency_groups``:
68+
69+
.. code-block:: ini
70+
71+
[tox]
72+
envlist = py{311,312}-django{52,60},docs,quality,pii_check
73+
74+
[testenv]
75+
runner = uv-venv-lock-runner
76+
dependency_groups =
77+
django42: django42
78+
django52: django52
79+
django60: test
80+
81+
5. **Regenerate the lockfile**:
82+
83+
.. code-block:: bash
84+
85+
make upgrade
86+
87+
Retiring a version
88+
******************
89+
90+
When a Django version reaches end-of-life and you want to drop it from the
91+
matrix:
92+
93+
1. Remove the legacy group (e.g. ``django42``) from ``[dependency-groups]``.
94+
2. Remove it from the ``conflicts`` list in ``[tool.uv]``.
95+
3. Remove the tox factor conditional line and the envlist entry.
96+
4. Run ``make upgrade`` to regenerate the lockfile.

backend/docs/how-tos/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
11
How-tos
22
#######
3+
4+
.. toctree::
5+
:maxdepth: 1
6+
7+
adding-a-matrix-dependency

backend/pyproject.toml

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,24 @@ Homepage = "https://openedx.org/openedx/sample-plugin"
4747
Repository = "https://openedx.org/openedx/sample-plugin"
4848

4949
[dependency-groups]
50-
test = [
50+
test-base = [
5151
"pytest-cov",
5252
"pytest-django",
5353
"code-annotations",
5454
"edx-django-utils",
5555
"django-extensions",
5656
]
57+
# Current default Django version used by quality, docs, and the default test
58+
# matrix entry. When adding or retiring a Django version from the matrix, see
59+
# docs/how-tos/adding-a-matrix-dependency.rst for the full process.
60+
test = [
61+
{include-group = "test-base"},
62+
"Django>=5.0,<6.0",
63+
]
64+
django42 = [
65+
{include-group = "test-base"},
66+
"Django>=4.0,<5.0",
67+
]
5768
quality = [
5869
{include-group = "test"},
5970
"edx-lint",
@@ -107,6 +118,13 @@ version_scheme = 'only-version'
107118
local_scheme = 'no-local-version'
108119

109120
[tool.uv]
121+
# Each entry lists groups with mutually exclusive version requirements so uv can
122+
# produce a single uv.lock that contains a separate resolution for each. Add a
123+
# new pair here whenever you add a legacy-version group to [dependency-groups].
124+
# See docs/how-tos/adding-a-matrix-dependency.rst for the full process.
125+
conflicts = [
126+
[{group = "test"}, {group = "django42"}],
127+
]
110128
# DO NOT EDIT constraint-dependencies DIRECTLY.
111129
# This list is managed by `edx_lint write_uv_constraints`
112130
# and will be overwritten the next time `make upgrade` is run.

backend/tox.ini

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
[tox]
2-
envlist = py{311,312}-django{52},docs,quality,pii_check
2+
envlist = py{311,312}-django{42,52},docs,quality,pii_check
3+
requires =
4+
tox-uv>=1
35

46
[doc8]
57
; D001 = Line too long
@@ -35,15 +37,18 @@ addopts = --cov sample_plugin --cov tests --cov-report term-missing --cov-report
3537
norecursedirs = .* docs requirements site-packages
3638

3739
[testenv]
38-
deps =
39-
django42: Django>=4.0,<5.0
40-
django52: Django>=5.0,<6.0
41-
-r{toxinidir}/requirements/test.txt
40+
runner = uv-venv-lock-runner
41+
dependency_groups =
42+
django42: django42
43+
django52: test
4244
commands =
4345
pytest {posargs}
4446
python manage.py check
4547

4648
[testenv:docs]
49+
runner = uv-venv-lock-runner
50+
dependency_groups =
51+
doc
4752
setenv =
4853
DJANGO_SETTINGS_MODULE = test_settings
4954
PYTHONPATH = {toxinidir}
@@ -52,8 +57,6 @@ setenv =
5257
allowlist_externals =
5358
make
5459
rm
55-
deps =
56-
-r{toxinidir}/requirements/doc.txt
5760
commands =
5861
doc8 --ignore-path docs/_build docs
5962
rm -f docs/sample_plugin.rst
@@ -64,12 +67,13 @@ commands =
6467
twine check dist/*
6568

6669
[testenv:quality]
70+
runner = uv-venv-lock-runner
71+
dependency_groups =
72+
quality
6773
allowlist_externals =
6874
make
6975
rm
7076
touch
71-
deps =
72-
-r{toxinidir}/requirements/quality.txt
7377
commands =
7478
touch tests/__init__.py
7579
pylint sample_plugin tests test_utils manage.py
@@ -80,10 +84,11 @@ commands =
8084
make selfcheck
8185

8286
[testenv:pii_check]
87+
runner = uv-venv-lock-runner
88+
dependency_groups =
89+
test
8390
setenv =
8491
DJANGO_SETTINGS_MODULE = test_settings
85-
deps =
86-
-r{toxinidir}/requirements/test.txt
8792
commands =
8893
code_annotations django_find_annotations --config_file .pii_annotations.yml --lint --report --coverage
8994

0 commit comments

Comments
 (0)