Skip to content

Commit

Permalink
Added relevant code to show suggestions based on the new fix#97
Browse files Browse the repository at this point in the history
	Changed the code to work with the custom resolve context function:   click_repl/_completer.py
	Added checks for empty optional-Arguments:   click_repl/_repl.py
	Added InvalidGroupFormat Exception class:   click_repl/exceptions.py
	Added custom resolve context function:   click_repl/utils.py
  • Loading branch information
GhostOps77 committed May 21, 2023
1 parent f3694c7 commit 6b8617e
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 92 deletions.
95 changes: 35 additions & 60 deletions click_repl/_completer.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from __future__ import unicode_literals

import os
from glob import iglob

import click
from prompt_toolkit.completion import Completion, Completer

from .utils import split_arg_string
from .utils import _resolve_context, split_arg_string

__all__ = ["ClickCompleter"]

Expand All @@ -20,22 +18,21 @@
HAS_CLICK_V8 = True
AUTO_COMPLETION_PARAM = "shell_complete"
except (ImportError, ModuleNotFoundError):
import click._bashcomplete # type: ignore[import]
import click._bashcomplete

HAS_CLICK_V8 = False
AUTO_COMPLETION_PARAM = "autocompletion"


def text_type(text):
return u"{}".format(text)


class ClickCompleter(Completer):
__slots__ = ("cli", "ctx")
__slots__ = ("cli", "cli_ctx", "parsed_args", "parsed_ctx", "ctx_command")

def __init__(self, cli, ctx=None):
def __init__(self, cli, ctx):
self.cli = cli
self.ctx = ctx
self.cli_ctx = ctx
self.parsed_args = []
self.parsed_ctx = ctx
self.ctx_command = ctx.command

