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 $'...' syntaxLIGHT_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 textzap() { local color=$1 local message=$2 local color_value=$(eval "echo \$$color") echo -e "${color_value}${message}${NC}"}
# Enhanced logging function with ANSI supportlog() { 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 confirmationsconfirm_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 systemdetect_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 detectiondetect_os
# Initialize pyenv within the scriptexport PYENV_ROOT="$HOME/.pyenv"export PATH="$PYENV_ROOT/bin:$PATH"
# Cache pyenv versions and virtual environmentscached_pyenv_versions=$(pyenv versions --bare)cached_virtualenvs=$(pyenv virtualenvs --bare | awk -F'/' '{print $NF}')
# Function to refresh cached pyenv datarefresh_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 configurationsetup_pyenv_in_shell() { local config_file="$HOME/.zshrc" local pyenv_config="# pyenv configurationexport 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 itif ! 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 fielse eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)" log "INFO" "pyenv has been initialized."fi
# Verify PYENV_ROOTif [[ ! -d "$PYENV_ROOT" ]]; then log "ERROR" "PYENV_ROOT directory does not exist at $PYENV_ROOT. Please install pyenv correctly." exit 1fi
# Confirm pyenv executable existsif [[ ! -x "$PYENV_ROOT/bin/pyenv" ]]; then log "ERROR" "pyenv executable not found in $PYENV_ROOT/bin/. Please reinstall pyenv." exit 1fi
# Function to update and upgrade packagesupdate_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 configurationexport 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 missinginstall_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 onceupdate_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 dependenciesinstall_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 filescleanup_temp_files() { rm -f /tmp/homebrew_updated}
# Register cleanup function to run on script exittrap cleanup_temp_files EXIT
# Install pyenvinstall_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 functioninstall_dependencies
# Define special cases dictionarytypeset -A special_casesspecial_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 caseshandle_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 handlinginstall_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 globalset_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 cacheclear_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 operationsupdate_and_upgrade_packages
# Call the clear_package_manager_cache function before any package operationsclear_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." fielse log "ERROR" "pyenv command not found. Skipping update."fi# --- End pyenv update ---
# Set the latest stable Python version as globalset_latest_stable_python_global
# Function to analyze pyenv environment structureanalyze_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 environmentslist_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 usedexpunge_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 ;; esacdoneshift $((OPTIND -1))# --- End argument parsing ---
# If command-line arguments are not provided, fall back to interactive inputif [ -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}'." fifi
# Handle special actions by setting env_number accordinglyif [ -n "$action" ]; then env_number="$action"fi
# Handle quit options (Esc or 'q') immediatelyif [[ "$env_number" == "q" || "$env_number" == $'\e' ]]; then log "ERROR" "Quitting script. No environment was activated." exit 0fi
# Handle deactivate option (D or d) immediatelyif [[ "$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 0fi
# Handle expunge option (e or E) immediatelyif [[ "$env_number" == "e" || "$env_number" == "E" ]]; then expunge_python_installation exit 0fi
# If env_number is a valid number, proceed; otherwise, handle invalid inputif [[ "$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 1fi
# Function to check if the environment is activated correctlycheck_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 environmentlog "NOTICE" "Activating the environment: ${MAGENTA}$selected_env${NC}."pyenv shell "$selected_env"
# Check if activation was successfulif [[ "$(pyenv version-name)" != "$selected_env" ]]; then log "ERROR" "Failed to activate the environment. Exiting." exit 1fi
log "SUCCESS" "Environment ${MAGENTA}$selected_env${NC} successfully activated."
# Modify PATH to prioritize the virtual environmentexport PATH="$VIRTUAL_ENV/bin:$PATH"
# Ensure VIRTUAL_ENV is set correctlyexport VIRTUAL_ENV="$(pyenv prefix)"
# Check if activate script existslog "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}" fielse log "ERROR" "Virtual environment directory not found: ${MAGENTA}$VIRTUAL_ENV${NC}"fi
# Set PYENV_VIRTUAL_ENV environment variableexport 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.pathlog "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 scriptlog "NOTICE" "Pip list summary:"python -c "import subprocessimport sysfrom 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 completenesslog "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 promptmodify_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 promptmodify_prompt
# Inform the user about the changeslog "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 directoryset_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 pathupdate_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-dateupdate_interpreter_path
# Check if .python-version file exists and handle accordinglyhandle_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 filehandle_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 0fi# --- End exit after activation ---
# Function to install packages, ensuring correct logginginstall_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 osimport astimport sysimport subprocessfrom collections import Counter
try: from importlib import metadataexcept 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 patternsgitignore_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 packagesneeded_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 packagestry: 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 selectionfzf_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 dependenciescheck_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, jsonpackages = 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, jsonpackages = 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, jsonpackages = 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 managementmanage_packages
# Function to check and request terminal permissionscheck_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 permissionshandle_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 scriptcheck_terminal_permissions
# Add environment cleanup functioncleanup_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 functionrecover_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}