From 0319e614b873b8ae4bd8adf9e858d4cbb06067bb Mon Sep 17 00:00:00 2001 From: Alexander Hess Date: Tue, 9 Aug 2022 13:53:43 +0200 Subject: [PATCH] Configure Python develop tool chain - use pyenv to manage the develop environments + install several Python versions (3.7 - 3.10 and 2.7) + each version receives its own copies of black, pipenv, and poetry - add two more virtual environments based off the latest version: + "interactive" => default environment optimized for interactive usage with with black, bpython, and ipython (also receives accidental `pip install`s) + "utils" => hosts various globally available tools/apps (e.g., mackup and youtube-dl) - add installation and update scripts for the entire tool chain - set up completions for bash and zsh - set up convenient aliases - configure bpython - configure poetry --- .bashrc | 31 ++++++++- .config/bpython/config | 7 ++ .config/git/ignore | 3 + .config/pypoetry/config.toml | 3 + .config/shell/aliases.sh | 19 ++++++ .config/shell/utils.sh | 123 +++++++++++++++++++++++++++++++++++ .profile | 5 ++ .zshrc | 24 +++++++ README.md | 19 ++++++ 9 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 .config/bpython/config create mode 100644 .config/pypoetry/config.toml diff --git a/.bashrc b/.bashrc index c967a32..fb768d7 100644 --- a/.bashrc +++ b/.bashrc @@ -5,6 +5,12 @@ [[ $- != *i* ]] && return +# Check if a command can be found on the $PATH +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + + # Enable XON/XOFF software flow control stty -ixon @@ -69,6 +75,13 @@ if ! shopt -oq posix; then fi 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)" + # Add tab completion for all aliases to commands with completion functions # (must come after bash completions have been set up) # Source: https://superuser.com/a/437508 @@ -193,5 +206,21 @@ _prompt_jobs() { # Indicate running background jobs with a"%" (( $(jobs -rp | wc -l) )) && printf "\033[0;32m %\033[0m" } -PS1='${chroot:+($_debian_chroot)}\w$(_prompt_git)$(_prompt_jobs) > ' +_prompt_pyenv() { # Mimic zsh's pyenv/venv integration + 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='... ' diff --git a/.config/bpython/config b/.config/bpython/config new file mode 100644 index 0000000..e554a04 --- /dev/null +++ b/.config/bpython/config @@ -0,0 +1,7 @@ +[general] + +# Make `bpython` and `python` share their history +hist_file = ~/.python_history + +# No limit +hist_length = 0 diff --git a/.config/git/ignore b/.config/git/ignore index c70f233..bac19a4 100644 --- a/.config/git/ignore +++ b/.config/git/ignore @@ -1,3 +1,6 @@ +# pyenv +.python-version + # Vim # Source: https://github.com/github/gitignore/blob/main/Global/Vim.gitignore # diff --git a/.config/pypoetry/config.toml b/.config/pypoetry/config.toml new file mode 100644 index 0000000..53b35d3 --- /dev/null +++ b/.config/pypoetry/config.toml @@ -0,0 +1,3 @@ +[virtualenvs] +create = true +in-project = true diff --git a/.config/shell/aliases.sh b/.config/shell/aliases.sh index b29027a..322714e 100644 --- a/.config/shell/aliases.sh +++ b/.config/shell/aliases.sh @@ -86,6 +86,25 @@ alias more='less' alias tree='tree -C --dirsfirst' +# Make working with Python more convenient + +# Interactive shells +alias py='python' +alias bpy='bpython' +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 + + # Various one-line utilities alias datetime='date +"%Y-%m-%d %H:%M:%S %z (%Z)"' alias datetime-iso='date --iso-8601=seconds' diff --git a/.config/shell/utils.sh b/.config/shell/utils.sh index 3ce16ef..381f765 100644 --- a/.config/shell/utils.sh +++ b/.config/shell/utils.sh @@ -9,6 +9,15 @@ _in_zsh() { [ -n "$ZSH_VERSION" ] } +prepend-to-path () { # if not already there + if [ -d "$1" ] ; then + case :$PATH: in + *:$1:*) ;; + *) PATH=$1:$PATH ;; + esac + fi +} + # Configure the keyboard: @@ -19,6 +28,16 @@ _command_exists xcape && xcape -e "Caps_Lock=Escape" +_init_pyenv () { # used further below as well + _command_exists pyenv || return + + eval "$(pyenv init -)" + eval "$(pyenv virtualenv-init -)" +} +_init_pyenv + + + # ============================== # Working with files and folders # ============================== @@ -174,6 +193,94 @@ 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 +# Important: The first version also holds the "interactive" and "utils" environments) +_py3_versions=('3.10.6' '3.9.13' '3.8.13' '3.7.13') +_py2_version='2.7.18' + +# Each Python version receives its own copy of black, pipenv, and poetry +# (e.g., to avoid possible integration problems between pyenv and poetry +# Source: https://github.com/python-poetry/poetry/issues/5252#issuecomment-1055697424) +_py3_site_packages=('black' 'pipenv' 'poetry') + +# The pyenv virtualenv "utils" contains some globally available tools (e.g., mackup) +_py3_utils=('mackup' 'youtube-dl') + +# Important: this REMOVES the old ~/.pyenv installation +_install_pyenv() { + echo "(Re-)Installing pyenv" + + # Ensure that pyenv is on the $PATH + # (otherwise, the pyenv installer emits warnings) + mkdir -p "$PYENV_ROOT/bin" + prepend-to-path "$PYENV_ROOT/bin" + + # Remove old pyenv for clean install + rm -rf "$PYENV_ROOT" >/dev/null + + # Run the official pyenv installer + curl https://pyenv.run | bash + + # Make pyenv usable after this installation in the same shell session + _init_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 "Installing/updating Python $_py2_version" + pyenv install --skip-existing $_py2_version + pyenv rehash # needed on a first install + 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 "Installing/updating Python $version" + pyenv install --skip-existing $version + pyenv rehash # needed on a first install + + # Start the new environment with the latest pip and setuptools versions + PYENV_VERSION=$version pip install --upgrade pip setuptools + PYENV_VERSION=$version python -c "import sys; print(sys.version)" + + # 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 "Installing/updating the global Python utilities" + pyenv virtualenv $_py3_versions[1] 'utils' + pyenv rehash # needed on a first install + 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 + echo "Installing/updating the default/interactive Python environment" + pyenv virtualenv $_py3_versions[1] 'interactive' + pyenv rehash # needed on a first install + PYENV_VERSION='interactive' pip install --upgrade pip setuptools + # Install some tools to make interactive usage nicer + PYENV_VERSION='interactive' pip install --upgrade black bpython ipython + + # Put all Python binaries/virtualenvs and the utilities on the $PATH + pyenv global 'interactive' $_py3_versions 'utils' $_py2_version +} + + + # ============================= # Automate the update machinery # ============================= @@ -280,6 +387,21 @@ _update_zplug() { } +_update_python() { + echo 'Updating the Python tool chain' + + if _command_exists pyenv; then + pyenv update + create-or-update-python-envs + fi + + if _command_exists zsh-pip-cache-packages; then + zsh-pip-clear-cache + zsh-pip-cache-packages + fi +} + + run-private-scripts() { # in the Nextcloud if [ -d "$HOME/data/getraenkemarkt/shell" ]; then for file in $HOME/data/getraenkemarkt/shell/*.sh; do @@ -298,6 +420,7 @@ update-machine() { _command_exists snap && sudo snap refresh && _remove_old_snaps _update_repositories _update_zsh + _update_python sudo --reset-timestamp } diff --git a/.profile b/.profile index 18d3a97..53d759c 100644 --- a/.profile +++ b/.profile @@ -11,6 +11,10 @@ export REPOS="$HOME/repos" export LESSHISTFILE="$HOME/.lesshst" +export PYENV_ROOT="$HOME/.pyenv" +# No need for *.pyc files on a dev machine +export PYTHONDONTWRITEBYTECODE=1 + export PSQLRC="$HOME/.psqlrc" @@ -26,6 +30,7 @@ prepend-to-path () { # if not already there prepend-to-path "$HOME/bin" prepend-to-path "$HOME/.local/bin" +prepend-to-path "$PYENV_ROOT/bin" diff --git a/.zshrc b/.zshrc index 517b184..1a9992d 100644 --- a/.zshrc +++ b/.zshrc @@ -5,6 +5,12 @@ [[ $- != *i* ]] && return +# Check if a command can be found on the $PATH +_command_exists() { + command -v "$1" 1>/dev/null 2>&1 +} + + # Enable Powerlevel10k instant prompt if [ -r "${XDG_CACHE_HOME:-$HOME/.cache}/zsh/p10k-instant-prompt-${(%):-%n}.zsh" ]; then @@ -61,7 +67,10 @@ plugins=( dirhistory dotenv # config in ~/.zshenv; `_update_repositories` temporarily disables this git-escape-magic + invoke # completions for invoke jsontools + pip # completions for pip + poetry # completions for poetry z ) @@ -124,6 +133,21 @@ zstyle ':completion:*:warnings' format 'No matches for: %d' zstyle ':completion:*' group-name '' +# Enable completions for various tools + +# invoke -> see plugins above; alternatively use +# _command_exists invoke && eval "$(invoke --print-completion-script=zsh)" + +_command_exists nox && eval "$(register-python-argcomplete nox)" + +# pip -> see plugins above; alternatively use +# _command_exists pip && eval "$(pip completion --zsh)" + +_command_exists pipx && eval "$(register-python-argcomplete pipx)" + +# poetry -> see plugins above; no alternative here + + # Define key bindings diff --git a/README.md b/README.md index 7d20b28..242b31f 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,22 @@ Further, `zsh` is set up Otherwise, `~/.profile` is probably *not* sourced. **Important**: Don't forget to back up your current dotfiles! + + +### Python Development Environments + +The develop environments for Python are managed via [`pyenv`](https://github.com/pyenv/pyenv). + +To set them up, run: + +```bash +create-or-update-python-envs +``` + +Several Python versions are installed. +Additionally, two `virtualenv`s, called "interactive" and "utils", are also created: + - "interactive" is the default environment, and + - "utils" hosts globally available utilities + (e.g., [youtube-dl](https://github.com/ytdl-org/youtube-dl/)). + +Use `pyenv local ...` to specify a particular Python binary for a project.