Zsh Mailing List Archive
Messages sorted by: Reverse Date, Date, Thread, Author

Completion for pip



Hi,

some time ago I've adopted an existing completion script for pip, which
has been removed from https://github.com/zsh-users/zsh-completions
(in https://github.com/zsh-users/zsh-completions/commit/890f3701).

I've also looked at oh-my-zsh's plugin
(https://github.com/robbyrussell/oh-my-zsh/blob/master/plugins/pip/_pip),
where I've adopted the idea to provide the list of installable packages
from.

I've enhanced it to provide completion for installable packages (both
using the PyPI's simple and  XMLRPC interface), and made it use pip's
completion interface to get the list of un-installable packages.
There are other fixes, like for completing filenames with
"pip install -r".

It is currently maintained at:
https://gist.github.com/blueyed/54a257c411310a28805a
I am attaching the current revision (cbd8ec9a).

I would appreciate feedback on it and then think it would be nice to
have it included in Zsh itself.

zsh-users/zsh-completions's policy is to not provide completion scripts,
if upstream does so - but pip's distributed completion is very limited:

    %  pip completion --zsh

    # pip zsh completion start
    function _pip_completion {
      local words cword
      read -Ac words
      read -cn cword
      reply=( $( COMP_WORDS="$words[*]" \
                 COMP_CWORD=$(( cword-1 )) \
                 PIP_AUTO_COMPLETE=1 $words[1] ) )
    }
    compctl -K _pip_completion pip
    # pip zsh completion end


Thanks,
Daniel.
#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.

local ret=1 state

# 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
# ZSH_PIP_INDEXES=(https://pypi.python.org/simple/)
ZSH_PIP_XMLRPC_INDEXES=(https://pypi.python.org/pypi)
local ZSH_PIP_XMLRPC_INDEX_DAYS=365


# Get all packages from (remote) ZSH_PIP_INDEXES.
_pip_all() {
  _pip_set_cache_policy
  if ( ! (( $+_zsh_all_pkgs )) || _cache_invalid pip_allpkgs ) &&
    ! _retrieve_cache pip_allpkgs;
  then
    typeset -a -g _zsh_all_pkgs
    _zsh_all_pkgs=()

    # 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 ts_1year=$(date +%s -d @$(( $(date +%s) - 86400 * ZSH_PIP_XMLRPC_INDEX_DAYS )))
    local xml_req="<?xml version='1.0'?><methodCall><methodName>updated_releases</methodName><params><param><value><int>${ts_1year}</int></value></param></params></methodCall>"

    for xmlrpc_index in $ZSH_PIP_XMLRPC_INDEXES; do
      _zsh_all_pkgs+=( $(echo $xml_req | curl -s -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!
      _zsh_all_pkgs+=( $(curl -s $index \
        | sed -n '/<a href/ s/.*>\([^<]\{1,\}\).*/\1/p' \
        | tr '\n' ' ') )
    done
    _store_cache pip_allpkgs _zsh_all_pkgs
  fi
}

# Get installed packages, using pips completion interface.
_pip_installed() {
  _pip_set_cache_policy

  if ( ! (( $+_zsh_installed_pkgs )) || _cache_invalid pip_installedpkgs ) &&
    ! _retrieve_cache pip_installedpkgs;
  then
    typeset -a -g _zsh_installed_pkgs
    # _zsh_installed_pkgs=($($pip freeze | cut -d '=' -f 1))
    _zsh_installed_pkgs=($(COMP_WORDS="pip uninstall" COMP_CWORD="2" PIP_AUTO_COMPLETE=1 $pip))
    _store_cache pip_installedpkgs _zsh_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:passwd@proxy.server: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"
)

_directories () {
  _wanted directories expl directory _path_files -/ "$@" -
}

_requirements_file () {
  # Prefer requirement* and *.txt pattern. This should fall back to "all files", according to the file-patterns style.
  _wanted files expl file _files -g 'requirement*' -g '*.txt' "$@" -
}

typeset -A opt_args
_arguments \
  ':subcommand:->subcommand' \
  $common_ops \
  '*::options:->options' && ret=0

case $state in
  subcommand)
    local -a subcommands
    subcommands=(
      "install:install packages"
      "uninstall:uninstall packages"
      "freeze:put all currently installed packages"
      "list:list installed packages"
      "show:show information about installed packages"
      "search:search pypi"
      "wheel:build wheels from your requirements"
      "zip:zip dividual packages"
      "unzip:unzip undividual packages"
      "bundle:create pybundle"
      "help:show available commands"
    )

    _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'

          $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 state for expensive actions.
    case $state in
      pkgs_all)
          _pip_all
          _wanted _zsh_all_pkgs expl 'packages' compadd -a _zsh_all_pkgs && ret=0
        ;;
      pkgs_installed)
        _pip_installed
        _wanted _zsh_installed_pkgs expl 'installed packages' compadd -a _zsh_installed_pkgs && ret=0
        ;;
    esac
  ;;
esac

return ret

# Local Variables:
# mode: Shell-Script
# sh-indentation: 2
# indent-tabs-mode: nil
# sh-basic-offset: 2
# End:
# vim: ft=zsh sw=2 ts=2 et

Attachment: signature.asc
Description: OpenPGP digital signature



Messages sorted by: Reverse Date, Date, Thread, Author