Question
Normally when I change the deps in my tox.ini file tox will notice the change and recreate the virtualenv with the new dependencies. But if I use deps = -r requirements.txt to read my dependencies from a requirements.txt file then tox doesn't update the virtualenv when requirements.txt changes. How can I automatically keep my tox virtualenvs in sync with my requirements.txt files?
Details
tox detects changes to deps in tox.ini
When using the deps setting in tox.ini to list your dependencies if you change the deps then the next time you run a tox command it'll notice the change and will create the virtualenv and install the new deps into the new virtualenv. Here's a minimal tox.ini file to demonstrate what I'm talking about:
# tox.ini
[tox]
skipsdist = true
[testenv]
deps = pytest
commands = pytest --version
If you run tox in a directory containing this tox.ini file then tox will create a virtualenv in the .tox directory, install pytest into that virtualenv, and run pytest --version in the virtualenv. If you run tox again it'll reuse the existing virtualenv and just run pytest --version without unnecessarily reinstalling pytest again. If you make a change to the deps in tox.ini, for example like this:
diff --git a/tox.ini b/tox.ini
index 7d92601..e45a612 100644
--- a/tox.ini
+++ b/tox.ini
@@ -2,5 +2,5 @@
skipsdist = true
[testenv]
-deps = pytest
-commands = pytest --version
+deps = pylint
+commands = pylint --version
Then the next time I run tox it'll recreate the virtualenv and reinstall the dependencies before running the commands:
$ tox
python recreate: /tmp/tox/.tox/python
python installdeps: pylint
python installed: astroid==2.12.4,dill==0.3.5.1,isort==5.10.1,lazy-object-proxy==1.7.1,mccabe==0.7.0,platformdirs==2.5.2,pylint==2.15.0,tomli==2.0.1,tomlkit==0.11.4,wrapt==1.14.1
python run-test-pre: PYTHONHASHSEED='4045882343'
python run-test: commands[0] | pylint --version
pylint 2.15.0
astroid 2.12.4
Python 3.10.4 (main, Jun 29 2022, 12:14:53) [GCC 11.2.0]
___________________________________ summary ____________________________________
python: commands succeeded
congratulations :)
You can put requirements files in deps
You can reference pip requirements files from tox.ini with -r, for example:
# tox.ini
[tox]
skipsdist = true
[testenv]
deps = -r requirements.txt
commands = pylint --version
Now when you run tox it'll install the dependencies from requirements.txt into the virtualenv.
The problem: tox doesn't detect changes to requirements.txt
But there's a problem: tox won't notice if the requirements.txt file changes. Your virtualenv will still contain the dependencies from the old version of the requirements.txt file. tox only notices direct changes to the deps setting in tox.ini itself.
Ideally you wouldn't want tox to recreate the virtualenv from scratch when requirements.txt changes like it does when tox.ini changes: requirements.txt files are often quite large and reinstalling them from scratch can take a long time. Ideally tox would update the virtualenv in place: installing, removing, updating and downgrading packages as necessary to synchronize the virtualenv with the requirements.txt file.
You can use the
pip-synccommand frompip-toolsto keep yourtoxvirtualenv synchronized with any changes to yourrequirements.txtfile:How this works:
-r requirements.txtfrom thetox.ini'sdeps: we're no longer usingtoxto install ourrequirements.txtfile.pip-toolsin thedeps. This meanstoxwill installpip-tools(the package that contains thepip-synccommand) whenever it creates a new virtualenv.pip-sync requirements.txtto thecommands_presetting intox.ini. Now every time we runtoxit will runpip-syncbefore running whatever is in thecommandssetting (pytest --versionin this example). Thispip-synccommand will have no effect if therequirements.txtfile hasn't changed, but ifrequirements.txthas changed then it'll update the virtualenv.Speeding it up with
pip-sync-fasterThere's one problem: running
pip-syncevery time you runtoxis slow, even when therequirements.txtfile hasn't changed and the virtualenv doesn't need to be updated. On my machine using a largerequirements.txtfile from a real app it takes about 1.5s to run a simple command likepytest --versionintox. For comparison a simpletox.inifile that doesn't callpip-sync(and therefore doesn't update the virtualenv whenrequirements.txtchanges) runspytest --versionin about 800ms.pip-sync-fasteris apip-syncwrapper script that can speed things up by doing a very fast check of whetherrequirements.txthas changed and only callingpip-syncif it has.You need to add
pip-sync-fasterto yourrequirements.txtfile, otherwise it'll uninstall itself! This is becausepip-sync-fastercallspip-syncwhich uninstalls any package that isn't inrequirements.txt, includingpip-sync-faster. Here's an example pinnedrequirements.txtfile containingpip-sync-faster,pytest, and their dependencies:With a
requirements.txtlike this you can now usepip-sync-fasterwith atox.inifile like this:With my real-world
requirements.txtrunningpytest --versionintoxnow takes about 850ms, a speed up of almost 600ms.The
deps = -r requirements.txtgetstoxto installpip-sync-fasterwhenever it creates a new virtualenv, thecommands_pre = pip-sync-faster requirements.txtthen installsrequirements.txtinto the virtualenv and updates the virtualenv ifrequirements.txtchanges, before running thecommands(remember:pip-sync-fasterneeds to be inrequirements.txtor it'll uninstall itself!)Speeding it up even more with
tox-fastertox-fasteris a littletoxplugin that can shave a few hundred more milliseconds off yourtoxstartup time (depending on the size of yourrequirements.txtfile). Just add it to therequiressetting in yourtox.inifile:With my app's
requirements.txtfile runningpytest --versionintoxnow takes about 650ms, another200msfaster.