Inleiding
In deze blog deel ik mijn zsh-configuratie om efficiënt virtuele omgevingen en Python-projecten te beheren. Het zijn in de basis een aantal shell functions en aliases die het beheer en het managen van virtuele omgevingen vergemakkelijken. Ook voorkomt bovendien dat je moet werken met fragiele constructies aangaande import statements en filepaths. Hierbij gebruik ik niet al te veel externe tools of oplossingen om vooral zelf te kunnen begrijpen wat er gebeurt in de code.
Wat context vooraf
Mijn basis zshell-configuratie past de volgende principes toe:
- Alle pad-modificaties plaats ik in
.zshenv
- De overige configuraties komen in
.zshrc
.zprofile
gebruik ik momenteel niet
In de .zshrc
laad ik verschillende bestanden en plugins:
aliases.zsh
: voor het definiëren van handige aliassenfunctions.zsh
: voor het opslaan van aangepaste functies- Diverse plugins voor extra functionaliteit:
source ~/.zsh_plugins/zsh-autosuggestions/zsh-autosuggestions.zsh
source ~/.zsh_plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh
source ~/.zsh_plugins/zsh-history-substring-search/zsh-history-substring-search.zsh
source <(fzf --zsh)
Deze structuur zorgt voor een overzichtelijke en modulaire configuratie, waardoor het gemakkelijk is om specifieke onderdelen aan te passen of uit te breiden. om de shell configuratie goed te kunnen begrijpen gebruik ik geen tools zoals oh my zsh, die zonder dat ik er van weet allemaal extra aliases instellen.
Een paar laatste disclaimers:
- Een deel van dit script heb ik uit een andere blog, alleen ik kan het niet weer vinden. Veel van de functies komen echter overeen met die in virtualenvwrapper, dus mijn vermoeden is dat het uit die hoek komt.
- Ik gebruik een MacBook met zsh en installeer dingen primair met Homebrew. Het is mogelijk dat sommige functionaliteiten alleen in deze setup werken.
Nu de basis van mijn shell-configuratie duidelijk is, wil ik verder ingaan op hoe ik mijn virtuele omgevingen en Python-projecten beheer.
Deze beheer ik via venv-settings.zsh
*.
- die ik dus als volgt in mijn .zshrc toevoeg:
source "$HOME/dotfiles/zsh/venv-settings.zsh"
Deel 1: shell configuratie voor het aanmaken en beheren van virtuele omgevingen en python projecten
TL;DR: mijn volledige venv-settings.zsh
is hier te vinden.
Basisinstellingen
export PROJECTS_HOME="$HOME/Developer"
export VENV_HOME="$HOME/.venv"
[ -d "$VENV_HOME" ] || mkdir $VENV_HOME
Deze regels stellen twee belangrijke paden in:
PROJECTS_HOME
: Hier worden al je projectmappen opgeslagen.VENV_HOME
: Hier worden je virtuele omgevingen opgeslagen.
Het script controleert ook of de VENV_HOME
directory bestaat en maakt deze aan, mocht dat niet zo zijn. Elk nieuw development project dat ik start, krijgt een virtuele omgeving met dezelfde naam in de VENV_HOME folder. (.venv in de homefolder in mijn configuratie)
De mkproject
functie
function mkproject() {
if [ $# -eq 0 ]; then
echo "ERROR: Project name not specified. Please specify the name of the project to create."
return 1
fi
local env_name=$1
shift
local python_version="python3.12"
for arg in "$@"; do
if [[ $arg == --python=* ]]; then
python_version="python${arg#*=}"
fi
done
local python_path
if [[ $python_version == python3.12 ]]; then
python_path="/opt/homebrew/bin/python3.12"
else
python_path=$(which $python_version)
fi
if [ ! -x "$python_path" ]; then
echo "ERROR: Python executable for $python_version not found."
return 1
fi
local venv_path=$VENV_HOME/$env_name
if [ -d "$venv_path" ]; then
echo "ERROR: Environment '$env_name' already exists. Switch to it with 'workon $env_name'."
return 1
fi
echo "Creating environment '$env_name' using $python_version at $venv_path"
$python_path -m venv $venv_path
source $venv_path/bin/activate
[ -d "$PROJECTS_HOME/$env_name" ] || mkdir $PROJECTS_HOME/$env_name
cd $PROJECTS_HOME/$env_name
}
Deze functie is de kern van mijn workflow. Het doet het volgende:
- Maakt een nieuwe virtuele omgeving aan in
$VENV_HOME
. - Activeert deze omgeving.
- Maakt een nieuwe projectmap aan in
$PROJECTS_HOME
. - Navigeert naar deze nieuwe projectmap.
Je kunt het gebruiken met:
mkproject mijn_nieuw_project
Of als je een specifieke Python-versie wilt gebruiken:
mkproject mijn_nieuw_project --python=3.11
Deze functie maakt snel een nieuwe virtuele omgeving aan en plaatst je direct in de bijbehorende projectmap. Bovendien komen alle virtuele omgevingen bij elkaar te staan, wat handig is bij het opschonen van oude virtuele omgevingen.
De workon
functie
function workon() {
if [ $# -eq 0 ]; then
\ls -1 $VENV_HOME
return 0
fi
local env_name=$1
VENV=$VENV_HOME/$env_name/bin/activate
if [ -f $VENV ]; then
if [ -n "$VIRTUAL_ENV" ]; then
deactivate
fi
source $VENV
cd $PROJECTS_HOME/$env_name
else
echo "ERROR: Environment '$1' does not exist. Create it with 'mkproject $1'."
fi
}
Deze functie maakt het gemakkelijk om tussen projecten te schakelen. Als je workon
zonder argumenten gebruikt, toont het een lijst van alle beschikbare virtuele omgevingen. Met een argument activeert het de gespecificeerde omgeving en navigeert naar de bijbehorende projectmap.
Gebruik het zo:
workon mijn_nieuw_project
De rmvirtualenv
functie
function rmvirtualenv() {
if [ -n "$VIRTUAL_ENV" ]; then
echo "Please deactivate the current venv before removing a venv."
return 1
fi
if [ $# -eq 0 ]; then
echo "Please specify an environment"
return 1
fi
echo "Removing $1..."
rm -r $VENV_HOME/$1
}
Deze functie verwijdert een virtuele omgeving.
Zsh Completion Function voor Virtuele Omgevingen
function _virtualenvs() {
local -a subcmds
_alternative "pids:process ID:($(\ls -1 $VENV_HOME))"
}
compdef _virtualenvs workon
compdef _virtualenvs rmvirtualenv
- De functie maakt een lijst van alle directories in
$VENV_HOME
met behulp van\ls -1 $VENV_HOME
. - Deze lijst wordt gebruikt als de mogelijke completions voor de commando’s.
- De
compdef
regels koppelen deze completion functie aan deworkon
enrmvirtualenv
commando’s.
Dit betekent dat wanneer je workon
of rmvirtualenv
typt gevolgd door een tab, zsh een lijst zal tonen van alle beschikbare virtuele omgevingen, wat het gebruik van deze commando’s veel gemakkelijker maakt.
De terminateproject
functie
function terminateproject() {
if [ -z "$VIRTUAL_ENV" ]; then
echo "ERROR: No active virtual environment detected."
return 1
fi
local env_name=$(basename "$VIRTUAL_ENV")
local project_path="$PROJECTS_HOME/$env_name"
local venv_path="$VENV_HOME/$env_name"
echo "WARNING: This will deactivate and remove the current virtual environment '$env_name'"
if [ -d "$project_path" ]; then
echo " and delete the project directory at '$project_path'."
else
echo " (Note: Project directory '$project_path' does not exist)"
fi
echo " This action cannot be undone."
read "response?Are you sure you want to proceed? (y/N) "
if [[ "$response" =~ ^[Yy]$ ]]; then
echo "Deactivating virtual environment..."
deactivate
echo "Removing virtual environment..."
rm -rf "$venv_path"
if [ -d "$project_path" ]; then
echo "Removing project directory..."
rm -rf "$project_path"
else
echo "Project directory not found. Skipping removal."
fi
echo "Project '$env_name' has been terminated."
else
echo "Operation cancelled."
fi
}
Deze functie is een krachtige tool om een project volledig op te ruimen. Het deactiveert de virtuele omgeving en verwijdert zowel de omgeving als de projectmap. Omdat dit een vrij drastische actie is word je wel eerst om een bevestiging gevraagd.
Bovenstaande functies maken het mogelijk om snel en gemakkelijk te schakelen tussen virtuele omgevingen en projecten en deze aan te maken of te verwijderen. Daarbij gebruiken we de standaard venv package van Python, wat alles transparant en duidelijk maakt.
Deel 2: Configuratie voor de initiële inrichting van een Python project
Mijn venv-settings.zsh
bevat nog 2 extra functies, maar om die goed te begrijpen wil ik eerst even kort de inhoud van developer/python-boilerplate-base
laten zien. In deze map heb ik een leeg template voor een Python project aangemaakt:
.
├── LICENSE
├── README.md
├── .env
├── .gitignore
├── pyproject.toml
└── src
└── projectname
├── __init__.py
└── main.py
3 directories, 7 files
pyproject.toml
Zoals je ziet, gebruik ik een pyproject.toml
bestand. Het grote voordeel hiervan vind ik dat het de import statements vergemakkelijkt. Als je projecten groter worden, is het gebruikelijk om te stoeien met relatieve en absolute imports. Dit kan bijvoorbeeld misgaan als je bestanden diep in een projectstructuur wilt importeren en je lange of ingewikkelde paden moet gebruiken, zoals from ../../module/submodule import X
. Dit leidt vaak tot verwarring en fouten, vooral als je code buiten de hoofdmap van je project wordt uitgevoerd, zoals in testomgevingen of scripts.
Veelgebruikte oplossingen, zoals het handmatig aanpassen van de sys.path
om modules te kunnen vinden, zijn fragiel en leiden tot moeilijk onderhoudbare code. Een correcte projectstructuur met een pyproject.toml
voorkomt dit, omdat het pad voor je modules consistent blijft, ongeacht waar je het project vandaan draait.
Mijn pyproject.toml
ziet er zo uit:
[build-system]
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "{{projectname}}"
version = "0.0.1"
authors = [
{name = "Jan Willem Altink", email = "janwillem@janwillemaltink.eu"}
]
readme = "README.md"
requires-python = ">=3.10"
license = {file = "LICENSE"}
dependencies = [
"python-dotenv",
]
[tool.setuptools]
package-dir = {"" = "src"}
[tool.setuptools.packages.find]
where = ["src"]
Het laatste deel van mijn zsh configuratie:
BOILERPLATE_DIR="$HOME/developer/python-boilerplate-base"
function replace_placeholders() {
local file="$1"
local project_name="$2"
sed -i '' \
-e "s/{{projectname}}/$project_name/g" \
-e "s/{{CURRENT_YEAR}}/$(date +%Y)/g" \
-e "s/{{CURRENT_DATE}}/$(date +%Y-%m-%d)/g" \
"$file"
}
function mkprojectsetup() {
# Check if we're in a virtual environment
if [ -z "$VIRTUAL_ENV" ]; then
echo "ERROR: No active virtual environment detected. Please activate one first."
return 1
fi
local project_name=$(basename "$VIRTUAL_ENV")
if [ ! -d "$BOILERPLATE_DIR" ]; then
echo "ERROR: Boilerplate directory not found at $BOILERPLATE_DIR"
return 1
fi
cp -R "$BOILERPLATE_DIR"/. .
mv src/projectname "src/$project_name"
replace_placeholders "pyproject.toml" "$project_name"
replace_placeholders "README.md" "$project_name"
echo "Project structure for '$project_name' has been created based on the boilerplate."
echo "Installing the package in editable mode..."
pip install -e .
echo "Project '$project_name' has been set up and installed in editable mode."
}
Dit deel van het script doet het volgende:
- Kopieert de inhoud van mijn boilerplate map naar de al eerder aangemaakte projectmap.
- Vervangt de placeholders in de juiste projectnaam/data velden.
- Installeert het boilerplate project in editable mode als een package, wat de volgende voordelen heeft:
Installeren als package
Het installeren als package zorgt ervoor dat je project als een module kan worden geïmporteerd binnen je virtuele omgeving. Dit voorkomt fouten met imports, omdat Python direct begrijpt waar de modules zich bevinden, zonder dat je handmatig paden hoeft aan te passen.
Editable mode
In editable mode wordt je project zo geïnstalleerd dat je wijzigingen direct beschikbaar zijn zonder het opnieuw te moeten installeren. Dit is handig tijdens de ontwikkeling, omdat je continu aanpassingen kunt maken zonder dat je een installatiestap hoeft te herhalen. Elke wijziging in de bronbestanden wordt direct doorgevoerd.
Conclusie
Het opzetten van mijn Python projecten doe ik eigenlijk primair met 2 commando’s:
mkproject
: Dit maakt een virtuele omgeving aan, activeert deze, en maakt een projectmap.mkprojectsetup
: Dit kopieert de boilerplate naar de projectmap, past de naam en datums aan, en installeert het project in editable mode.
Voor mij werkt dit makkelijk, terwijl het een opgeruimde en nette structuur oplevert. Bovendien is debuggen eenvoudig, omdat de projectstructuur consistent blijft en importproblemen worden vermeden. Mocht je opmerkingen, ideeen of verbetersuggesties hebben op mijn werkwijze, hoor ik het heel graag.