Skip to content

Commit

Permalink
Merge pull request #2363 from pypa/feature/repatch-shell-detection
Browse files Browse the repository at this point in the history
Upgrade Click-Completion and apply Shellingham for shell detection
  • Loading branch information
techalchemy authored Jun 15, 2018
2 parents 2244585 + adcb497 commit e326ce6
Show file tree
Hide file tree
Showing 15 changed files with 376 additions and 116 deletions.
38 changes: 19 additions & 19 deletions pipenv/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,19 @@
version_option,
BadParameter,
)
from click_completion import init as init_completion
from click_completion import get_code
from click_didyoumean import DYMCommandCollection

import click_completion
import crayons
import delegator

from .__version__ import __version__

from . import environments
from .environments import *
from .utils import is_valid_url

# Enable shell completion.
init_completion()
click_completion.init()
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])


Expand Down Expand Up @@ -80,10 +79,13 @@ def validate_python_path(ctx, param, value):
raise BadParameter('Expected Python at path %s does not exist' % value)
return value


def validate_pypi_mirror(ctx, param, value):
if value and not is_valid_url(value):
raise BadParameter('Invalid PyPI mirror URL: %s' % value)
return value


@group(
cls=PipenvGroup,
invoke_without_command=True,
Expand Down Expand Up @@ -163,19 +165,17 @@ def cli(
completion=False,
):
if completion: # Handle this ASAP to make shell startup fast.
if PIPENV_SHELL:
echo(
get_code(
shell=PIPENV_SHELL.split(os.sep)[-1], prog_name='pipenv'
)
)
else:
from . import shells
try:
shell = shells.detect_info()[0]
except shells.ShellDetectionFailure:
echo(
'Please ensure that the {0} environment variable '
'is set.'.format(crayons.normal('SHELL', bold=True)),
'Fail to detect shell. Please provide the {0} environment '
'variable.'.format(crayons.normal('PIPENV_SHELL', bold=True)),
err=True,
)
sys.exit(1)
print(click_completion.get_code(shell=shell, prog_name='pipenv'))
sys.exit(0)

from .core import (
Expand Down Expand Up @@ -238,7 +238,7 @@ def cli(
# --rm was passed...
elif rm:
# Abort if --system (or running in a virtualenv).
if PIPENV_USE_SYSTEM:
if environments.PIPENV_USE_SYSTEM:
echo(
crayons.red(
'You are attempting to remove a virtualenv that '
Expand Down Expand Up @@ -308,7 +308,7 @@ def cli(
)
@option(
'--pypi-mirror',
default=PIPENV_PYPI_MIRROR,
default=environments.PIPENV_PYPI_MIRROR,
nargs=1,
callback=validate_pypi_mirror,
help="Specify a PyPI mirror.",
Expand Down Expand Up @@ -467,7 +467,7 @@ def install(
)
@option(
'--pypi-mirror',
default=PIPENV_PYPI_MIRROR,
default=environments.PIPENV_PYPI_MIRROR,
nargs=1,
callback=validate_pypi_mirror,
help="Specify a PyPI mirror.",
Expand Down Expand Up @@ -518,7 +518,7 @@ def uninstall(
)
@option(
'--pypi-mirror',
default=PIPENV_PYPI_MIRROR,
default=environments.PIPENV_PYPI_MIRROR,
nargs=1,
callback=validate_pypi_mirror,
help="Specify a PyPI mirror.",
Expand Down Expand Up @@ -727,7 +727,7 @@ def check(
)
@option(
'--pypi-mirror',
default=PIPENV_PYPI_MIRROR,
default=environments.PIPENV_PYPI_MIRROR,
nargs=1,
callback=validate_pypi_mirror,
help="Specify a PyPI mirror.",
Expand Down Expand Up @@ -964,7 +964,7 @@ def run_open(module, three=None, python=None):
)
@option(
'--pypi-mirror',
default=PIPENV_PYPI_MIRROR,
default=environments.PIPENV_PYPI_MIRROR,
nargs=1,
callback=validate_pypi_mirror,
help="Specify a PyPI mirror.",
Expand Down
3 changes: 2 additions & 1 deletion pipenv/environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
bool(os.environ.get('PYENV_SHELL')) or bool(os.environ.get('PYENV_ROOT'))
)
SESSION_IS_INTERACTIVE = bool(os.isatty(sys.stdout.fileno()))
PIPENV_SHELL_EXPLICIT = os.environ.get('PIPENV_SHELL')
PIPENV_SHELL = os.environ.get('SHELL') or os.environ.get('PYENV_SHELL')
PIPENV_CACHE_DIR = os.environ.get('PIPENV_CACHE_DIR', user_cache_dir('pipenv'))
# Tells pipenv to override PyPI index urls with a mirror.
PIPENV_PYPI_MIRROR = os.environ.get('PIPENV_PYPI_MIRROR')
PIPENV_PYPI_MIRROR = os.environ.get('PIPENV_PYPI_MIRROR')
23 changes: 23 additions & 0 deletions pipenv/shells.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os

from .environments import PIPENV_SHELL_EXPLICIT, PIPENV_SHELL
from .vendor import shellingham


class ShellDetectionFailure(shellingham.ShellDetectionFailure):
pass


def _build_info(value):
return (os.path.splitext(os.path.basename(value))[0], value)


def detect_info():
if PIPENV_SHELL_EXPLICIT:
return _build_info(PIPENV_SHELL_EXPLICIT)
try:
return shellingham.detect_shell()
except (shellingham.ShellDetectionFailure, TypeError):
if PIPENV_SHELL:
return _build_info(PIPENV_SHELL)
raise ShellDetectionFailure
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -15,67 +15,17 @@

from click import echo, MultiCommand, Option, Argument, ParamType

__version__ = '0.2.1'
__version__ = '0.3.1'

_invalid_ident_char_re = re.compile(r'[^a-zA-Z0-9_]')

FISH_TEMPLATE = 'complete --command {{prog_name}} --arguments "(env {{complete_var}}=complete-fish COMMANDLINE=(commandline -cp){% for k, v in extra_env.items() %} {{k}}={{v}}{% endfor %} {{prog_name}})" -f'

ZSH_TEMPLATE = '''
#compdef {{prog_name}}
_{{prog_name}}() {
eval $(env COMMANDLINE="${words[1,$CURRENT]}" {{complete_var}}=complete-zsh {% for k, v in extra_env.items() %} {{k}}={{v}}{% endfor %} {{prog_name}})
}
if [[ "$(basename ${(%):-%x})" != "_{{prog_name}}" ]]; then
autoload -U compinit && compinit
compdef _{{prog_name}} {{prog_name}}
fi
'''

POWERSHELL_COMPLETION_SCRIPT = '''
if ((Test-Path Function:\TabExpansion) -and -not (Test-Path Function:\{{prog_name}}TabExpansionBackup)) {
Rename-Item Function:\TabExpansion {{prog_name}}TabExpansionBackup
}
class CompletionConfiguration(object):
def __init__(self):
self.complete_options = False

function TabExpansion($line, $lastWord) {
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
$aliases = @("{{prog_name}}") + @(Get-Alias | where { $_.Definition -eq "{{prog_name}}" } | select -Exp Name)
$aliasPattern = "($($aliases -join '|'))"
if($lastBlock -match "^$aliasPattern ") {
$Env:{{complete_var}} = "complete-powershell"
$Env:COMMANDLINE = "$lastBlock"
{%- for k, v in extra_env.items() %}
$Env:{{k}} = "{{v}}"
{%- endfor %}
({{prog_name}}) | ? {$_.trim() -ne "" }
Remove-Item Env:{{complete_var}}
Remove-Item Env:COMMANDLINE
{%- for k in extra_env.keys() %}
Remove-Item $Env:{{k}}
{%- endfor %}
}
elseif (Test-Path Function:\{{prog_name}}TabExpansionBackup) {
# Fall back on existing tab expansion
{{prog_name}}TabExpansionBackup $line $lastWord
}
}
'''

BASH_COMPLETION_SCRIPT = '''
_{{prog_name}}_completion() {
local IFS=$'\\t'
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
COMP_CWORD=$COMP_CWORD \\
{%- for k, v in extra_env.items() %}
{{k}}={{v}} \\
{%- endfor %}
{{complete_var}}=complete-bash $1 ) )
return 0
}

complete -F _{{prog_name}}_completion -o default {{prog_name}}
'''

_invalid_ident_char_re = re.compile(r'[^a-zA-Z0-9_]')
completion_configuration = CompletionConfiguration()


def resolve_ctx(cli, prog_name, args):
Expand Down Expand Up @@ -110,26 +60,23 @@ def get_choices(cli, prog_name, args, incomplete):
choices = []
if optctx:
choices += [c if isinstance(c, tuple) else (c, None) for c in optctx.type.complete(ctx, incomplete)]
elif incomplete and not incomplete[:1].isalnum():
for param in ctx.command.get_params(ctx):
if not isinstance(param, Option):
continue
for opt in param.opts:
if startswith(opt, incomplete):
choices.append((opt, param.help))
for opt in param.secondary_opts:
if startswith(opt, incomplete):
# don't put the doc so fish won't group the primary and
# and secondary options
choices.append((opt, None))
elif isinstance(ctx.command, MultiCommand):
for name in ctx.command.list_commands(ctx):
if startswith(name, incomplete):
choices.append((name, ctx.command.get_command_short_help(ctx, name)))
else:
for param in ctx.command.get_params(ctx):
if isinstance(param, Argument):
choices += [c if isinstance(c, tuple) else (c, None) for c in param.type.complete(ctx, incomplete)]
if (completion_configuration.complete_options or incomplete and not incomplete[:1].isalnum()) and isinstance(param, Option):
for opt in param.opts:
if startswith(opt, incomplete):
choices.append((opt, param.help))
for opt in param.secondary_opts:
if startswith(opt, incomplete):
# don't put the doc so fish won't group the primary and
# and secondary options
choices.append((opt, None))
if isinstance(ctx.command, MultiCommand):
for name in ctx.command.list_commands(ctx):
if startswith(name, incomplete):
choices.append((name, ctx.command.get_command_short_help(ctx, name)))

for item, help in choices:
yield (item, help)
Expand Down Expand Up @@ -197,7 +144,7 @@ def do_fish_complete(cli, prog_name):

for item, help in get_choices(cli, prog_name, args, incomplete):
if help:
echo("%s\t%s" % (item, help))
echo("%s\t%s" % (item, re.sub('\s', ' ', help)))
else:
echo(item)

Expand All @@ -221,7 +168,10 @@ def escape(s):
res.append('"%s"\:"%s"' % (escape(item), escape(help)))
else:
res.append('"%s"' % escape(item))
echo("_arguments '*: :((%s))'" % '\n'.join(res))
if res:
echo("_arguments '*: :((%s))'" % '\n'.join(res))
else:
echo("_files")

return True

Expand Down Expand Up @@ -330,13 +280,24 @@ def _shellcomplete(cli, prog_name, complete_var=None):
sys.exit()


def init():
"""patch click to support enhanced completion"""
import click
click.types.ParamType.complete = param_type_complete
click.types.Choice.complete = choice_complete
click.core.MultiCommand.get_command_short_help = multicommand_get_command_short_help
click.core._bashcomplete = _shellcomplete
_initialized = False


def init(complete_options=False):
"""Initialize the enhanced click completion
Args:
complete_options (bool): always complete the options, even when the user hasn't typed a first dash
"""
global _initialized
if not _initialized:
import click
click.types.ParamType.complete = param_type_complete
click.types.Choice.complete = choice_complete
click.core.MultiCommand.get_command_short_help = multicommand_get_command_short_help
click.core._bashcomplete = _shellcomplete
completion_configuration.complete_options = complete_options
_initialized = True


class DocumentedChoice(ParamType):
Expand Down Expand Up @@ -382,25 +343,15 @@ def complete(self, ctx, incomplete):

def get_code(shell=None, prog_name=None, env_name=None, extra_env=None):
"""Return the specified completion code"""
from jinja2 import Template
from jinja2 import Environment, FileSystemLoader
if shell in [None, 'auto']:
shell = get_auto_shell()
prog_name = prog_name or click.get_current_context().find_root().info_name
env_name = env_name or '_%s_COMPLETE' % prog_name.upper().replace('-', '_')
extra_env = extra_env if extra_env else {}
if shell == 'fish':
return Template(FISH_TEMPLATE).render(prog_name=prog_name, complete_var=env_name, extra_env=extra_env)
elif shell == 'bash':
return Template(BASH_COMPLETION_SCRIPT).render(prog_name=prog_name, complete_var=env_name, extra_env=extra_env)
elif shell == 'zsh':
return Template(ZSH_TEMPLATE).render(prog_name=prog_name, complete_var=env_name, extra_env=extra_env)
elif shell == 'powershell':
return Template(POWERSHELL_COMPLETION_SCRIPT).render(prog_name=prog_name, complete_var=env_name, extra_env=extra_env)
else:
raise click.ClickException('%s is not supported.' % shell)



env = Environment(loader=FileSystemLoader(os.path.dirname(__file__)))
template = env.get_template('%s.j2' % shell)
return template.render(prog_name=prog_name, complete_var=env_name, extra_env=extra_env)


def get_auto_shell():
Expand Down
12 changes: 12 additions & 0 deletions pipenv/vendor/click_completion/bash.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
_{{prog_name}}_completion() {
local IFS=$'\t'
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \
COMP_CWORD=$COMP_CWORD \
{%- for k, v in extra_env.items() %}
{{k}}={{v}} \
{%- endfor %}
{{complete_var}}=complete-bash $1 ) )
return 0
}

complete -F _{{prog_name}}_completion -o default {{prog_name}}
1 change: 1 addition & 0 deletions pipenv/vendor/click_completion/fish.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
complete --command {{prog_name}} --arguments "(env {{complete_var}}=complete-fish COMMANDLINE=(commandline -cp){% for k, v in extra_env.items() %} {{k}}={{v}}{% endfor %} {{prog_name}})" -f
26 changes: 26 additions & 0 deletions pipenv/vendor/click_completion/powershell.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
if ((Test-Path Function:\TabExpansion) -and -not (Test-Path Function:\{{prog_name}}TabExpansionBackup)) {
Rename-Item Function:\TabExpansion {{prog_name}}TabExpansionBackup
}

function TabExpansion($line, $lastWord) {
$lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
$aliases = @("{{prog_name}}") + @(Get-Alias | where { $_.Definition -eq "{{prog_name}}" } | select -Exp Name)
$aliasPattern = "($($aliases -join '|'))"
if($lastBlock -match "^$aliasPattern ") {
$Env:{{complete_var}} = "complete-powershell"
$Env:COMMANDLINE = "$lastBlock"
{%- for k, v in extra_env.items() %}
$Env:{{k}} = "{{v}}"
{%- endfor %}
({{prog_name}}) | ? {$_.trim() -ne "" }
Remove-Item Env:{{complete_var}}
Remove-Item Env:COMMANDLINE
{%- for k in extra_env.keys() %}
Remove-Item $Env:{{k}}
{%- endfor %}
}
elseif (Test-Path Function:\{{prog_name}}TabExpansionBackup) {
# Fall back on existing tab expansion
{{prog_name}}TabExpansionBackup $line $lastWord
}
}
Loading

0 comments on commit e326ce6

Please sign in to comment.