Set up Python develop environments

- use `pyenv` to manage the various develop environments
  + install several Python binaries
  + each environment receives its own `poetry` install
- add two virtual environments:
  + "interactive" => default environment with no library,
                     which receives accidental `pip install`s
  + "utils" => hosts various globally available tools/apps
               (e.g., `mackup`)
- add installation and update scripts
This commit is contained in:
Alexander Hess 2022-07-18 21:54:29 +02:00
parent 5c3a914658
commit dcdb32585a
Signed by: alexander
GPG key ID: 344EA5AB10D868E0
8 changed files with 240 additions and 2 deletions

26
.bashrc
View file

@ -156,6 +156,13 @@ if ! shopt -oq posix; then
fi
# Enable completions for various tools
command_exists invoke && eval "$(invoke --print-completion-script=bash)"
command_exists nox && eval "$(register-python-argcomplete nox)"
command_exists pip && eval "$(pip completion --bash)"
command_exists pipx && eval "$(register-python-argcomplete pipx)"
command_exists poetry && eval "$(poetry completions bash)"
# ============
# Key Bindings
@ -234,5 +241,22 @@ _prompt_jobs() {
(( $(jobs -rp | wc -l) )) && echo -e "\033[0;32m %\033[0m"
}
PS1='${chroot:+($_debian_chroot)}\w$(_prompt_git)$(_prompt_jobs) > '
# Mimic zsh's pyenv/venv integration
_prompt_pyenv() {
if [ -n "$VIRTUAL_ENV" ]; then
echo -e "\033[0;36m py $(python -c "import os, sys; (hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix)) and print(os.path.basename(sys.prefix))")\033[0m"
elif [ -n "$PYENV_VERSION" ]; then
if [ "$PYENV_VERSION" != "system" ]; then
echo -e "\033[0;36m py $PYENV_VERSION\033[0m"
fi
elif [ -f "$(pwd)/.python-version" ]; then
echo -e "\033[0;36m py $(cat .python-version | sed ':a;N;$!ba;s/\n/:/g')\033[0m"
fi
}
# Disable the default prompts set by pyenv and venv
export PYENV_VIRTUALENV_DISABLE_PROMPT=1
export VIRTUAL_ENV_DISABLE_PROMPT=1
PS1='${chroot:+($_debian_chroot)}\w$(_prompt_git)$(_prompt_jobs)$(_prompt_pyenv) > '
PS2='... '

View file

@ -0,0 +1,3 @@
[virtualenvs]
create = true
in-project = true

View file

@ -80,6 +80,23 @@ alias more='less'
alias tree='tree -C --dirsfirst'
# Make working with Python more convenient
alias py='python'
alias ipy='ipython'
if command_exists poetry; then
alias pr='poetry run'
fi
if command_exists pyenv; then
alias pyvenvs='pyenv virtualenvs --bare --skip-aliases'
alias pyver='pyenv version'
alias pyvers='pyenv versions --skip-aliases'
alias pywhich='pyenv which'
fi
# Aliases for various utilities
alias datetime='date +"%Y-%m-%d %H:%M:%S %z (%Z)"'
alias datetime-iso='date --iso-8601=seconds'

View file

@ -20,7 +20,7 @@ git clone --bare git@git.webartifex.biz:alexander/dotfiles.git "$HOME/.dotfiles"
# Backup old dotfiles
rm -rf "$HOME/.dotfiles.bak" >/dev/null
mkdir -p $HOME/.dotfiles.bak/.config/{bat,flameshot,git,Nextcloud,pop-system-updater,psql,shell} && \
mkdir -p $HOME/.dotfiles.bak/.config/{bat,flameshot,git,Nextcloud,pop-system-updater,psql,pypoetry,shell} && \
mkdir -p $HOME/.dotfiles.bak/.vim/{after/ftplugin,backup,swap,undo} && \
/usr/bin/git --git-dir=$HOME/.dotfiles/ --work-tree=$HOME checkout 2>&1 | egrep "\s+\." | awk {'print $1'} | \
xargs -I{} mv {} "$HOME/.dotfiles.bak"/{}

View file

