Skip to content

manage_pyenv.sh

Source: Notion | Last edited: 2024-11-25 | ID: 1002d2dc-3ef...


2024-11-25 Clear up some redundancies, and align pyenv Virtual environment against its symlink alias and base versioning.

2024-11-02 upgrade pip before installing new packages

2024-09-25 compatible with both macOS and fresh Ubuntu installation and maintenance.

2024-09-19 pyenv update build-in at the start of the script run, bug fixes on naming of virtual environments, no more need for users to press return key to confirm.

2024-09-17 formatting fixes, more handling of special cases

2024-09-15 zsh compatible instead of bash compatible

2024-09-13 fix formatting errors

#!/usr/bin/env zsh
clear
# Define ANSI color codes using $'...' syntax
LIGHT_CYAN=$'\033[1;96m'
YELLOW=$'\033[1;93m'
RED=$'\033[1;91m'
GREEN=$'\033[1;92m'
MAGENTA=$'\033[0;35m'
BLUE=$'\033[1;94m'
ORANGE=$'\033[38;5;214m'
CYAN=$'\033[0;36m'
WHITE=$'\033[1;37m'
NC=$'\033[0m' # No Color
# Function to print colored text
zap() {
local color=$1
local message=$2
local color_value=$(eval "echo \$$color")
echo -e "${color_value}${message}${NC}"
}
# Enhanced logging function with ANSI support
log() {
local level="$1"
local message="$2"
local color="$NC"
case "$level" in
INFO)
color="$GREEN"
;;
WARNING)
color="$YELLOW"
;;
ERROR)
color="$RED"
;;
DEBUG)
color="$LIGHT_CYAN"
;;
SUCCESS)
color="$BLUE"
;;
NOTICE)
color="$ORANGE"
;;
CRITICAL)
color="$MAGENTA"
;;
TRACE)
color="$CYAN"
;;
VERBOSE)
color="$WHITE"
;;
*)
color="$NC"
;;
esac
# Print the log level with main color
printf "${color}[%s] " "$level"
# Process the message to reapply main color after embedded resets
local processed_message
processed_message="${message//${NC}/${color}}"
# Print the processed message
printf "%s\n" "$processed_message"
# Reset color at the end to prevent color bleeding
printf "${NC}"
}
# Function for user confirmations
confirm_action() {
local prompt_message="$1"
local default_choice="${2:-Y}"
local prompt_options="Y/n"
local color="${3:-YELLOW}"
if [[ "$default_choice" == "N" ]]; then
prompt_options="y/N"
fi
zap "$color" "$prompt_message (${MAGENTA}$prompt_options${NC})"
read -k 1 -r user_choice
echo # Move to a new line after input
if [[ "$default_choice" == "N" ]]; then
[[ "$user_choice" =~ ^[Yy]$ ]] || return 1
else
[[ "$user_choice" =~ ^[Nn]$ ]] && return 1
fi
return 0
}
# Function to detect the operating system
detect_os() {
if [[ "$OSTYPE" == "darwin"* ]]; then
OS='macos'
PACKAGE_MANAGER='brew'
elif [[ -f /etc/os-release ]]; then
. /etc/os-release
if [[ "$ID" == "ubuntu" ]]; then
OS='ubuntu'
PACKAGE_MANAGER='apt'
else
log "ERROR" "Unsupported Linux distribution: $ID"
exit 1
fi
else
log "ERROR" "Unsupported operating system: $OSTYPE"
exit 1
fi
}
# Initialize OS detection
detect_os
# Initialize pyenv within the script
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
# Cache pyenv versions and virtual environments
cached_pyenv_versions=$(pyenv versions --bare)
cached_virtualenvs=$(pyenv virtualenvs --bare | awk -F'/' '{print $NF}')
# Function to refresh cached pyenv data
refresh_pyenv_cache() {
# Add mutex lock to prevent concurrent cache updates
local lock_file="/tmp/pyenv_cache.lock"
if ! mkdir "$lock_file" 2>/dev/null; then
log "DEBUG" "Another process is updating pyenv cache. Waiting..."
while ! mkdir "$lock_file" 2>/dev/null; do
sleep 0.1
done
fi
cached_pyenv_versions=$(pyenv versions --bare)
cached_virtualenvs=$(pyenv virtualenvs --bare | awk -F'/' '{print $NF}')
rmdir "$lock_file"
log "DEBUG" "Pyenv cache refreshed."
}
# Function to set up pyenv in the shell configuration
setup_pyenv_in_shell() {
local config_file="$HOME/.zshrc"
local pyenv_config="
# pyenv configuration
export PYENV_ROOT=\"\$HOME/.pyenv\"
export PATH=\"\$PYENV_ROOT/bin:\$PATH\"
eval \"\$(pyenv init --path)\"
eval \"\$(pyenv init -)\"
eval \"\$(pyenv virtualenv-init -)\"
"
if ! grep -q "pyenv init" "$config_file"; then
echo "$pyenv_config" >> "$config_file"
log "SUCCESS" "pyenv configuration added to ${MAGENTA}$config_file${NC}"
log "NOTICE" "Please restart your shell or run 'exec \$SHELL -l' for changes to take effect."
else
log "INFO" "pyenv configuration already exists in ${MAGENTA}$config_file${NC}"
fi
}
# Check if pyenv is in PATH, if not, try to initialize it
if ! command -v pyenv >/dev/null 2>&1; then
if [[ -x "$PYENV_ROOT/bin/pyenv" ]]; then
log "NOTICE" "pyenv found but not in PATH. Initializing..."
eval "$($PYENV_ROOT/bin/pyenv init -)"
eval "$($PYENV_ROOT/bin/pyenv virtualenv-init -)"
setup_pyenv_in_shell
else
log "ERROR" "pyenv is not installed or not in PATH. Please install pyenv and ensure it's accessible."
exit 1
fi
else
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"
log "INFO" "pyenv has been initialized."
fi
# Verify PYENV_ROOT
if [[ ! -d "$PYENV_ROOT" ]]; then
log "ERROR" "PYENV_ROOT directory does not exist at $PYENV_ROOT. Please install pyenv correctly."
exit 1
fi
# Confirm pyenv executable exists
if [[ ! -x "$PYENV_ROOT/bin/pyenv" ]]; then
log "ERROR" "pyenv executable not found in $PYENV_ROOT/bin/. Please reinstall pyenv."
exit 1
fi
# Function to update and upgrade packages
update_and_upgrade_packages() {
# Add timestamp check to avoid frequent updates
local update_interval=3600 # 1 hour in seconds
local timestamp_file="/tmp/last_package_update"
if [[ -f "$timestamp_file" ]] && (( $(date +%s) - $(cat "$timestamp_file") < update_interval )); then
log "DEBUG" "Package manager was updated recently. Skipping update."
return 0
fi
log "NOTICE" "Updating package manager..."
if [[ "$PACKAGE_MANAGER" == "brew" ]]; then
if brew update; then
log "SUCCESS" "Homebrew updated successfully."
else
log "ERROR" "Failed to update Homebrew."
exit 1
fi
log "NOTICE" "Upgrading all installed Homebrew packages..."
brew outdated --formula | awk '{print $1}' | while read pkg; do
if brew upgrade "$pkg"; then
log "SUCCESS" "Upgraded $pkg successfully."
else
log "ERROR" "Failed to upgrade $pkg."
fi
done
elif [[ "$PACKAGE_MANAGER" == "apt" ]]; then
if sudo apt update; then
log "SUCCESS" "APT updated successfully."
else
log "ERROR" "Failed to update APT."
exit 1
fi
log "NOTICE" "Upgrading all installed APT packages..."
if sudo apt upgrade -y; then
log "SUCCESS" "All APT packages upgraded successfully."
else
log "ERROR" "Failed to upgrade APT packages."
exit 1
fi
fi
date +%s > "$timestamp_file"
}
setup_pyenv_configuration() {
local config_file=""
local config_files=("$HOME/.zshrc" "$HOME/.bash_profile" "$HOME/.profile" "$HOME/.bashrc")
for file in "${config_files[@]}"; do
if [[ -f "$file" ]]; then
config_file="$file"
break
fi
done
if [[ -z "$config_file" ]]; then
log "ERROR" "No suitable configuration file found. Please specify the path to your shell configuration file:"
read config_file
fi
if [[ ! -f "$config_file" ]]; then
log "ERROR" "Specified file does not exist. Creating it."
touch "$config_file"
fi
if ! grep -q "PYENV_ROOT" "$config_file" || ! grep -q "pyenv init" "$config_file"; then
log "NOTICE" "Adding pyenv configuration to ${MAGENTA}$config_file${NC}"
cat << EOF >> "$config_file"
# pyenv configuration
export PYENV_ROOT="\$HOME/.pyenv"
export PATH="\$PYENV_ROOT/bin:\$PATH"
eval "\$(pyenv init --path)"
eval "\$(pyenv init -)"
eval "\$(pyenv virtualenv-init -)"
EOF
log "SUCCESS" "pyenv configuration added to ${MAGENTA}$config_file${NC}"
log "NOTICE" "Please restart your shell or run 'exec \$SHELL -l' for changes to take effect."
else
log "INFO" "pyenv configuration already exists in ${MAGENTA}$config_file${NC}"
fi
}
install_pyenv() {
log "NOTICE" "Installing pyenv..."
if [[ "$OS" == "macos" ]]; then
if brew install pyenv pyenv-virtualenv; then
log "SUCCESS" "pyenv installed successfully via Homebrew."
else
log "ERROR" "Failed to install pyenv via Homebrew."
return 1
fi
elif [[ "$OS" == "ubuntu" ]]; then
if sudo apt-get update && \
sudo apt-get install -y make build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev; then
curl https://pyenv.run | bash
if [[ $? -eq 0 ]]; then
log "SUCCESS" "pyenv installed successfully via curl."
else
log "ERROR" "Failed to install pyenv via curl."
return 1
fi
else
log "ERROR" "Failed to install pyenv dependencies."
return 1
fi
else
log "ERROR" "Unsupported operating system for pyenv installation."
return 1
fi
setup_pyenv_configuration
return 0
}
# Function to check and install a command if missing
install_command_if_missing() {
local command_name="$1"
local install_command_mac="$2"
local install_command_linux="$3"
local success_message="$4"
local skip_update="${5:-false}" # New parameter to control update behavior
log "DEBUG" "Checking for command/package: ${MAGENTA}$command_name${NC}"
local is_installed=false
if [[ "$command_name" == "build-essential" ]]; then
if dpkg -s build-essential &> /dev/null; then
is_installed=true
fi
elif [[ "$command_name" == "ripgrep" ]]; then
if command -v rg &> /dev/null || dpkg -s ripgrep &> /dev/null; then
is_installed=true
fi
elif [[ "$command_name" == "brew" ]]; then
if [[ "$OS" == "macos" ]] && command -v brew &> /dev/null; then
is_installed=true
fi
else
if command -v "$command_name" &> /dev/null; then
is_installed=true
fi
fi
if [[ "$is_installed" == "true" ]]; then
log "DEBUG" "${MAGENTA}$command_name${NC} is already installed."
if [[ "$PACKAGE_MANAGER" == "apt" ]]; then
if [[ "$command_name" != "pyenv" ]]; then
log "DEBUG" "Upgrading package: ${MAGENTA}$command_name${NC}"
sudo apt upgrade -y "$command_name"
if [[ $? -eq 0 ]]; then
log "SUCCESS" "${MAGENTA}$command_name${NC} has been upgraded."
else
log "ERROR" "Failed to upgrade ${MAGENTA}$command_name${NC}."
exit 1
fi
else
log "DEBUG" "Skipping upgrade for ${MAGENTA}$command_name${NC} as it's not managed by apt."
fi
elif [[ "$PACKAGE_MANAGER" == "brew" && "$skip_update" == "false" ]]; then
# Only update if skip_update is false
update_homebrew_once
fi
else
log "WARNING" "${MAGENTA}$command_name${NC} is not installed."
if confirm_action "Would you like to install ${MAGENTA}$command_name${NC} now?" "Y"; then
log "INFO" "Installing ${MAGENTA}$command_name${NC}..."
if [[ "$PACKAGE_MANAGER" == "brew" ]]; then
log "DEBUG" "Executing command: ${MAGENTA}$install_command_mac${NC}"
handle_homebrew_operation "$install_command_mac" "$command_name"
elif [[ "$PACKAGE_MANAGER" == "apt" ]]; then
log "DEBUG" "Executing command: ${MAGENTA}$install_command_linux${NC}"
eval "$install_command_linux"
fi
local exit_status=$?
log "DEBUG" "Command exit status: ${MAGENTA}$exit_status${NC}"
# Verify installation
if [[ "$command_name" == "build-essential" ]]; then
if dpkg -s build-essential &> /dev/null; then
log "INFO" "$success_message"
else
log "ERROR" "Failed to install ${MAGENTA}$command_name${NC}. Exiting."
exit 1
fi
elif [[ "$command_name" == "ripgrep" ]]; then
log "DEBUG" "Checking ripgrep installation..."
if command -v rg &> /dev/null; then
log "DEBUG" "rg command found in PATH"
log "INFO" "$success_message"
elif dpkg -s ripgrep &> /dev/null; then
log "DEBUG" "ripgrep package found in dpkg"
log "INFO" "$success_message"
else
log "ERROR" "Failed to install ${MAGENTA}$command_name${NC}. Exiting."
exit 1
fi
elif [[ "$command_name" == "brew" ]]; then
if brew --version &> /dev/null; then
log "INFO" "$success_message"
else
log "ERROR" "Failed to install ${MAGENTA}$command_name${NC}. Exiting."
exit 1
fi
elif command -v "$command_name" &> /dev/null; then
log "INFO" "$success_message"
if [[ "$command_name" == "pyenv" ]]; then
setup_pyenv_configuration
fi
else
log "ERROR" "Failed to install ${MAGENTA}$command_name${NC}. Exiting."
exit 1
fi
fi
fi
}
# Function to handle Homebrew updates once
update_homebrew_once() {
local update_lock="/tmp/homebrew_updated"
if [[ ! -f "$update_lock" ]]; then
log "NOTICE" "Updating Homebrew..."
brew update
if [[ $? -eq 0 ]]; then
log "SUCCESS" "Homebrew updated successfully."
touch "$update_lock"
else
log "ERROR" "Failed to update Homebrew."
exit 1
fi
log "NOTICE" "Upgrading installed Homebrew packages..."
brew upgrade
if [[ $? -eq 0 ]]; then
log "SUCCESS" "Homebrew packages upgraded successfully."
else
log "ERROR" "Failed to upgrade Homebrew packages."
exit 1
fi
fi
}
# Function to install dependencies
install_dependencies() {
local dependencies_mac=(
"git"
"wget"
"fzf"
"rg" # ripgrep
)
local dependencies_linux=(
"git"
"wget"
"fzf"
"ripgrep"
)
local dependencies=()
if [[ "$OS" == "macos" ]]; then
dependencies=("${dependencies_mac[@]}")
elif [[ "$OS" == "ubuntu" ]]; then
dependencies=("${dependencies_linux[@]}")
fi
# Update package manager once at the start
if [[ "$PACKAGE_MANAGER" == "brew" ]]; then
update_homebrew_once
fi
# Install dependencies with skip_update=true to prevent redundant updates
for pkg in "${dependencies[@]}"; do
install_command_if_missing "$pkg" "brew install $pkg" "sudo apt install -y $pkg" "$pkg installed successfully." true
done
}
# Cleanup function to remove temporary files
cleanup_temp_files() {
rm -f /tmp/homebrew_updated
}
# Register cleanup function to run on script exit
trap cleanup_temp_files EXIT
# Install pyenv
install_command_if_missing "pyenv" "install_pyenv" "install_pyenv" "pyenv installed successfully."
# Install brew (for macOS) or build-essential (for Ubuntu)
if [[ "$OS" == "macos" ]]; then
install_command_if_missing "brew" "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" "" "Homebrew installed successfully."
elif [[ "$OS" == "ubuntu" ]]; then
install_command_if_missing "build-essential" "" "sudo apt install -y build-essential" "build-essential installed successfully."
fi
# Call the install_dependencies function
install_dependencies
# Define special cases dictionary
typeset -A special_cases
special_cases=(
["TA-Lib"]="$([[ '$PACKAGE_MANAGER' == 'brew' ]] && echo 'brew install ta-lib && pyenv exec python -m pip install TA-Lib' || echo 'sudo apt install -y ta-lib && pyenv exec python -m pip install TA-Lib')"
["talib"]="$([[ '$PACKAGE_MANAGER' == 'brew' ]] && echo 'brew install ta-lib && pyenv exec python -m pip install TA-Lib' || echo 'sudo apt install -y ta-lib && pyenv exec python -m pip install TA-Lib')"
["dateutil"]="pyenv exec python -m pip install python-dateutil"
["json_log_formatter"]="pyenv exec python -m pip install JSON-log-formatter"
["PIL"]="pyenv exec python -m pip install pillow"
["cv2"]="pyenv exec python -m pip install opencv-python"
["sklearn"]="pyenv exec python -m pip install scikit-learn"
["bs4"]="pyenv exec python -m pip install beautifulsoup4"
["yaml"]="pyenv exec python -m pip install PyYAML"
["wx"]="pyenv exec python -m pip install wxPython"
["dotenv"]="pyenv exec python -m pip install python-dotenv"
["jwt"]="pyenv exec python -m pip install PyJWT"
["fitz"]="pyenv exec python -m pip install PyMuPDF"
["magic"]="pyenv exec python -m pip install python-magic"
["gdal"]="$([[ '$PACKAGE_MANAGER' == 'brew' ]] && echo 'brew install gdal && pyenv exec python -m pip install GDAL' || echo 'sudo apt install -y gdal-bin libgdal-dev && pyenv exec python -m pip install GDAL')"
["tkinter"]="$([[ '$PACKAGE_MANAGER' == 'brew' ]] && echo 'brew install python-tk@3.12 && pyenv exec python -m pip install tk' || echo 'sudo apt install -y python3-tk && pyenv exec python -m pip install tk')"
["psycopg2"]="pyenv exec python -m pip install psycopg2-binary"
["pil"]="pyenv exec python -m pip install pillow"
["Image"]="pyenv exec python -m pip install pillow"
["pydantic"]="pyenv exec python -m pip install pydantic[email]"
["pywt"]="pyenv exec python -m pip install PyWavelets"
)
# Function to handle special cases
handle_special_case() {
local package_name="$1"
local max_retries=3
local retry_count=0
while (( retry_count < max_retries )); do
if eval "${special_cases[$package_name]}"; then
log "SUCCESS" "${MAGENTA}$package_name${NC} installed successfully."
return 0
else
(( retry_count++ ))
if (( retry_count < max_retries )); then
log "WARNING" "Retrying installation of ${MAGENTA}$package_name${NC} (attempt $retry_count of $max_retries)..."
sleep 2
fi
fi
done
log "ERROR" "Failed to install ${MAGENTA}$package_name${NC} after $max_retries attempts."
return 1
}
# Function to install a Python package with special case handling
install_python_package() {
local package_name="$1"
local parallel_jobs=4 # Adjust based on system capabilities
if [[ -n "${special_cases[$package_name]}" ]]; then
handle_special_case "$package_name"
else
log "NOTICE" "Installing ${MAGENTA}$package_name${NC}..."
if pyenv exec python -m pip install --jobs "$parallel_jobs" "$package_name" --verbose; then
log "SUCCESS" "${MAGENTA}$package_name${NC} installed successfully."
else
log "ERROR" "Failed to install ${MAGENTA}$package_name${NC}."
return 1
fi
fi
}
# Function to set the latest stable Python version as global
set_latest_stable_python_global() {
log "NOTICE" "Checking for the latest stable Python version..."
# This regex matches stable versions like 3.10.0, 3.11.2, etc., but not 3.11.0b1 or similar
latest_stable_version=$(echo "$cached_pyenv_versions" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1)
if [ -n "$latest_stable_version" ]; then
log "NOTICE" "Latest stable Python version found: ${MAGENTA}$latest_stable_version${NC}"
if ! echo "$cached_pyenv_versions" | grep -q "^$latest_stable_version$"; then
log "NOTICE" "Installing Python ${MAGENTA}$latest_stable_version${NC}..."
pyenv install -s "$latest_stable_version"
refresh_pyenv_cache
else
log "INFO" "Python ${MAGENTA}$latest_stable_version${NC} is already installed."
fi
log "NOTICE" "Setting global Python version to ${MAGENTA}$latest_stable_version${NC}"
pyenv global "$latest_stable_version"
log "SUCCESS" "Global Python version set to ${MAGENTA}$latest_stable_version${NC}"
else
log "ERROR" "Failed to determine the latest stable Python version"
return 1
fi
}
# Function to clear package manager cache
clear_package_manager_cache() {
log "NOTICE" "Clearing package manager cache..."
if [[ "$PACKAGE_MANAGER" == "brew" ]]; then
brew cleanup --prune=1 -s
rm -rf "$(brew --cache)"
log "SUCCESS" "Homebrew cache cleared."
elif [[ "$PACKAGE_MANAGER" == "apt" ]]; then
sudo apt clean
sudo apt autoclean
log "SUCCESS" "APT cache cleared."
fi
}
# Call the update and upgrade function before performing other operations
update_and_upgrade_packages
# Call the clear_package_manager_cache function before any package operations
clear_package_manager_cache
# --- Begin pyenv update ---
if command -v pyenv >/dev/null 2>&1; then
log "NOTICE" "Updating pyenv to the latest version..."
if pyenv update; then
log "SUCCESS" "pyenv updated successfully."
refresh_pyenv_cache
else
log "WARNING" "Failed to update pyenv. Please update it manually."
fi
else
log "ERROR" "pyenv command not found. Skipping update."
fi
# --- End pyenv update ---
# Set the latest stable Python version as global
set_latest_stable_python_global
# Function to analyze pyenv environment structure
analyze_pyenv_structure() {
log "DEBUG" "Analyzing pyenv environment structure..."
# Log raw pyenv commands output
log "DEBUG" "Raw pyenv versions output:"
pyenv versions --bare | while IFS= read -r version; do
log "TRACE" "Version entry: ${MAGENTA}$version${NC}"
done
log "DEBUG" "Raw pyenv virtualenvs output:"
pyenv virtualenvs --bare | while IFS= read -r venv; do
log "TRACE" "Virtualenv entry: ${MAGENTA}$venv${NC}"
done
# Analyze symlinks and real paths
log "DEBUG" "Analyzing environment paths and symlinks..."
pyenv versions --bare | while IFS= read -r version; do
local version_path="$PYENV_ROOT/versions/$version"
if [[ -L "$version_path" ]]; then
local real_path=$(readlink -f "$version_path")
log "TRACE" "Symlink: ${MAGENTA}$version${NC} -> ${MAGENTA}$real_path${NC}"
elif [[ -d "$version_path" ]]; then
log "TRACE" "Directory: ${MAGENTA}$version${NC} at ${MAGENTA}$version_path${NC}"
fi
done
# Analyze version relationships
log "DEBUG" "Analyzing version relationships..."
pyenv virtualenvs --bare | while IFS= read -r venv; do
if [[ "$venv" =~ "/" ]]; then
local base_version=${venv%%/*}
local env_name=${venv##*/}
log "TRACE" "Virtual env: ${MAGENTA}$env_name${NC} based on ${MAGENTA}$base_version${NC}"
# Check if there's a corresponding alias
if pyenv versions --bare | grep -q "^$env_name$"; then
log "TRACE" "Found alias: ${MAGENTA}$env_name${NC} for ${MAGENTA}$venv${NC}"
fi
fi
done
}
# Function to list and sort environments
list_and_sort_environments() {
# Add detailed environment analysis
analyze_pyenv_structure
# Cache the environment list for subsequent calls
if [[ -z "$cached_environments" ]]; then
log "DEBUG" "Building environment cache..."
cached_environments=$(pyenv versions --bare)
cached_virtual_envs=$(pyenv virtualenvs --bare)
# Log the structure of cached data
log "DEBUG" "Cached environments structure:"
echo "$cached_environments" | while IFS= read -r env; do
log "TRACE" "Cached env: ${MAGENTA}$env${NC}"
done
log "DEBUG" "Cached virtual environments structure:"
echo "$cached_virtual_envs" | while IFS= read -r venv; do
log "TRACE" "Cached venv: ${MAGENTA}$venv${NC}"
done
fi
log "DEBUG" "Step 2: Listing available pyenv environments."
log "NOTICE" "We will now display environments prioritized in this order:"
log "NOTICE" "1. Current .python-version environment (if exists)"
log "NOTICE" "2. Other environments sorted by:"
log "NOTICE" " - Python version (newest first)"
log "NOTICE" " - Project name length (longest first)"
log "NOTICE" " - Full name (alphabetically)"
log "NOTICE" "3. Base Python installations"
# Get all pyenv versions and virtual environments from cached variables
local all_versions="$cached_environments"
local virtual_envs="$cached_virtual_envs"
# Separate virtual environments and base installations
local virtual_env_list=()
local base_install_list=()
while IFS= read -r version; do
if echo "$virtual_envs" | grep -q "^$version$"; then
virtual_env_list+=("$version")
elif [[ ! "$version" =~ "/" ]]; then
base_install_list+=("$version")
fi
done <<< "$all_versions"
# Check for .python-version file
local python_version_file=""
if [[ -f ".python-version" ]]; then
python_version_file=$(cat .python-version)
log "NOTICE" "Found .python-version file with version: ${MAGENTA}${python_version_file}${NC}"
fi
# Custom sort function
sort_versions() {
local python_version_file="$1"
shift
printf '%s\n' "$@" | awk -v pv="$python_version_file" '
function extract_version(str) {
match(str, /[0-9]+(\.[0-9]+)*/)
return substr(str, RSTART, RLENGTH)
}
{
version = extract_version($0)
name_without_version = $0
sub(/-?[0-9]+(\.[0-9]+)*$/, "", name_without_version)
if ($0 == pv) {
print "0", version, length(name_without_version), $0
} else if (version == "") {
print "2", "0.0.0", "0", $0 # Bare versions treated as having shortest name length
} else {
print "1", version, length(name_without_version), $0
}
}' | sort -k1,1n -k2,2Vr -k3,3nr -k4 | cut -d' ' -f4-
}
# Sort virtual environments and base installations
IFS=$'\n' sorted_virtual_envs=($(sort_versions "$python_version_file" "${virtual_env_list[@]}"))
IFS=$'\n' sorted_base_installs=($(sort_versions "$python_version_file" "${base_install_list[@]}"))
unset IFS
# Display sorted environments with correct color handling
local index=1
printf "Virtual Environments:\n"
for env in "${sorted_virtual_envs[@]}"; do
# Skip entries that are full paths if they have aliases
if [[ "$env" =~ /envs/ ]]; then
continue
fi
# Find the corresponding full path and base version
local full_path=""
local base_version=""
while IFS= read -r venv; do
if [[ "$venv" =~ /envs/$env$ ]]; then
full_path="$venv"
base_version="${venv%%/*}"
break
fi
done < <(pyenv virtualenvs --bare)
if [[ -n "$full_path" ]]; then
printf "%d ${MAGENTA}%s${NC} (${CYAN}%s${NC}, based on ${BLUE}%s${NC})\n" \
"$index" "$env" "$full_path" "$base_version"
else
printf "%d ${MAGENTA}%s${NC}\n" "$index" "$env"
fi
((index++))
done
printf "\nBase Installations:\n"
for install in "${sorted_base_installs[@]}"; do
# Only show actual base installations, not paths
if [[ ! "$install" =~ /envs/ ]]; then
printf "%d ${MAGENTA}%s${NC}\n" "$index" "$install"
((index++))
fi
done
log "DEBUG" "Step 3: Determining the default environment."
if [ ${#sorted_virtual_envs[@]} -gt 0 ]; then
default_env=${sorted_virtual_envs[0]}
default_index=1
else
default_env=${sorted_base_installs[0]}
default_index=1
fi
log "NOTICE" "The default environment is ${MAGENTA}$default_env${NC} (option ${MAGENTA}$default_index${NC})."
# Store environments for selection
all_environments=("${sorted_virtual_envs[@]}" "${sorted_base_installs[@]}")
# Display options only once, without redundant default mention
log "NOTICE" "Press the number key corresponding to the environment you want to activate,"
log "NOTICE" "or press ${MAGENTA}q${NC} or ${MAGENTA}Esc${NC} to quit,"
log "NOTICE" "${MAGENTA}D${NC} to deactivate the current virtual environment, or"
log "NOTICE" "${MAGENTA}e${NC} to expunge a base Python installation and its associated environments"
}
# Define the expunge_python_installation function before it's used
expunge_python_installation() {
log "NOTICE" "Expunging a base Python installation and its associated environments"
# List base Python installations
local base_installations=$(echo "$cached_pyenv_versions" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$')
# Use fzf to select a base installation to expunge
local selected_version=$(echo "$base_installations" | fzf --height=40% --layout=reverse --border --prompt="Select base Python installation to expunge: ")
if [ -z "$selected_version" ]; then
log "ERROR" "No version selected. Expunge operation cancelled."
return
fi
if confirm_action "Are you sure you want to expunge Python ${MAGENTA}$selected_version${NC} and all its associated environments?"; then
# Find and uninstall associated virtual environments
local associated_envs=$(echo "$cached_virtualenvs" | grep "$selected_version")
for env in $associated_envs; do
log "NOTICE" "Uninstalling virtual environment: ${MAGENTA}$env${NC}"
pyenv uninstall -f "$env"
done
# Uninstall the base Python version
log "NOTICE" "Uninstalling base Python version: ${MAGENTA}$selected_version${NC}"
pyenv uninstall -f "$selected_version"
log "SUCCESS" "Python ${MAGENTA}$selected_version${NC} and its associated environments have been expunged."
# Rehash pyenv
pyenv rehash
# Clean up any .python-version files referencing the expunged version
log "NOTICE" "Cleaning up .python-version files..."
find . -name ".python-version" -type f -print0 | while IFS= read -r -d '' file; do
if grep -q "^$selected_version$" "$file"; then
log "NOTICE" "Removing ${MAGENTA}$file${NC}"
rm -f "$file"
fi
done
# Check if the current directory's .python-version was removed
if [ ! -f ".python-version" ]; then
log "NOTICE" "Local .python-version file was removed. Setting global Python version."
pyenv global "$(pyenv versions --bare | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -n1)"
fi
log "NOTICE" "Expunge operation completed. You may need to restart your shell."
else
log "NOTICE" "Expunge operation cancelled."
fi
}
# Add this block after the shebang and before defining colors
# --- Begin argument parsing ---
while getopts ":n:a:x" opt; do
case ${opt} in
n )
env_number=$OPTARG
;;
a )
action=$OPTARG
;;
x )
exit_after_activation=true
;;
\? )
log "ERROR" "Invalid option: -${MAGENTA}$OPTARG${NC}" >&2
exit 1
;;
: )
log "ERROR" "Option -${MAGENTA}$OPTARG${NC} requires an argument." >&2
exit 1
;;
esac
done
shift $((OPTIND -1))
# --- End argument parsing ---
# If command-line arguments are not provided, fall back to interactive input
if [ -z "$env_number" ] && [ -z "$action" ]; then
# Display options only once
list_and_sort_environments
# Read a single keypress without requiring Enter
read -k 1 env_number
echo # Move to a new line after input
# Log the raw input for debugging
log "DEBUG" "Raw input from user: '$env_number'"
# Handle 'Enter' key (no input) by checking if env_number is empty or a newline
if [[ -z "$env_number" || "$env_number" == $'\n' || "$env_number" == $'\r' ]]; then
env_number=$default_index
log "NOTICE" "No input or Return key detected, selecting the default environment: ${MAGENTA}${all_environments[$default_index]}${NC}."
else
log "NOTICE" "You have selected input: '${env_number}'."
fi
fi
# Handle special actions by setting env_number accordingly
if [ -n "$action" ]; then
env_number="$action"
fi
# Handle quit options (Esc or 'q') immediately
if [[ "$env_number" == "q" || "$env_number" == $'\e' ]]; then
log "ERROR" "Quitting script. No environment was activated."
exit 0
fi
# Handle deactivate option (D or d) immediately
if [[ "$env_number" == "D" || "$env_number" == "d" ]]; then
log "NOTICE" "Attempting to deactivate the current virtual environment."
log "DEBUG" "State before deactivation:"
log "NOTICE" "Python version: ${MAGENTA}$(python --version 2>&1)${NC}"
log "NOTICE" "Pyenv version: ${MAGENTA}$(pyenv version)${NC}"
log "NOTICE" "Virtual environment: ${MAGENTA}$(pyenv virtualenv-name 2>/dev/null || echo 'None')${NC}"
log "NOTICE" "PYENV_VERSION: ${MAGENTA}$PYENV_VERSION${NC}"
log "NOTICE" "PATH: ${MAGENTA}$PATH${NC}"
# Remove .python-version file if it exists
if [ -f ".python-version" ]; then
log "NOTICE" "Removing .python-version file"
rm .python-version
fi
# Set pyenv version to global
global_version=$(pyenv global)
log "NOTICE" "Setting pyenv version to global: ${MAGENTA}$global_version${NC}"
pyenv shell "$global_version"
# Deactivate pyenv virtualenv
pyenv deactivate 2>/dev/null || true
# Unset pyenv shell version
pyenv shell --unset
# Unset PYENV_VERSION environment variable
unset PYENV_VERSION
# Unset VIRTUAL_ENV environment variable
unset VIRTUAL_ENV
# Remove virtual environment from PATH
if [ -n "$PYENV_VERSION" ]; then
export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "${PYENV_ROOT}/versions/${PYENV_VERSION}" | tr '\n' ':' | sed 's/:$//')
fi
# Rehash pyenv
pyenv rehash
log "DEBUG" "State after deactivation:"
log "NOTICE" "Python version: ${MAGENTA}$(python --version 2>&1)${NC}"
log "NOTICE" "Pyenv version: ${MAGENTA}$(pyenv version)${NC}"
log "NOTICE" "Virtual environment: ${MAGENTA}$(pyenv virtualenv-name 2>/dev/null || echo 'None')${NC}"
log "NOTICE" "PYENV_VERSION: ${MAGENTA}$PYENV_VERSION${NC}"
log "NOTICE" "PATH: ${MAGENTA}$PATH${NC}"
log "SUCCESS" "Virtual environment deactivated."
# Force reload of shell
exec "$SHELL"
exit 0
fi
# Handle expunge option (e or E) immediately
if [[ "$env_number" == "e" || "$env_number" == "E" ]]; then
expunge_python_installation
exit 0
fi
# If env_number is a valid number, proceed; otherwise, handle invalid input
if [[ "$env_number" =~ ^[0-9]+$ ]]; then
# Check that env_number is within the range of available options
num_environments=${#all_environments[@]}
if (( env_number < 1 || env_number > num_environments )); then
log "ERROR" "Invalid selection. Please enter a number between 1 and $num_environments."
exit 1
fi
# Now it's safe to index the array
selected_env=${all_environments[$env_number]}
else
# If not a special key or a number, it's invalid
log "ERROR" "Invalid input. Please enter a valid number."
exit 1
fi
# Function to check if the environment is activated correctly
check_environment_activation() {
local env_name="$1"
local python_path=$(pyenv which python)
local current_version=$(python --version 2>&1)
log "NOTICE" "Checking environment activation:"
log "MAGENTA" "Python path: ${MAGENTA}$python_path${NC}"
log "MAGENTA" "Python version: ${MAGENTA}$current_version${NC}"
log "MAGENTA" "PYENV_VERSION: ${MAGENTA}$PYENV_VERSION${NC}"
if [[ "$python_path" == *"$env_name"* ]] && [[ "$PYENV_VERSION" == *"$env_name"* ]]; then
log "INFO" "Environment ${MAGENTA}$env_name${NC} successfully activated."
return 0
else
log "ERROR" "Failed to activate environment ${MAGENTA}$env_name${NC}."
return 1
fi
}
# Activate the selected environment
log "NOTICE" "Activating the environment: ${MAGENTA}$selected_env${NC}."
pyenv shell "$selected_env"
# Check if activation was successful
if [[ "$(pyenv version-name)" != "$selected_env" ]]; then
log "ERROR" "Failed to activate the environment. Exiting."
exit 1
fi
log "SUCCESS" "Environment ${MAGENTA}$selected_env${NC} successfully activated."
# Modify PATH to prioritize the virtual environment
export PATH="$VIRTUAL_ENV/bin:$PATH"
# Ensure VIRTUAL_ENV is set correctly
export VIRTUAL_ENV="$(pyenv prefix)"
# Check if activate script exists
log "NOTICE" "Checking for activate script in the virtual environment directory:"
log "DEBUG" "VIRTUAL_ENV is set to: ${MAGENTA}$VIRTUAL_ENV${NC}"
if [[ -d "$VIRTUAL_ENV" ]]; then
log "INFO" "Virtual environment directory exists: ${MAGENTA}$VIRTUAL_ENV${NC}"
if [[ -f "$VIRTUAL_ENV/bin/activate" ]]; then
log "INFO" "activate script found in ${MAGENTA}$VIRTUAL_ENV/bin/activate${NC}"
else
log "ERROR" "activate script not found in ${MAGENTA}$VIRTUAL_ENV/bin/activate${NC}"
fi
else
log "ERROR" "Virtual environment directory not found: ${MAGENTA}$VIRTUAL_ENV${NC}"
fi
# Set PYENV_VIRTUAL_ENV environment variable
export PYENV_VIRTUAL_ENV="$VIRTUAL_ENV"
log "NOTICE" "PYENV_VIRTUAL_ENV environment variable:"
printf "${MAGENTA}%s${NC}\n" "$PYENV_VIRTUAL_ENV"
# Verify no system-wide site-packages in sys.path
log "NOTICE" "Checking for system-wide site-packages in sys.path:"
system_site_packages=$(python -c "import sys; print(any('/usr/lib' in p for p in sys.path))")
if [[ "$system_site_packages" == "False" ]]; then
log "INFO" "No system-wide site-packages found in sys.path"
else
log "WARNING" "System-wide site-packages found in sys.path"
fi
# Check pip list using a Python script
log "NOTICE" "Pip list summary:"
python -c "
import subprocess
import sys
from collections import Counter
try:
result = subprocess.run(['pyenv', 'exec', 'python', '-m', 'pip', 'list', '--format=freeze'], capture_output=True, text=True, check=True)
packages = result.stdout.strip().split('\n')
total_packages = len(packages)
print(f'Total number of packages: {total_packages}')
if total_packages > 0:
print('\nFirst 5 packages:')
for package in packages[:5]:
name, version = package.split('==')
print(f'{name:<30} {version}')
if total_packages > 5:
print(f'\n... and {total_packages - 5} more packages')
# Count and display package version statistics
versions = Counter(package.split('==')[1] for package in packages)
print('\nVersion statistics:')
for version, count in versions.most_common(3):
print(f'{version:<10} {count} packages')
# Count and display top-level domains of package names
tlds = Counter(package.split('==')[0].split('.')[-1] for package in packages)
print('\nTop-level domain statistics:')
for tld, count in tlds.most_common(3):
print(f'.{tld:<9} {count} packages')
except subprocess.CalledProcessError as e:
print(f'Error running pip list: {e}', file=sys.stderr)
print(f'Return code: {e.returncode}', file=sys.stderr)
print(f'Error output: {e.stderr}', file=sys.stderr)
except Exception as e:
print(f'Unexpected error: {e}', file=sys.stderr)
"
# Additional checks for completeness
log "NOTICE" "Python sys.prefix:"
python -c "import sys; print(sys.prefix)"
log "NOTICE" "Python sys.base_prefix:"
python -c "import sys; print(sys.base_prefix)"
log "NOTICE" "Is this a virtual environment?"
python -c "import sys; print(sys.prefix != sys.base_prefix)"
# Function to modify the prompt
modify_prompt() {
local env_name=$(basename "$VIRTUAL_ENV")
export PS1="(${MAGENTA}$env_name${NC}) $PS1"
log "INFO" "Prompt modified to show virtual environment."
log "NOTICE" "Your prompt should now show the virtual environment name."
log "NOTICE" "Note: This change is temporary and will revert when you close this shell session."
}
# Modify the prompt
modify_prompt
# Inform the user about the changes
log "NOTICE" "To see the changes, you may need to start a new command prompt."
log "NOTICE" "You can do this by typing 'exec ${MAGENTA}$SHELL${NC}' and pressing Enter."
log "SUCCESS" "Script execution completed."
log "NOTICE" "To exit this environment, simply close the terminal or type 'exit'."
# Function to set the local Python version for the current directory
set_local_python_version() {
local version="$1"
log "NOTICE" "Setting local Python version to ${MAGENTA}$version${NC} for the current directory."
pyenv local "$version"
log "SUCCESS" "Local Python version set to ${MAGENTA}$version${NC} for the current directory."
}
# Function to update the interpreter path
update_interpreter_path() {
if [[ -f ".python-version" ]]; then
local env_name=$(cat .python-version)
log "NOTICE" "Environment set in .python-version: ${MAGENTA}$env_name${NC}"
pyenv shell "$env_name"
local interpreter_path=$(pyenv which python)
echo "$interpreter_path" > .interpreter_path
log "SUCCESS" "Full interpreter path written to .interpreter_path: ${MAGENTA}$interpreter_path${NC}"
else
log "ERROR" "No .python-version file found."
return 1
fi
}
# Call the update_interpreter_path function at the start to ensure the interpreter path is up-to-date
update_interpreter_path
# Check if .python-version file exists and handle accordingly
handle_python_version_file() {
local selected_env="$1"
log "DEBUG" "Handling .python-version file:"
if [ -f ".python-version" ]; then
local current_version
current_version=$(cat .python-version)
log "NOTICE" "Updating .python-version file from ${MAGENTA}$current_version${NC} to ${MAGENTA}$selected_env${NC}."
else
log "NOTICE" "Creating new .python-version file with the selected environment: ${MAGENTA}$selected_env${NC}."
fi
log "BLUE" "Purpose: This file tells pyenv which Python version to use in this directory."
echo "$selected_env" > .python-version
log "SUCCESS" ".python-version file updated/created successfully."
log "DEBUG" "Local Python version set to ${MAGENTA}$selected_env${NC} for the current directory."
log "BLUE" "Effect: Any Python or pip commands run in this directory will now use ${MAGENTA}$selected_env${NC}."
# Update the interpreter path
update_interpreter_path
}
# Handle .python-version file
handle_python_version_file "$selected_env"
# Add this block to handle exiting after activation
# --- Begin exit after activation ---
if [ "$exit_after_activation" = true ]; then
log "NOTICE" "Exiting script as per the '-x' option after activating the environment."
# Optionally modify the prompt before exiting
modify_prompt
# Exit the script
exit 0
fi
# --- End exit after activation ---
# Function to install packages, ensuring correct logging
install_package() {
local full_env_name="$PYENV_VERSION"
log "DEBUG" "Analyzing packages needed in ${MAGENTA}${full_env_name}${NC}:"
# Use Python to analyze the current directory and find needed packages
local needed_packages=$(pyenv exec python -c "
import os
import ast
import sys
import subprocess
from collections import Counter
try:
from importlib import metadata
except ImportError:
import importlib_metadata as metadata
def ensure_pathspec_installed():
try:
import pathspec
return pathspec
except ImportError:
print('Warning: pathspec module not found. Attempting to install it...')
try:
subprocess.check_call(['pyenv', 'exec', 'python', '-m', 'pip', 'install', 'pathspec'])
import pathspec
print('Successfully installed and imported pathspec.')
return pathspec
except subprocess.CalledProcessError:
print('Failed to install pathspec. Unable to respect .gitignore patterns.')
return None
except ImportError:
print('Installed pathspec but failed to import. Unable to respect .gitignore patterns.')
return None
pathspec = ensure_pathspec_installed()
def get_imports(node):
if isinstance(node, ast.Import):
return [n.name.split('.')[0] for n in node.names]
elif isinstance(node, ast.ImportFrom):
return [node.module.split('.')[0]] if node.module else []
return []
# Load .gitignore patterns
gitignore_path = '.gitignore'
if os.path.exists(gitignore_path) and pathspec:
with open(gitignore_path, 'r') as f:
gitignore_patterns = f.read().splitlines()
spec = pathspec.PathSpec.from_lines('gitwildmatch', gitignore_patterns)
else:
spec = pathspec.PathSpec([]) if pathspec else None
# Analyze the current directory for needed packages
needed_packages = set()
for root, _, files in os.walk('.'):
if spec and spec.match_file(root):
continue
for file in files:
file_path = os.path.join(root, file)
if spec and spec.match_file(file_path):
continue
if file.endswith('.py'):
with open(file_path, 'r') as f:
try:
tree = ast.parse(f.read(), filename=file)
except SyntaxError:
continue # Skip files with syntax errors
for node in ast.walk(tree):
needed_packages.update(get_imports(node))
# Filter out standard library and already installed packages
try:
standard_libs = set(sys.stdlib_module_names)
except AttributeError:
# Fallback for older Python versions
standard_libs = set(sys.builtin_module_names).union(['os', 'sys', 'math', 're', 'time', 'random', 'datetime', 'json', 'csv', 'collections'])
installed_packages = {pkg.metadata['Name'] for pkg in metadata.distributions()}
needed_packages = needed_packages - standard_libs - installed_packages
print(' '.join(needed_packages))
")
if [[ -z "$needed_packages" ]]; then
log "INFO" "All required packages are already installed."
return
fi
log "NOTICE" "The following packages may need to be installed:"
echo "$needed_packages" | tr ' ' '\n' | sed 's/^/ /'
# Check for special cases
local selected=""
for pkg in $needed_packages; do
if [[ -n "${special_cases[$pkg]}" ]]; then
log "NOTICE" "Handling special installation for ${MAGENTA}$pkg${NC}..."
handle_special_case "$pkg"
else
selected+="$pkg "
fi
done
# Use fzf to select remaining packages to install
if [[ -n "$selected" ]]; then
local package_list=$(echo "$selected" | tr ' ' '\n')
local selected_packages=$(fzf_select_packages \
"Select packages to install (TAB to multi-select, ENTER to confirm): " \
"Use arrow keys to navigate, TAB to select, ENTER to confirm" \
"$package_list" \
"pyenv exec python -m pip show {1} 2>/dev/null || echo Package not installed")
if [[ -n "$selected_packages" ]]; then
log "NOTICE" "You've selected to install:"
echo "$selected_packages" | sed 's/^/ /'
if confirm_action "Are you sure you want to install these packages?"; then
log "NOTICE" "Installing selected packages..."
echo "$selected_packages" | while read pkg; do
install_python_package "$pkg"
done
log "SUCCESS" "Installation process complete."
else
log "NOTICE" "Installation cancelled."
fi
else
log "NOTICE" "No packages selected for installation."
fi
fi
}
# Utility function for FZF package selection
fzf_select_packages() {
local prompt="$1"
local header="$2"
local package_list="$3"
local preview_cmd="$4"
echo "$package_list" | fzf --multi --height=40% --layout=reverse --border --ansi \
--prompt="$prompt" \
--header="$header" \
--preview="$preview_cmd" \
--preview-window=right:50%:wrap \
--bind="esc:abort" \
--exit-0
}
# Utility function to check dependencies
check_dependencies() {
local packages="$1"
local action="$2"
log "NOTICE" "Checking dependencies..."
local dependency_issues=$(echo "$packages" | while read pkg; do
pyenv exec python -m pip check 2>&1 | grep -v "$pkg" | grep -i "depends on $pkg"
done)
if [[ -n "$dependency_issues" ]]; then
log "ERROR" "Dependency issues detected:"
echo "$dependency_issues"
if confirm_action "Do you want to proceed with the $action? This may cause conflicts." "N"; then
log "NOTICE" "$action cancelled."
return 1
fi
fi
return 0
}
update_package() {
local full_env_name="$PYENV_VERSION"
log "DEBUG" "Updating packages in ${MAGENTA}${full_env_name}${NC}:"
local outdated_packages=$(pyenv exec python -m pip list --outdated --format=json | python -c "
import sys, json
packages = json.load(sys.stdin)
for package in packages:
print(f'{package[\"name\"]}=={package[\"version\"]} -> {package[\"latest_version\"]}')
")
if [[ -z "$outdated_packages" ]]; then
log "SUCCESS" "All packages are up to date."
return
fi
log "NOTICE" "The following packages have updates available:"
echo "$outdated_packages" | sed 's/^/ /'
local selected=$(fzf_select_packages \
"Select packages to update (TAB to multi-select, ENTER to confirm, ESC to go back): " \
"Use arrow keys to navigate, TAB to select, ENTER to confirm, ESC to go back" \
"$outdated_packages" \
"pkg=\$(echo {1} | cut -d'=' -f1); pyenv exec python -m pip show \$pkg")
if [[ -z "$selected" ]]; then
log "WARNING" "Returning to main menu."
return
fi
local packages_to_update=$(echo "$selected" | cut -d'=' -f1)
log "WARNING" "You've selected to update:"
echo "$packages_to_update" | sed 's/^/ /'
check_dependencies "$packages_to_update" "update" || return
if confirm_action "Are you sure you want to update these packages?"; then
log "WARNING" "Updating selected packages..."
echo "$packages_to_update" | while read pkg; do
log "WARNING" "Updating ${MAGENTA}${pkg}${NC}..."
if pyenv exec python -m pip install --upgrade "$pkg" 2>&1 | tee /tmp/pip_update_log.txt; then
log "INFO" "Successfully updated ${MAGENTA}$pkg${NC}"
else
log "ERROR" "Failed to update ${MAGENTA}$pkg${NC}"
log "WARNING" "Checking for conflicts..."
conflicts=$(grep "ERROR: pip's dependency resolver" /tmp/pip_update_log.txt)
if [[ -n "$conflicts" ]]; then
log "ERROR" "Dependency conflicts detected:"
echo "$conflicts"
log "WARNING" "You may need to manually resolve these conflicts."
fi
fi
done
log "INFO" "Update process complete."
else
log "WARNING" "Update cancelled."
fi
}
uninstall_package() {
local full_env_name="$PYENV_VERSION"
log "INFO" "Uninstalling packages from ${MAGENTA}${full_env_name}${NC}:"
local installed_packages=$(pyenv exec python -m pip list --format=json | python -c "
import sys, json
packages = json.load(sys.stdin)
for package in packages:
print(f'{package[\"name\"]}=={package[\"version\"]}')
")
if [[ -z "$installed_packages" ]]; then
log "NOTICE" "No packages installed."
return
fi
local selected=$(fzf_select_packages \
"Select packages to uninstall (TAB to multi-select, ENTER to confirm, ESC to go back): " \
"Use arrow keys to navigate, TAB to select, ENTER to confirm, ESC to go back" \
"$installed_packages" \
"pkg=\$(echo {1} | cut -d'=' -f1); pyenv exec python -m pip show \$pkg")
if [[ -z "$selected" ]]; then
log "NOTICE" "Returning to main menu."
return
fi
local packages_to_uninstall=$(echo "$selected" | cut -d'=' -f1)
log "NOTICE" "You've selected to uninstall:"
echo "$packages_to_uninstall" | sed 's/^/ /'
check_dependencies "$packages_to_uninstall" "uninstallation" || return
if confirm_action "Are you sure you want to uninstall these packages?"; then
log "NOTICE" "Uninstalling selected packages..."
echo "$packages_to_uninstall" | while read pkg; do
log "NOTICE" "Uninstalling ${MAGENTA}${pkg}${NC}..."
if pyenv exec python -m pip uninstall -y "$pkg" 2>&1 | tee /tmp/pip_uninstall_log.txt; then
log "SUCCESS" "Successfully uninstalled ${MAGENTA}$pkg${NC}"
else
log "ERROR" "Failed to uninstall ${MAGENTA}$pkg${NC}"
log "NOTICE" "Checking for errors..."
errors=$(grep "ERROR:" /tmp/pip_uninstall_log.txt)
if [[ -n "$errors" ]]; then
log "ERROR" "Errors detected:"
echo "$errors"
fi
fi
done
log "SUCCESS" "Uninstallation process complete."
else
log "NOTICE" "Uninstallation cancelled."
fi
}
create_new_environment() {
log "INFO" "Creating a new virtual environment:"
# List available Python versions
log "NOTICE" "Fetching available Python versions..."
available_versions=$(pyenv install --list | grep -E '^\s*[0-9]+\.[0-9]+\.[0-9]+$' | sed 's/^[[:space:]]*//' | sort -rV)
if [ -z "$available_versions" ]; then
log "ERROR" "No available Python versions found."
return
fi
# Use fzf to select a Python version
log "NOTICE" "Select the Python version to use:"
python_version=$(echo "$available_versions" | fzf --height=40% --layout=reverse --border --ansi --prompt="Select Python version: ")
if [ -z "$python_version" ]; then
log "ERROR" "No Python version selected. Exiting."
return
fi
log "NOTICE" "Using selected Python version: ${MAGENTA}${python_version}${NC}"
# Check if the selected version is installed
if ! pyenv versions --bare | grep -q "^$python_version$"; then
log "NOTICE" "Python ${MAGENTA}$python_version${NC} is not installed. Do you want to install it? (y/N)"
read -k 1 install_choice
echo # Newline after keypress
if [[ "$install_choice" =~ ^[Yy]$ ]]; then
log "NOTICE" "Installing Python ${MAGENTA}$python_version${NC}..."
# Determine the latest version of OpenSSL
if [[ "$PACKAGE_MANAGER" == "brew" ]]; then
latest_openssl=$(brew list --versions | grep -E '^openssl(@[0-9]+)?' | awk '{print $1}' | sort -rV | head -n 1)
if [ -z "$latest_openssl" ]; then
log "ERROR" "No OpenSSL version found. Please install OpenSSL via Homebrew."
return
fi
log "NOTICE" "Using OpenSSL version: ${MAGENTA}${latest_openssl}${NC}"
# Use PYTHON_CONFIGURE_OPTS to specify OpenSSL path
PYTHON_CONFIGURE_OPTS="--with-openssl=$(brew --prefix $latest_openssl)" pyenv install "$python_version"
elif [[ "$PACKAGE_MANAGER" == "apt" ]]; then
log "NOTICE" "Installing Python ${MAGENTA}$python_version${NC} with system OpenSSL."
pyenv install "$python_version"
fi
if [[ $? -eq 0 ]]; then
log "SUCCESS" "Python ${MAGENTA}$python_version${NC} installed successfully."
else
log "ERROR" "Failed to install Python ${MAGENTA}$python_version${NC}. Exiting."
return
fi
else
log "ERROR" "Cannot create environment without installing Python ${MAGENTA}$python_version${NC}. Exiting."
return
fi
else
log "INFO" "Python ${MAGENTA}$python_version${NC} is already installed."
fi
# Prompt for environment name with best practice suggestion
default_env_name="env"
log "NOTICE" "Enter a name for the new virtual environment (default: ${MAGENTA}${default_env_name}-${python_version}${NC}):"
read env_name
# Use default name if none provided
if [ -z "$env_name" ]; then
env_name="$default_env_name"
fi
# Check if the environment name already exists with the same Python version
while true; do
if pyenv virtualenvs --bare | grep -q "^${env_name}-${python_version}$"; then
log "WARNING" "An environment named ${MAGENTA}${env_name}-${python_version}${NC} already exists."
log "NOTICE" "Please enter a different name (or press Enter to cancel):"
read new_env_name
if [ -z "$new_env_name" ]; then
log "NOTICE" "Environment creation cancelled."
return
fi
env_name="$new_env_name"
else
break
fi
done
# Append Python version to the environment name
full_env_name="${env_name}-${python_version}"
# Create the virtual environment
log "NOTICE" "Creating virtual environment ${MAGENTA}${full_env_name}${NC} with Python ${MAGENTA}${python_version}${NC}..."
if pyenv virtualenv "$python_version" "$full_env_name"; then
log "SUCCESS" "Virtual environment ${MAGENTA}${full_env_name}${NC} created successfully."
# Change permissions of the activate script to be executable
chmod +x "$PYENV_ROOT/versions/$full_env_name/bin/activate"
if [ $? -eq 0 ]; then
log "SUCCESS" "Set execute permissions for the activate script."
else
log "ERROR" "Failed to set execute permissions for the activate script."
return
fi
# Install pathspec by default
log "NOTICE" "Installing pathspec in the new environment..."
pyenv shell "$full_env_name"
pyenv exec python -m pip install pathspec
log "SUCCESS" "pathspec installed successfully."
# Offer to switch to the new environment
log "NOTICE" "Do you want to switch to the new environment? (y/N)"
read -k 1 switch_choice
echo # Newline after keypress
if [[ "$switch_choice" =~ ^[Yy]$ ]]; then
pyenv shell "$full_env_name"
log "SUCCESS" "Switched to ${MAGENTA}${full_env_name}${NC}."
# Update VIRTUAL_ENV and PATH
export VIRTUAL_ENV="$(pyenv prefix)"
export PATH="$VIRTUAL_ENV/bin:$PATH"
# Modify prompt
modify_prompt
else
log "NOTICE" "Staying in the current environment."
fi
else
log "ERROR" "Failed to create virtual environment ${MAGENTA}${full_env_name}${NC}."
fi
}
delete_virtual_environment() {
log "INFO" "Deleting a virtual environment:"
# List available pyenv virtual environments
log "NOTICE" "Fetching available pyenv virtual environments..."
available_envs=$(pyenv virtualenvs --bare | grep -v '/')
if [ -z "$available_envs" ]; then
log "ERROR" "No pyenv virtual environments found."
return
fi
# Use fzf to select a virtual environment to delete
log "NOTICE" "Select the virtual environment to delete:"
selected_env=$(echo "$available_envs" | fzf --height=40% --layout=reverse --border --ansi --prompt="Select environment to delete: ")
if [ -z "$selected_env" ]; then
log "ERROR" "No environment selected. Exiting."
return
fi
# Confirm deletion
log "ERROR" "Are you sure you want to delete the virtual environment ${MAGENTA}${selected_env}${NC}? (y/N)"
read -k 1 confirm_delete
echo # Newline after keypress
if [[ "$confirm_delete" =~ ^[Yy]$ ]]; then
log "NOTICE" "Deleting virtual environment ${MAGENTA}${selected_env}${NC}..."
if pyenv uninstall -f "$selected_env"; then
log "SUCCESS" "Virtual environment ${MAGENTA}${selected_env}${NC} deleted successfully."
# Clean up the terminal environment
log "NOTICE" "Cleaning up terminal environment..."
pyenv shell --unset
unset PYENV_VERSION
unset VIRTUAL_ENV
export PATH=$(echo "$PATH" | tr ':' '\n' | grep -v "${PYENV_ROOT}/versions/${selected_env}" | tr '\n' ':' | sed 's/:$//')
pyenv rehash
# Remove .python-version file if it exists
if [ -f ".python-version" ]; then
log "NOTICE" "Removing .python-version file"
rm .python-version
fi
log "SUCCESS" "Terminal environment cleaned up."
# Exit the shell
log "NOTICE" "Exiting the shell to complete cleanup."
exec "$SHELL"
else
log "ERROR" "Failed to delete virtual environment ${MAGENTA}${selected_env}${NC}."
fi
else
log "NOTICE" "Deletion cancelled."
fi
}
manage_packages() {
local full_env_name="$PYENV_VERSION"
log "NOTICE" "Package Management for ${MAGENTA}${full_env_name}${NC}"
local options=(
"List installed packages: View all packages in the current environment with their versions."
"Install a package: Search and install packages from PyPI."
"Uninstall a package: Remove packages with dependency checks."
"Update a package: Upgrade packages to their latest compatible versions."
"Create new virtual environment: Create a new virtual environment with a specific Python version."
"Delete virtual environment: Delete an existing pyenv virtual environment."
)
while true; do
local header="Package Management for ${MAGENTA}${full_env_name}${NC}"
# Use fzf with --ansi to correctly handle ANSI color codes
local choice=$(printf "%s\n" "${options[@]}" | fzf --ansi --height=30% --layout=reverse --border \
--prompt="Select an option: " \
--header="$header" \
--bind="esc:abort" \
--no-multi \
--history="${HOME}/.package_management_history" \
--color="header:italic:underline,prompt:bold,pointer:reverse")
if [[ -z "$choice" ]]; then
log "SUCCESS" "Exiting package management."
break
fi
case "$choice" in
"List installed packages:"*)
list_packages
;;
"Install a package:"*)
install_package
;;
"Uninstall a package:"*)
uninstall_package
;;
"Update a package:"*)
update_package
;;
"Create new virtual environment:"*)
create_new_environment
;;
"Delete virtual environment:"*)
delete_virtual_environment
;;
*)
log "ERROR" "Invalid choice. Please try again."
;;
esac
done
}
list_packages() {
local full_env_name="$PYENV_VERSION"
log "INFO" "Listing installed packages in ${MAGENTA}${full_env_name}${NC}:"
log "NOTICE" "Debug: Running pip list command..."
local installed_packages=$(pyenv exec python -m pip list --format=json | python -c "
import sys, json
packages = json.load(sys.stdin)
for package in packages:
print(f'{package[\"name\"]}=={package[\"version\"]}')
")
if [[ -z "$installed_packages" ]]; then
log "NOTICE" "No packages installed."
return
fi
log "NOTICE" "Installed packages:"
echo "$installed_packages" | sed 's/^/ /'
}
# Start package management
manage_packages
# Function to check and request terminal permissions
check_terminal_permissions() {
log "DEBUG" "Checking terminal permissions..."
if [[ "$OS" == "macos" ]]; then
# Check if Terminal has Full Disk Access
if ! osascript -e 'tell application "System Events" to get processes' &>/dev/null; then
log "WARNING" "Terminal needs Full Disk Access permissions for optimal Homebrew operation."
log "NOTICE" "Please grant Full Disk Access permission to Terminal:"
log "NOTICE" "1. Open System Preferences"
log "NOTICE" "2. Go to Security & Privacy > Privacy > Full Disk Access"
log "NOTICE" "3. Click the lock to make changes"
log "NOTICE" "4. Add Terminal.app to the list"
if confirm_action "Would you like to open System Preferences now?" "Y" "YELLOW"; then
open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles"
log "NOTICE" "Please restart the script after granting permissions."
exit 0
fi
else
log "SUCCESS" "Terminal has necessary permissions."
fi
fi
}
# Function to handle Homebrew operations with proper permissions
handle_homebrew_operation() {
local operation="$1"
local package="$2"
if [[ "$OS" == "macos" ]]; then
# Check if we have write permission to Homebrew directory
if [[ -w "$(brew --prefix)" ]]; then
log "DEBUG" "Has write permissions to Homebrew directory"
eval "$operation $package"
else
log "WARNING" "Insufficient permissions for Homebrew operation"
if confirm_action "Would you like to run the operation with sudo?" "N" "RED"; then
sudo bash -c "$(which brew) $operation $package"
else
return 1
fi
fi
else
eval "$operation $package"
fi
}
# Call check_terminal_permissions at the start of the script
check_terminal_permissions
# Add environment cleanup function
cleanup_environment() {
log "NOTICE" "Cleaning up environment..."
# Remove temporary files
rm -f /tmp/pip_update_log.txt /tmp/pip_uninstall_log.txt
# Clear pip cache
pyenv exec python -m pip cache purge
# Remove unused packages
pyenv exec python -m pip autoremove
}
# Add error recovery function
recover_from_error() {
local error_code=$1
local operation=$2
log "WARNING" "Error occurred during $operation (code: $error_code). Attempting recovery..."
case "$error_code" in
1) # General error
cleanup_environment
refresh_pyenv_cache
;;
2) # Package installation error
pyenv exec python -m pip install --upgrade pip
pyenv rehash
;;
*) # Unknown error
log "ERROR" "Unknown error occurred. Manual intervention required."
;;
esac
}