Both direnv and mise are tools that hardly need any introduction. If they do not ring any bells I highly recommend you read up on them. They are great at managing dependencies and general configuration for your projects. There are some caveats though that you need to be aware of when using them together. In this post I am going to show how I use these tools to manage python dependencies with uv, poetry and plain old pip.
Configuring dotenv Link to heading
I am going to start at the deep end here by going over the configuration needed for direnv
. We
are going to create new commands that we can use in our .envrc
files. We do that by adding
these two corresponding files in the $HOME/.config/direnv/lib
directory.
use_mise.sh Link to heading
Note that this is only needed if you are actually using the
.tool-versions
file in your project.
This script ensures that the mise
path is correctly set in direnv
. It also prints out any
tools that are currently missing. It is important that this command is always the first command
in our .envrc
file.
#!/usr/bin/env bash
use_mise() {
if ! has mise; then
log_error "bin \`mise\` not found"
return
fi
direnv_load mise direnv exec
IFS=$'\n'
for l in $(mise current 2>&1 | grep "not installed"); do
log_error "$l"
done
unset IFS
}
layout_uv.sh Link to heading
This script ensures that uv
installs required interpreters, sets up a virtual environment and
syncs dependencies to the environment. It also activates the virtual environment in your shell.
#!/usr/bin/env bash
layout_uv() {
UV=$(which uv || true)
if has mise; then
UV=$(mise which uv || true)
fi
if [ -z "$UV" ]; then
log_error "bin \`uv\` not found"
return
fi
VIRTUAL_ENV="$(pwd)/.venv"
export VIRTUAL_ENV
PYPROJECT_TOML="${PYPROJECT_TOML:-pyproject.toml}"
if [ ! -f "$PYPROJECT_TOML" ]; then
log_error "no \`$PYPROJECT_TOML\` found"
log_status "creating a \`$PYPROJECT_TOML\` with \`uv\`"
$UV init
fi
if [ ! -d "$VIRTUAL_ENV" ]; then
log_status "installing python interpreters with \`uv\`"
$UV python install -q
log_status "creating venv with \`uv\`"
$UV venv .venv -q
fi
log_status "syncing venv with \`uv\`"
$UV sync
PATH_add "$VIRTUAL_ENV/bin"
}
layout_poetry.sh Link to heading
This script ensures that poetry
has setup a virtual environment and has installed required
dependencies. It also automatically activates the virtual environment and informs you of any
problems with the environment.
#!/usr/bin/env bash
layout_poetry() {
POETRY=$(which poetry || true)
if has mise; then
POETRY=$(mise which poetry || true)
fi
if [ -z "$POETRY" ]; then
log_error "bin \`poetry\` not found"
return
fi
PYPROJECT_TOML="${PYPROJECT_TOML:-pyproject.toml}"
if [ ! -f "$PYPROJECT_TOML" ]; then
log_error "no \`$PYPROJECT_TOML\` found"
log_status "creating a \`$PYPROJECT_TOML\` with \`poetry\`"
touch README.md
$POETRY init -q
$POETRY install --no-root
fi
if [ -d ".venv" ]; then
VIRTUAL_ENV="$(pwd)/.venv"
else
VIRTUAL_ENV="$($POETRY env list --full-path | head -1 | awk '{print $1}')"
fi
if [ -z "$VIRTUAL_ENV" ] || [ ! -d "$VIRTUAL_ENV" ]; then
log_status "creating venv with \`poetry\`"
$POETRY install
VIRTUAL_ENV="$($POETRY env list --full-path | head -1 | awk '{print $1}')"
fi
if has mise; then
mise current python | sed 's/ /\n/g' | rg -q "$($POETRY run python -V | rg -o '\d+\.\d+\.\d+')" || {
log_error "python venv version and .tool-versions does not match"
}
fi
log_status "running poetry check"
$POETRY check --ansi >/dev/null || true
PATH_add "$VIRTUAL_ENV/bin"
export POETRY_ACTIVE=1
export VIRTUAL_ENV
}
layout_pip.sh Link to heading
This script ensures that a virtual environment is created and the requirements.txt
file is
installed with pip
. It also automatically activates the virtual environment.
#!/usr/bin/env bash
layout_pip() {
PYTHON=$(which python || true)
if has mise; then
PYTHON=$(mise which python || true)
fi
if [ -z "$PYTHON" ]; then
log_error "bin \`python\` not found"
return
fi
VIRTUAL_ENV="$(pwd)/.venv"
export VIRTUAL_ENV
REQ_TXT="${REQ_TXT:-requirements.txt}"
if [ ! -f "$REQ_TXT" ]; then
log_error "no \`$REQ_TXT\` found"
log_status "creating a \`$REQ_TXT\`"
touch $REQ_TXT
fi
BIN="$VIRTUAL_ENV/bin"
if [ ! -d "$VIRTUAL_ENV" ]; then
log_status "creating venv with \`python\`"
$PYTHON -m venv $VIRTUAL_ENV
log_status "installing requirements from \`$REQ_TXT\`"
$BIN/pip install -r $REQ_TXT
fi
if has mise; then
mise current python | sed 's/ /\n/g' | rg -q "$($BIN/python -V | rg -o '\d+\.\d+\.\d+')" || {
log_error "python venv version and .tool-versions does not match"
}
fi
PATH_add "$BIN"
}
}
Project with uv Link to heading
Now we have everything to setup our project.
# Create project dir and cd into it
$ mkdir myproject && cd myproject
# Create .tool-versions file
$ echo uv 0.4.17 > .tool-versions
# Install needed tools
$ mise install
mise all runtimes are installed
# Create .envrc file, remember that `use mise` should always be the first command
$ echo "use mise # Should always be first command in file" > .envrc && \
echo layout uv >> .envrc
# Allow .envrc file
$ direnv allow
direnv: loading /tmp/myproject/.envrc
direnv: using mise
direnv: no `pyproject.toml` found
direnv: creating a `pyproject.toml` with `uv`
Initialized project `myproject`
direnv: installing python interpreters with `uv`
direnv: creating venv with `uv`
direnv: syncing venv with `uv`
Resolved 1 package in 2ms
Audited in 0.00ms
direnv: export +VIRTUAL_ENV ~PATH
Now, everytime you open up this directory, direnv will whip up your environment 🎉 You can verify that the virtual environment is activated by running these commands.
# Exit and enter our project directory again
$ cd myproject/
direnv: loading /tmp/myproject/.envrc
direnv: using mise
direnv: syncing venv with `uv`
Resolved 1 package in 1ms
Audited in 0.01ms
direnv: export ~PATH
$ which python
/tmp/myproject/.venv/bin/python
Projects with poetry or pip Link to heading
The process above can be replicated for poetry
or pip
by using their respective layouts.