@ -12,6 +12,16 @@ in_zsh() {
[ -n "$ZSH_VERSION" ]
}
# Prepend a folder to $PATH if it is not already there
_prepend_to_path () {
if [ -d "$1" ] ; then
case :$PATH: in
*:$1:*) ;;
*) PATH=$1:$PATH ;;
esac
fi
}
# =========================
@ -30,6 +40,13 @@ fi
command_exists lesspipe && eval "$(SHELL=/bin/sh lesspipe)"
# Initialize pyenv if it is installed
if command_exists pyenv; then
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
fi
# Configure the keyboard:
# - make right alt and menu keys the compose key, e.g., for umlauts
# - make caps lock a ctrl modifier and Esc key
@ -38,6 +55,32 @@ command_exists xcape && xcape -e "Caps_Lock=Escape"
# ==========================
# Command not found handlers
# ==========================
# Check if an unknown command is in a local Python venv
command_not_found_handle() {
if [ -x ".venv/bin/$1" ]; then
echo 'You forgot to activate the virtualenv' 1>&2
exe=".venv/bin/$1"
shift
"$exe" "$@"
return $?
else
echo "$1: command not found" 1>&2
return 127
fi
}
# zsh uses another name for the handler
command_not_found_handler() {
command_not_found_handle "$@"
}
# ==============================
# Working with files and folders
# ==============================
@ -194,6 +237,89 @@ genemail() {
# ===================================================
# Set up & maintain the Python (develop) environments
# ===================================================
# TODO: This needs to be updated regularly (or find an automated solution)
# The Python versions `pyenv` creates (in descending order)
_py3_versions=('3.10.5' '3.9.13' '3.8.13' '3.7.13')
_py2_version='2.7.18'
# Each Python environment uses its own `poetry` installation to avoid
# integration problems between `pyenv` and `poetry`
# Source: https://github.com/python-poetry/poetry/issues/5252#issuecomment-1055697424
_py3_site_packages=('poetry')
# The pyenv virtualenv "utils" contains some globally available tools (e.g., `mackup`)
_py3_utils=('mackup')
install-pyenv() {
echo -e "\nInstalling pyenv\n"
# The official installer does a bit more than the `git clone`s below
# `curl https://pyenv.run | bash`
git clone https://github.com/pyenv/pyenv.git "$HOME/.pyenv"
git clone https://github.com/pyenv/pyenv-doctor.git "$HOME/.pyenv/plugins/pyenv-doctor"
git clone https://github.com/pyenv/pyenv-update.git "$HOME/.pyenv/plugins/pyenv-update"
git clone https://github.com/pyenv/pyenv-virtualenv.git "$HOME/.pyenv/plugins/pyenv-virtualenv"
git clone https://github.com/pyenv/pyenv-which-ext.git "$HOME/.pyenv/plugins/pyenv-which-ext"
# On a first install, "$PYENV_ROOT/bin" is NOT on the $PATH
_prepend_to_path "$PYENV_ROOT/bin"
}
re-install-pyenv() {
echo -e "\nRemoving pyenv\n"
rm -rf "$HOME/.pyenv" >/dev/null
install-pyenv
}
create-or-update-python-envs() {
command_exists pyenv || install-pyenv
eval "$(pyenv init --path)"
# Keep a legacy Python 2.7, just in case
echo -e "\nInstalling/updating Python $_py2_version\n"
pyenv install --skip-existing $_py2_version
PYENV_VERSION=$_py2_version pip install --upgrade pip setuptools
PYENV_VERSION=$_py2_version python -c "import sys; print sys.version"
for version in ${_py3_versions[@]}; do
echo -e "\nInstalling/updating Python $version\n"
pyenv install --skip-existing $version
# Start the new environment with the latest `pip` and `setuptools` versions
PYENV_VERSION=$version pip install --upgrade pip setuptools
# Put the specified utilities in the fresh environments (or update them)
for lib in ${_py3_site_packages[@]}; do
PYENV_VERSION=$version pip install --upgrade $lib
done
done
# Create a virtualenv based off the latest Python version to host global utilities
echo -e "\nInstalling/updating global Python utilities\n"
pyenv virtualenv $_py3_versions[1] 'utils'
PYENV_VERSION='utils' pip install --upgrade pip setuptools
for util in ${_py3_utils[@]}; do
PYENV_VERSION='utils' pip install --upgrade $util
done
# Create a virtualenv based off the latest Python version for interactive usage
# (This virtualenv is empty and is the target of accidental `pip install`s)
echo -e "\nInstalling/updating the default/interactive Python environment\n"
pyenv virtualenv $_py3_versions[1] 'interactive'
PYENV_VERSION='interactive' pip install --upgrade pip setuptools
# Put all Python binaries and the utilities on the $PATH
pyenv global 'interactive' $_py3_versions 'utils' $_py2_version
}
# =============================
# Automate the update machinery
# =============================
@ -278,6 +404,28 @@ update-zsh() {
}
# Update the entire Python tool chain
update-python() {
echo -e '\nUpdating the Python tool chain\n'
if command_exists pyenv; then
echo -e '\nUpdating pyenv\n'
pyenv update
echo
echo -e '\nUpdating Python environments\n'
create-or-update-python-envs
echo
fi
if command_exists zsh-pip-cache-packages; then
echo -e '\nUpdating pip packages cache\n'
zsh-pip-clear-cache
zsh-pip-cache-packages
fi
}
# Wrapper to run several update functions at once
update-machine() {
sudo --validate || return
@ -295,9 +443,14 @@ update-machine() {
remove-old-snaps
fi
update-python
update-dotfiles
update-zsh
echo -e '\nUpdating the configs managed by mackup'
mackup restore --force
echo -e '\nUpdating password store\n'
pass git pull
echo

View file

@ -34,6 +34,11 @@ export BAT_CONFIG_PATH="$HOME/.config/bat/config"
export LESSHISTFILE="${XDG_CACHE_HOME:-$HOME/.cache}/.lesshst"
export PYENV_ROOT="$HOME/.pyenv"
_prepend_to_path "$PYENV_ROOT/bin"
# No need for *.pyc files on a dev machine
export PYTHONDONTWRITEBYTECODE=1
export PSQLRC="$HOME/.psqlrc"

18
.zshrc
View file

@ -102,7 +102,10 @@ zplug "plugins/command-not-found", from:oh-my-zsh
zplug "plugins/dotenv", from:oh-my-zsh
zplug "plugins/dirhistory", from:oh-my-zsh
zplug "plugins/git-escape-magic", from:oh-my-zsh
zplug "plugins/invoke", from:oh-my-zsh # completions for `invoke`
zplug "plugins/jsontools", from:oh-my-zsh
zplug "plugins/pip", from:oh-my-zsh # completions for `pip`
zplug "plugins/poetry", from:oh-my-zsh # completions for `poetry`
zplug "plugins/z", from:oh-my-zsh
zplug "romkatv/powerlevel10k", as:theme, depth:1
@ -141,6 +144,21 @@ zstyle ':completion:*:warnings' format 'No matches for: %d'
zstyle ':completion:*' group-name ''
# Enable completions for various tools
# invoke -> see plugins above
# command_exists invoke && eval "$(invoke --print-completion-script=zsh)"
command_exists nox && eval "$(register-python-argcomplete nox)"
# pip -> see plugins above
# command_exists pip && eval "$(pip completion --zsh)"
command_exists pipx && eval "$(register-python-argcomplete pipx)"
# poetry -> see plugins above
# ============
# Key Bindings

View file

@ -23,3 +23,21 @@ Furthermore, `zsh` is set up with [`oh-my-zsh`](https://ohmyz.sh/) and `zplug`.
Otherwise, `~/.profile` is probably *not* sourced.
Don't worry: Your current dotfiles are backed up in the `~/.dotfiles.bak` folder!
### Python Development Environments
The develop environments for Python are managed by [`pyenv`](https://github.com/pyenv/pyenv).
To set them up, run:
```bash
install-pyenv && create-or-update-python-envs
```
Several Python binaries are installed.
Additionally, two `virtualenv`s, "interactive" and "utils", are also created:
- "interactive" is the default environment with *no* libraries installed, and
- "utils" hosts globally available utilities.
Use `pyenv local ...` to specify a particular Python binary for a project.