Last active
February 6, 2023 00:53
-
-
Save blueyed/54a257c411310a28805a to your computer and use it in GitHub Desktop.
Zsh completion for pip (a tool for installing and managing Python packages), to be added to Zsh soonish.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#compdef -P pip[0-9.]# | |
# | |
# Completion script for pip (http://pypi.python.org/pypi/pip). | |
# | |
# Taken from https://github.com/zsh-users/zsh-completions/commit/890f3701 | |
# (where it got removed). Original source: | |
# https://github.com/technolize/zsh-completion-funcs. | |
# | |
# Currently maintained in https://gist.github.com/blueyed/54a257c411310a28805a. | |
# Setup $python, based on pip version from word[1]. | |
# Must get done early, otherwise $words might have been changed already. | |
local python pip | |
pip=${words[1]} | |
python=${${pip}/pip/python} | |
[[ $python == $pip ]] && python=python | |
# The index(es) to use. The simple index only provides a full list, while | |
# the xmlrpc index can be queried by "last updated in". | |
# This should be probably made configurable through zstyle and then be used | |
# in the cache name. | |
# For the xmlrpc index, the "days" could be made configurable | |
typeset -a ZSH_PIP_INDEXES ZSH_PIP_XMLRPC_INDEXES | |
# Enabling this would additionally use the simple PyPI index, which does not | |
# support limiting by date. | |
# ZSH_PIP_INDEXES=(https://pypi.python.org/simple/) | |
# The XMLRPC index, which supports filtering by "last updated since". | |
ZSH_PIP_XMLRPC_INDEXES=(https://pypi.python.org/pypi) | |
# Using packages updated in the last year results in ~26798 packages currently. | |
local ZSH_PIP_XMLRPC_INDEX_DAYS=365 | |
# Get all packages from (remote) indexes. | |
_pip_all() { | |
_pip_set_cache_policy | |
if ( (( $+__zsh_pip_all_pkgs )) && ! _cache_invalid pip_allpkgs ) \ | |
|| _retrieve_cache pip_allpkgs; then | |
return | |
fi | |
typeset -a -g __zsh_pip_all_pkgs | |
if zstyle -T ":completion:${curcontext}:" remote-access; then | |
typeset -a new_zsh_all_pkgs | |
local ts_xmlrpc_start | |
if zmodload -F zsh/datetime +p:EPOCHSECONDS 2>/dev/null; then | |
ts_xmlrpc_start=$((EPOCHSECONDS - (86400 * ZSH_PIP_XMLRPC_INDEX_DAYS))) | |
else | |
ts_xmlrpc_start=$(date +%s -d @$(($(date +%s) - (86400 * ZSH_PIP_XMLRPC_INDEX_DAYS)))) | |
fi | |
# Build XMLPC request to pypi.python.org to get a list of packages updated | |
# in the last year. This is meant to reduce the number of packages. | |
local xml_req="<?xml version='1.0'?><methodCall><methodName>updated_releases</methodName><params><param><value><int>${ts_xmlrpc_start}</int></value></param></params></methodCall>" | |
# TODO: Standard error is not redirected | |
for xmlrpc_index in $ZSH_PIP_XMLRPC_INDEXES; do | |
new_zsh_all_pkgs+=($(_call_program fetch-all-xml "echo ${(qqqq)xml_req} \ | |
| curl -s --max-time 30 -d @- -H \"Content-Type: text/xml\" $xmlrpc_index \ | |
| sed -n '/^<value><string>/p' | sed -n '1~2 s/^<value><string>//p' | sed -n 's/<\/string><\/value>$//p' \ | |
| tr '\n' ' '") ) | |
done | |
for index in $ZSH_PIP_INDEXES; do | |
# All packages, more than 54000! | |
new_zsh_all_pkgs+=( $(curl -s --max-time 30 $index \ | |
| sed -n '/<a href/ s/.*>\([^<]\{1,\}\).*/\1/p' \ | |
| tr '\n' ' ') ) | |
done | |
if [[ $new_zsh_all_pkgs ]]; then | |
__zsh_pip_all_pkgs=($new_zsh_all_pkgs) | |
_store_cache pip_allpkgs __zsh_pip_all_pkgs | |
else | |
_message "Could not update package cache." | |
fi | |
fi | |
} | |
# Get installed packages, using pips completion interface. | |
_pip_installed() { | |
installed_pkgs=($(_call_program fetch-installed \ | |
"env COMP_WORDS='pip uninstall' COMP_CWORD=2 PIP_AUTO_COMPLETE=1 $pip")) | |
# TODO: using a cache might be benefical, but needs to cache by the "pip" | |
# being used, which seems not to be worth it. | |
# _pip_set_cache_policy | |
# | |
# if ( ! (( $+__zsh_pip_installed_pkgs )) || _cache_invalid pip_installedpkgs ) && | |
# ! _retrieve_cache pip_installedpkgs; | |
# then | |
# typeset -a -g __zsh_pip_installed_pkgs | |
# # __zsh_pip_installed_pkgs=($($pip freeze | cut -d '=' -f 1)) | |
# __zsh_pip_installed_pkgs=($(_call_program fetch-installed \ | |
# "env COMP_WORDS='pip uninstall' COMP_CWORD=2 PIP_AUTO_COMPLETE=1 $pip")) | |
# _store_cache pip_installedpkgs __zsh_pip_installed_pkgs | |
# fi | |
} | |
_pip_set_cache_policy() { | |
local cache_policy | |
zstyle -s ":completion:${curcontext}:" cache-policy cache_policy | |
if [[ -z "$cache_policy" ]]; then | |
zstyle ":completion:${curcontext}:" cache-policy _pip_caching_policy | |
fi | |
} | |
_pip_caching_policy() { | |
if [[ ${1:t} == pip_allpkgs ]]; then | |
# rebuild if cache is more than two weeks old | |
local -a oldp | |
oldp=( "$1"(Nm+14) ) | |
(( $#oldp )) | |
else # pip_installedpkgs | |
# Compare cache file's timestamp to the most recently modified sys.path entry. | |
# This gets changed/touched when installing removing packages. | |
local newest_sys_path=$($python -c ' | |
import sys | |
from os.path import exists, getmtime | |
print(sorted(sys.path, key=lambda x: exists(x) and getmtime(x))[-1])') | |
[[ $newest_sys_path -nt $1 ]] | |
fi | |
} | |
local -a common_ops | |
common_ops=( | |
'(* : -)'{-h,--help}"[show help]" | |
\*{-v,--verbose}"[give more output]" | |
{-V,--version}"[show version and exit]" | |
{-q,--quiet}"[give less output]" | |
"--log=[log file where a complete record will be kept]:file" | |
"--proxy=[specify a proxy in the form user:[email protected]:port]:proxy" | |
"--timeout=[set the socket timeout (default 15 seconds)]:second" | |
"--exists-action=[default action when a path already exists: (s)witch, (i)gnore, (w)ipe, (b)ackup]:action" | |
"--cert=[path to alternate CA bundle]:path" | |
) | |
_requirements_file () { | |
# Prefer requirement* and *.txt pattern. This falls back to "all files", | |
# according to the file-patterns style. | |
_wanted files expl file _files -g 'requirement*(-.)' -g '*.txt(-.)' "$@" - | |
} | |
typeset -A opt_args | |
local curcontext="$curcontext" state line ret=1 | |
curcontext="${curcontext%:*}-$words[2]:" | |
_arguments -C \ | |
':subcommand:->subcommand' \ | |
$common_ops \ | |
'*::options:->options' && ret=0 | |
case $state in | |
subcommand) | |
local -a subcommands | |
subcommands=( | |
"install:install packages" | |
"uninstall:uninstall packages" | |
"freeze:output installed packages in requirements format" | |
"list:list installed packages" | |
"show:show information about installed packages" | |
"search:search PyPI for packages" | |
"wheel:build wheels from your requirements" | |
"help:show available commands" | |
"zip:zip dividual packages" | |
"unzip:unzip undividual packages" | |
"bundle:create pybundle" | |
) | |
_describe -t subcommands 'pip subcommand' subcommands && ret=0 | |
;; | |
options) | |
local -a args | |
args=( | |
$common_ops | |
) | |
local -a pi_ops | |
pi_ops=( | |
{-i,--index-url=}"[base URL of Python Package Index]:URL" | |
"--extra-index-url=[extra URLs of package indexes to use in addition to --index-url]:URL" | |
"--no-index[ignore package index (only looking at --find-links URLs instead)]" | |
{-f,--find-links=}"[URL to look for packages at]:URL" | |
{-M,--use-mirrors}"[use the PyPI mirrors as a fallback in case the main index is down]" | |
"--mirrors=[specific mirror URLs to query when --use-mirrors is used]:URL" | |
"--allow-external=[allow the installation of externally hosted files]:package" | |
"--allow-all-external[allow the installation of all externally hosted files]" | |
"--no-allow-external[disallow the installation of all externally hosted files]" | |
"--allow-insecure=[allow the installation of insecure and unverifiable files]:package" | |
"--no-allow-insecure[disallow the installation of insecure and unverifiable files]" | |
) | |
case $words[1] in | |
install | bundle) | |
args+=( | |
{-e,--editable=}"[install a package directly from a checkout]:directory or VCS+REPOS_URL[@REV]#egg=PACKAGE:_files -/" | |
{-r,--requirement=}"[install all the packages listed in the given requirements file]:requirements file:_requirements_file" | |
{-b,--build=}"[unpack packages into DIR]:directory:_directories" | |
{-t,--target=}"[install packages into DIR]:directory:_directories" | |
{-d,--download=}"[download packages into DIR instead of installing them]:directory:_directories" | |
"--download-cache=[cache downloaded packages in DIR]:directory:_directories" | |
"--src=[check out --editable packages into DIR]:directory:_directories" | |
{-U,--upgrade}"[upgrade all packages to the newest available version]" | |
"--force-reinstall[when upgrading, reinstall all packages even if they are already up-to-date]" | |
{-I,--ignore-installed}"[ignore the installed packages]" | |
"--no-deps[don't install package dependencies]" | |
"--no-install[download and unpack all packages, but don't actually install them]" | |
"--no-download[don't download any packages, just install the ones already downloaded]" | |
"--install-option=[extra arguments to be supplied to the setup.py install command]:options" | |
"--global-option=[extra global options to be supplied to the setup.py call before the install command]:options" | |
"--user[install using the user scheme]" | |
"--egg[install as self contained egg file, like easy_install does]" | |
"--root=[install everything relative to this alternate root directory]:directory:_directories" | |
"--use-wheel[find and prefer wheel archives when searching indexes and find-links locations]" | |
"--pre[include pre-release and development versions]" | |
"--no-clean[don't clean up build directories]" | |
':package name:->pkgs_all_and_dirs' | |
$pi_ops | |
) | |
;; | |
uninstall) | |
args+=( | |
{-r,--requirement=}"[install all the packages listed in the given requirements file]::requirements file:_requirements_file" | |
{-y,--yes}"[don't ask for confirmation of uninstall deletions]" | |
':installed package:->pkgs_installed' | |
) | |
;; | |
freeze) | |
args+=( | |
{-r,--requirement=}"[install all the packages listed in the given requirements file]::requirements file:_requirements_file" | |
{-f,--find-links=}"[URL to look for packages at]:URL" | |
{-l,--local}"[If in a virtualenv that has global access, do not list globally-installed packages]" | |
) | |
;; | |
list) | |
args+=( | |
{-o,--outdated}"[list outdated packages (excluding editables)]" | |
{-u,--uptodated}"[list uptodated packages (excluding editables)]" | |
{-e,--editable}"[list editable projects]" | |
{-l,--local}"[If in a virtualenv that has global access, do not list globally-installed packages]" | |
"--pre[include pre-release and development versions]" | |
$pi_ops | |
) | |
;; | |
show) | |
args+=( | |
{-f,--files}"[show the full list of installed files for each package]" | |
':installed package:->pkgs_installed' | |
) | |
;; | |
search) | |
args+=( | |
"--index[base URL of Python Package Index]:URL" | |
) | |
;; | |
wheel) | |
args+=( | |
{-w,--wheel-dir=}"[build wheels into DIR, where the default is '<cwd>/wheelhouse']:directory:_directories" | |
"--use-wheel[find and prefer wheel archives when searching indexes and find-links locations]" | |
"--build-option=[extra arguments to be supplied to 'setup.py bdist_wheel']:options" | |
{-r,--requirement=}"[install all the packages listed in the given requirements file]::requirements file:_requirements_file" | |
"--download-cache=[cache downloaded packages in DIR]:directory:_directories" | |
"--no-deps[don't install package dependencies]" | |
{-b,--build=}"[directory to unpack packages into and build in]:directory:_directories" | |
"--global-option=[extra global options to be supplied to the setup.py call before the 'bdist_wheel' command]:options" | |
"--pre[include pre-release and development versions]" | |
"--no-clean[don't clean up build directories]" | |
$pi_ops | |
) | |
;; | |
unzip | zip) | |
args+=( | |
"--unzip[unzip a package]" | |
"--no-pyc[do not include .pyc files in zip files]" | |
{-l,--list}"[list the packages available, and their zip status]" | |
"--sort-files[with --list, sort packages according to how many files they contain]" | |
"--path=[restrict operation to the given paths]:paths" | |
{-n,--simulate}"[do not actually perform the zip/unzip operation]" | |
) | |
;; | |
esac | |
_arguments $args && ret=0 | |
# Additional states for expensive actions. | |
case $state in | |
pkgs_all_and_dirs) | |
_pip_all | |
_wanted all-packages expl 'packages' compadd -a __zsh_pip_all_pkgs && ret=0 | |
# TODO: only let _files fallback to dirs? Use **/setup.py with _path_files and some limiting? | |
_wanted directories expl 'directory with setup.py' _files -g '*/setup.py(:h/)' -s /setup.py && ret=0 | |
;; | |
pkgs_installed) | |
local -a installed_pkgs | |
_pip_installed | |
_wanted installed-package expl 'installed packages' compadd -a installed_pkgs && ret=0 | |
;; | |
esac | |
;; | |
esac | |
return ret |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment