Switching from using Tox to Just
I became aware of just
while watching Hynek’s
second video on uv
a
few months ago. I immediately fell in love with its elegance and simplicity, so
I have begun replacing task running in my repositories that relied on
tox
with just
. This post gives a bit of
background, context, and walks through making the switch on one of my
repositories that has some annoying dependencies.
What is Tox
tox
is a tool tailored for developing Python
packages. It takes care of creating a virtual environment for the configuration
then installing the current package with the specified extras and dependency
groups. Here’s what my testing configuration in the
ssslm
package looked like:
[testenv]
description = Run unit and integration tests.
commands =
coverage run -p -m pytest --durations=20 {posargs:tests}
coverage combine
coverage xml
extras =
gilda-slim
web
scispacy
gliner
rdflib
ontology
pandas
dependency_groups =
tests
en-core-sci-sm
This can be run with tox -e py
after installing tox (e.g., uvx tox -e py
or
uvx --with tox-uv tox -e py
to enable virtual environment creation with uv)
Tox can actually do a lot more things than this, including skipping installing
the current repository as a Python package to instead install/run development
tools like ruff
. It also has the ability to refer from the parts of one
configuration to another, which is useful for having different flavors of the
same command (e.g., documentation build vs. documentation test).
Check the Tox documentation or see the tox.ini
file in SSSLM v0.1.2, the last
version before I switched from Tox to Just:
https://github.com/cthoyt/ssslm/blob/v0.1.2/tox.ini.
What is Just
just
is a more generic tool for writing
tasks, which is a much better fit than Makefiles - read the Just homepage for
excellent arguments which I won’t recapitulate here. Here’s what the previous
Tox configuration looks using Just’s custom syntax in a file called justfile
,
this time using uv to do the heavy lifting:
[doc("run unit and integration tests")]
test:
just coverage erase
uv run --group tests --all-extras --no-extra scispacy --no-extra gilda -m coverage run -p -m pytest
just coverage combine
just coverage xml
[doc("run `coverage` with a given subcommand")]
@coverage command:
uvx --from coverage[toml] coverage {{command}}
This can be run with just test
after installing just (e.g.,
uvx --from rust-just just test
).
Note that uv
is now doing the heavy lifting for environment management and
installation instead of Tox, and Just simply ties it all together.
Making the Switch
I finally got around to testing replacing running my tests with tox with running my tests with just in cthoyt/ssslm#32. I did this because:
tox
is feeling very slow these days. Thetox-uv
extension was a nice respite, but it only usesuv
for virtual environment construction (iirc) and doesn’t take advantage of the other fast parts of uv.tox
creates a wastefully large cache in every projectuvx
is a more fit-for-purpose solution for many of the things I usetox
for, i.e., installing and running a tool in an isolated virtual environment
I had to start with two other PRs to SSSLM (cthoyt/ssslm#31 and cthoyt/ssslm#33) that did some restructuring of tests and the way dependencies were declared such that it would even be possible to run tests without all extras/groups installed.
Then, I had to look into the issue caused by ScispaCy, which is running based on notoriously old dependencies (only works on Python <= 3.12 and with NumPy < 2.0). FYI, I’m not just complaining, I’ve been making upstream PRs to their repository to help get to broader compatibility.
The biggest problem with old NumPy dependencies is they’re often either 1) not available as a wheel or 2) difficult to compile in an automated setting.
One solution is to use the tool.uv.conflicts
configuration to say that the
pandas
and scispacy
extras can’t be installed at the same time because their
(transitive) dependencies have conflicts. Don’t confuse these names with the
packages - the extras include a list of related things that are defined in the
project.optional-dependencies
block of my project configuration.
Ideally, I wanted my test configuration in my justfile
to have two
back-to-back calls to uv run
like this:
$ uv run --group tests --all-extras --no-extra scispacy --no-extra gilda -m coverage run -p -m pytest
$ uv run --group tests --group en-core-sci-sm --extra scispacy --extra ontology -m coverage run -p -m pytest
The first command runs most tests, and the second one just installs the scispacy
extras and runs that. However, this didn’t work (it didn’t seem to manage to
install the scispacy
extra). I think there is a solution for this, but I am
still learning about the nuances of uv run
and uv’s notion of locking.
I tabled getting ScispaCy tests for SSSLM to work for now, since this is not a generic concern of most packages. The next steps are to test replacing all Tox environments with corresponding just commands in SSSLM, then upstream this to my cookiecutter template https://github.com/cthoyt/cookiecutter-snekpack so all of my repositories can benefit.
What We Really Want
It would be great if uv
had a built-in notion of task definitions, since most
of the things I had in my tox
configuration (and now justfile
) are calls to
install Python environments or run Python things inside them (this is true for
testing, linting, documentation building, publishing, etc.).
There’s a long-standing issue on their tracker
astral-sh/uv#5903 that I’m sure
will be addressed in the future, when they’ve made an excellent design for the
developer experience. I’m looking forward to the future where I can write a
follow-up post entitled Switching from just
+ uv
to just uv
(wordplay
intended).