def _get_completion_from_autocompletion_functions(
self,
Expand All @@ -49,15 +46,13 @@ def _get_completion_from_autocompletion_functions(
if HAS_CLICK_V8:
autocompletions = param.shell_complete(autocomplete_ctx, incomplete)
else:
autocompletions = param.autocompletion( # type: ignore[attr-defined]
autocomplete_ctx, args, incomplete
)
autocompletions = param.autocompletion(autocomplete_ctx, args, incomplete)

for autocomplete in autocompletions:
if isinstance(autocomplete, tuple):
param_choices.append(
Completion(
text_type(autocomplete[0]),
str(autocomplete[0]),
-len(incomplete),
display_meta=autocomplete[1],
)
Expand All @@ -66,14 +61,10 @@ def _get_completion_from_autocompletion_functions(
elif HAS_CLICK_V8 and isinstance(
autocomplete, click.shell_completion.CompletionItem
):
param_choices.append(
Completion(text_type(autocomplete.value), -len(incomplete))
)
param_choices.append(Completion(autocomplete.value, -len(incomplete)))

else:
param_choices.append(
Completion(text_type(autocomplete), -len(incomplete))
)
param_choices.append(Completion(autocomplete, -len(incomplete)))

return param_choices

Expand All @@ -82,22 +73,22 @@ def _get_completion_from_choices_click_le_7(self, param, incomplete):
incomplete = incomplete.lower()
return [
Completion(
text_type(choice),
str(choice),
-len(incomplete),
display=text_type(repr(choice) if " " in choice else choice),
display=repr(choice) if " " in choice else choice,
)
for choice in param.type.choices # type: ignore[attr-defined]
for choice in param.type.choices
if choice.lower().startswith(incomplete)
]

else:
return [
Completion(
text_type(choice),
str(choice),
-len(incomplete),
display=text_type(repr(choice) if " " in choice else choice),
display=repr(choice) if " " in choice else choice,
)
for choice in param.type.choices # type: ignore[attr-defined]
for choice in param.type.choices
if choice.startswith(incomplete)
]

Expand Down Expand Up @@ -129,19 +120,17 @@ def _get_completion_for_Path_types(self, param, args, incomplete):

choices.append(
Completion(
text_type(path),
path,
-len(incomplete),
display=text_type(os.path.basename(path.strip("'\""))),
display=os.path.basename(path.strip("'\"")),
)
)

return choices

def _get_completion_for_Boolean_type(self, param, incomplete):
return [
Completion(
text_type(k), -len(incomplete), display_meta=text_type("/".join(v))
)
Completion(k, -len(incomplete), display_meta="/".join(v))
for k, v in {
"true": ("1", "true", "t", "yes", "y", "on"),
"false": ("0", "false", "f", "no", "n", "off"),
Expand All @@ -150,7 +139,6 @@ def _get_completion_for_Boolean_type(self, param, incomplete):
]

def _get_completion_from_params(self, autocomplete_ctx, args, param, incomplete):

choices = []
param_type = param.type

Expand Down Expand Up @@ -207,9 +195,9 @@ def _get_completion_for_cmd_args(
elif option.startswith(incomplete):
choices.append(
Completion(
text_type(option),
option,
-len(incomplete),
display_meta=text_type(param.help or ""),
display_meta=param.help or "",
)
)

Expand Down Expand Up @@ -249,52 +237,39 @@ def get_completions(self, document, complete_event=None):
# command, so give all relevant completions for this context.
incomplete = ""

# Resolve context based on click version
if HAS_CLICK_V8:
ctx = click.shell_completion._resolve_context(self.cli, {}, "", args)
else:
ctx = click._bashcomplete.resolve_ctx(self.cli, "", args)
if self.parsed_args != args:
self.parsed_args = args
self.parsed_ctx = _resolve_context(args, self.cli_ctx)
self.ctx_command = self.parsed_ctx.command

# if ctx is None:
# return # type: ignore[unreachable]

autocomplete_ctx = self.ctx or ctx
ctx_command = ctx.command

if getattr(ctx_command, "hidden", False):
if getattr(self.ctx_command, "hidden", False):
return

try:
choices.extend(
self._get_completion_for_cmd_args(
ctx_command, incomplete, autocomplete_ctx, args
self.ctx_command, incomplete, self.parsed_ctx, args
)
)

if isinstance(ctx_command, click.MultiCommand):
if isinstance(self.ctx_command, click.MultiCommand):
incomplete_lower = incomplete.lower()

for name in ctx_command.list_commands(ctx):
command = ctx_command.get_command(ctx, name)
for name in self.ctx_command.list_commands(self.parsed_ctx):
command = self.ctx_command.get_command(self.parsed_ctx, name)
if getattr(command, "hidden", False):
continue

elif name.lower().startswith(incomplete_lower):
choices.append(
Completion(
text_type(name),
name,
-len(incomplete),
display_meta=getattr(command, "short_help", ""),
)
)

except Exception as e:
click.echo("{}: {}".format(type(e).__name__, str(e)))

# If we are inside a parameter that was called, we want to show only
# relevant choices
# if param_called:
# choices = param_choices
click.echo(f"{type(e).__name__}: {e}")

for item in choices:
yield item
yield from choices
54 changes: 36 additions & 18 deletions click_repl/_repl.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
from __future__ import with_statement

import click
import sys
from prompt_toolkit import PromptSession
from prompt_toolkit.history import InMemoryHistory

from ._completer import ClickCompleter
from .exceptions import ClickExit # type: ignore[attr-defined]
from .exceptions import CommandLineParserError, ExitReplException
from .exceptions import ExitReplException, InvalidGroupFormat, ClickExit
from .utils import _execute_internal_and_sys_cmds


__all__ = ["bootstrap_prompt", "register_repl", "repl"]
__all__ = ["register_repl", "repl"]


def bootstrap_prompt(
Expand All @@ -29,29 +26,58 @@ def bootstrap_prompt(
defaults = {
"history": InMemoryHistory(),
"completer": ClickCompleter(group, ctx=ctx),
"message": u"> ",
"message": "> ",
}

defaults.update(prompt_kwargs)
return defaults


def repl(
old_ctx, prompt_kwargs={}, allow_system_commands=True, allow_internal_commands=True
old_ctx,
prompt_kwargs={},
allow_system_commands=True,
allow_internal_commands=True,
):
"""
Start an interactive shell. All subcommands are available in it.
:param old_ctx: The current Click context.
:param prompt_kwargs: Parameters passed to
:py:func:`prompt_toolkit.PromptSession`.
:param allow_system_commands: Allow system commands (prefix '!')
to be executed through REPL
:param allow_internal_commands: Allow internal commands (prefix ':')
to be executed through REPL
If stdin is not a TTY, no prompt will be printed, but only commands read
from stdin.
"""
# parent should be available, but we're not going to bother if not
group_ctx = old_ctx.parent or old_ctx

# Switching to the parent context that has a Group as its command
# as a Group acts as a CLI for all of its subcommands
if old_ctx.parent is not None and not isinstance(old_ctx.command, click.Group):
group_ctx = old_ctx.parent
else:
group_ctx = old_ctx

group = group_ctx.command

# An Optional click.Argument in the CLI Group, that has no value
# will consume the first word from the REPL input, causing issues in
# executing the command
# So, if there's an empty Optional Argument
for param in group.params:
if (
isinstance(param, click.Argument)
and group_ctx.params[param.name] is None
and not param.required
):
raise InvalidGroupFormat(
f"{type(group).__name__} '{group.name}' requires value for "
f"an optional argument '{param.name}' in REPL mode"
)

isatty = sys.stdin.isatty()

# Delete the REPL command from those available, as we don't want to allow
Expand All @@ -71,8 +97,6 @@ def repl(

if isatty:
prompt_kwargs = bootstrap_prompt(group, prompt_kwargs, group_ctx)

if isatty:
session = PromptSession(**prompt_kwargs)

def get_command():
Expand Down Expand Up @@ -102,20 +126,14 @@ def get_command():
if args is None:
continue

except CommandLineParserError:
continue

except ExitReplException:
break

try:
# The group command will dispatch based on args.
old_protected_args = group_ctx.protected_args
try:
group_ctx.protected_args = args
group.invoke(group_ctx)
finally:
group_ctx.protected_args = old_protected_args

except click.ClickException as e:
e.show()
except (ClickExit, SystemExit):
Expand Down
2 changes: 1 addition & 1 deletion click_repl/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class ExitReplException(InternalCommandException):
pass


class CommandLineParserError(Exception):
class InvalidGroupFormat(Exception):
pass


Expand Down
Loading

0 comments on commit 6b8617e

Please sign in to comment.