add
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import typing as t
|
||||
from glob import iglob
|
||||
from typing import Generator
|
||||
|
||||
import click
|
||||
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
||||
from prompt_toolkit.document import Document
|
||||
|
||||
from .utils import _resolve_context, split_arg_string
|
||||
|
||||
__all__ = ["ClickCompleter"]
|
||||
|
||||
IS_WINDOWS = os.name == "nt"
|
||||
|
||||
|
||||
# Handle backwards compatibility between Click<=7.0 and >=8.0
|
||||
try:
|
||||
import click.shell_completion
|
||||
|
||||
HAS_CLICK_V8 = True
|
||||
AUTO_COMPLETION_PARAM = "shell_complete"
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
import click._bashcomplete # type: ignore[import]
|
||||
|
||||
HAS_CLICK_V8 = False
|
||||
AUTO_COMPLETION_PARAM = "autocompletion"
|
||||
|
||||
|
||||
class ClickCompleter(Completer):
|
||||
__slots__ = ("cli", "ctx", "parsed_args", "parsed_ctx", "ctx_command")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cli: click.MultiCommand,
|
||||
ctx: click.Context,
|
||||
show_only_unused: bool = False,
|
||||
shortest_only: bool = False,
|
||||
) -> None:
|
||||
self.cli = cli
|
||||
self.ctx = ctx
|
||||
self.parsed_args: list[str] = []
|
||||
self.parsed_ctx = ctx
|
||||
self.ctx_command = ctx.command
|
||||
self.show_only_unused = show_only_unused
|
||||
self.shortest_only = shortest_only
|
||||
|
||||
def _get_completion_from_autocompletion_functions(
|
||||
self,
|
||||
param: click.Parameter,
|
||||
autocomplete_ctx: click.Context,
|
||||
args: list[str],
|
||||
incomplete: str,
|
||||
) -> list[Completion]:
|
||||
param_choices: list[Completion] = []
|
||||
|
||||
if HAS_CLICK_V8:
|
||||
autocompletions = param.shell_complete(autocomplete_ctx, incomplete)
|
||||
else:
|
||||
autocompletions = param.autocompletion( # type: ignore[attr-defined]
|
||||
autocomplete_ctx, args, incomplete
|
||||
)
|
||||
|
||||
for autocomplete in autocompletions:
|
||||
if isinstance(autocomplete, tuple):
|
||||
param_choices.append(
|
||||
Completion(
|
||||
str(autocomplete[0]),
|
||||
-len(incomplete),
|
||||
display_meta=autocomplete[1],
|
||||
)
|
||||
)
|
||||
|
||||
elif HAS_CLICK_V8 and isinstance(
|
||||
autocomplete, click.shell_completion.CompletionItem
|
||||
):
|
||||
param_choices.append(Completion(autocomplete.value, -len(incomplete)))
|
||||
|
||||
else:
|
||||
param_choices.append(Completion(str(autocomplete), -len(incomplete)))
|
||||
|
||||
return param_choices
|
||||
|
||||
def _get_completion_from_choices_click_le_7(
|
||||
self, param: click.Parameter, incomplete: str
|
||||
) -> list[Completion]:
|
||||
param_type = t.cast(click.Choice, param.type)
|
||||
|
||||
if not getattr(param.type, "case_sensitive", True):
|
||||
incomplete = incomplete.lower()
|
||||
return [
|
||||
Completion(
|
||||
choice,
|
||||
-len(incomplete),
|
||||
display=repr(choice) if " " in choice else choice,
|
||||
)
|
||||
for choice in param_type.choices # type: ignore[attr-defined]
|
||||
if choice.lower().startswith(incomplete)
|
||||
]
|
||||
|
||||
else:
|
||||
return [
|
||||
Completion(
|
||||
choice,
|
||||
-len(incomplete),
|
||||
display=repr(choice) if " " in choice else choice,
|
||||
)
|
||||
for choice in param_type.choices # type: ignore[attr-defined]
|
||||
if choice.startswith(incomplete)
|
||||
]
|
||||
|
||||
def _get_completion_for_Path_types(
|
||||
self, param: click.Parameter, args: list[str], incomplete: str
|
||||
) -> list[Completion]:
|
||||
if "*" in incomplete:
|
||||
return []
|
||||
|
||||
choices: list[Completion] = []
|
||||
_incomplete = os.path.expandvars(incomplete)
|
||||
search_pattern = _incomplete.strip("'\"\t\n\r\v ").replace("\\\\", "\\") + "*"
|
||||
quote = ""
|
||||
|
||||
if " " in _incomplete:
|
||||
for i in incomplete:
|
||||
if i in ("'", '"'):
|
||||
quote = i
|
||||
break
|
||||
|
||||
for path in iglob(search_pattern):
|
||||
if " " in path:
|
||||
if quote:
|
||||
path = quote + path
|
||||
else:
|
||||
if IS_WINDOWS:
|
||||
path = repr(path).replace("\\\\", "\\")
|
||||
else:
|
||||
if IS_WINDOWS:
|
||||
path = path.replace("\\", "\\\\")
|
||||
|
||||
choices.append(
|
||||
Completion(
|
||||
path,
|
||||
-len(incomplete),
|
||||
display=os.path.basename(path.strip("'\"")),
|
||||
)
|
||||
)
|
||||
|
||||
return choices
|
||||
|
||||
def _get_completion_for_Boolean_type(
|
||||
self, param: click.Parameter, incomplete: str
|
||||
) -> list[Completion]:
|
||||
boolean_mapping: dict[str, tuple[str, ...]] = {
|
||||
"true": ("1", "true", "t", "yes", "y", "on"),
|
||||
"false": ("0", "false", "f", "no", "n", "off"),
|
||||
}
|
||||
|
||||
return [
|
||||
Completion(k, -len(incomplete), display_meta="/".join(v))
|
||||
for k, v in boolean_mapping.items()
|
||||
if any(i.startswith(incomplete) for i in v)
|
||||
]
|
||||
|
||||
def _get_completion_from_params(
|
||||
self,
|
||||
autocomplete_ctx: click.Context,
|
||||
args: list[str],
|
||||
param: click.Parameter,
|
||||
incomplete: str,
|
||||
) -> list[Completion]:
|
||||
choices: list[Completion] = []
|
||||
param_type = param.type
|
||||
|
||||
# Flags (is_flag=True, incl. secondary opts like --foo/--no-foo) do not
|
||||
# consume a value, so we must not offer any value completions for them.
|
||||
# See https://github.com/click-contrib/click-repl/issues/116
|
||||
if getattr(param, "is_flag", False):
|
||||
return choices
|
||||
|
||||
# shell_complete method for click.Choice was introduced in Click v8
|
||||
if not HAS_CLICK_V8 and isinstance(param_type, click.Choice):
|
||||
choices.extend(
|
||||
self._get_completion_from_choices_click_le_7(param, incomplete)
|
||||
)
|
||||
|
||||
elif isinstance(param_type, click.types.BoolParamType):
|
||||
choices.extend(self._get_completion_for_Boolean_type(param, incomplete))
|
||||
|
||||
elif isinstance(param_type, (click.Path, click.File)):
|
||||
choices.extend(self._get_completion_for_Path_types(param, args, incomplete))
|
||||
|
||||
elif getattr(param, AUTO_COMPLETION_PARAM, None) is not None:
|
||||
choices.extend(
|
||||
self._get_completion_from_autocompletion_functions(
|
||||
param,
|
||||
autocomplete_ctx,
|
||||
args,
|
||||
incomplete,
|
||||
)
|
||||
)
|
||||
|
||||
return choices
|
||||
|
||||
def _get_completion_for_cmd_args(
|
||||
self,
|
||||
ctx_command: click.Command,
|
||||
incomplete: str,
|
||||
autocomplete_ctx: click.Context,
|
||||
args: list[str],
|
||||
) -> list[Completion]:
|
||||
choices: list[Completion] = []
|
||||
param_called = False
|
||||
|
||||
for param in ctx_command.params:
|
||||
if isinstance(param.type, click.types.UnprocessedParamType):
|
||||
return []
|
||||
|
||||
elif getattr(param, "hidden", False):
|
||||
continue
|
||||
|
||||
elif isinstance(param, click.Option):
|
||||
opts = param.opts + param.secondary_opts
|
||||
previous_args = args[: param.nargs * -1]
|
||||
current_args = args[param.nargs * -1 :]
|
||||
|
||||
# Show only unused opts
|
||||
already_present = any([opt in previous_args for opt in opts])
|
||||
hide = self.show_only_unused and already_present and not param.multiple
|
||||
|
||||
# Show only shortest opt
|
||||
if (
|
||||
self.shortest_only
|
||||
and not incomplete # just typed a space
|
||||
# not selecting a value for a longer version of this option
|
||||
and (not args or args[-1] not in opts)
|
||||
):
|
||||
opts = [min(opts, key=len)]
|
||||
|
||||
for option in opts:
|
||||
# We want to make sure if this parameter was called
|
||||
# If we are inside a parameter that was called, we want to show only
|
||||
# relevant choices
|
||||
if option in current_args: # noqa: E203
|
||||
param_called = True
|
||||
break
|
||||
|
||||
elif option.startswith(incomplete) and not hide:
|
||||
choices.append(
|
||||
Completion(
|
||||
option,
|
||||
-len(incomplete),
|
||||
display_meta=param.help or "",
|
||||
)
|
||||
)
|
||||
|
||||
if param_called:
|
||||
choices = self._get_completion_from_params(
|
||||
autocomplete_ctx, args, param, incomplete
|
||||
)
|
||||
break
|
||||
|
||||
elif isinstance(param, click.Argument):
|
||||
# Show only unused arguments. A variadic argument (nargs=-1)
|
||||
# always accepts more values, so it is never hidden.
|
||||
already_present = False
|
||||
get_source = getattr(autocomplete_ctx, "get_parameter_source", None)
|
||||
source = None
|
||||
if get_source is not None:
|
||||
try:
|
||||
source = get_source(param.name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if source is not None:
|
||||
already_present = getattr(source, "name", None) == "COMMANDLINE"
|
||||
else:
|
||||
already_present = autocomplete_ctx.params.get(param.name) not in (
|
||||
None,
|
||||
(),
|
||||
[],
|
||||
)
|
||||
|
||||
if self.show_only_unused and already_present and param.nargs != -1:
|
||||
continue
|
||||
|
||||
choices.extend(
|
||||
self._get_completion_from_params(
|
||||
autocomplete_ctx, args, param, incomplete
|
||||
)
|
||||
)
|
||||
|
||||
return choices
|
||||
|
||||
def get_completions(
|
||||
self, document: Document, complete_event: CompleteEvent | None = None
|
||||
) -> Generator[Completion, None, None]:
|
||||
# Code analogous to click._bashcomplete.do_complete
|
||||
|
||||
args = split_arg_string(document.text_before_cursor, posix=False)
|
||||
|
||||
choices: list[Completion] = []
|
||||
cursor_within_command = (
|
||||
document.text_before_cursor.rstrip() == document.text_before_cursor
|
||||
)
|
||||
|
||||
if document.text_before_cursor.startswith(("!", ":")):
|
||||
return
|
||||
|
||||
if args and cursor_within_command:
|
||||
# We've entered some text and no space, give completions for the
|
||||
# current word.
|
||||
incomplete = args.pop()
|
||||
else:
|
||||
# We've not entered anything, either at all or for the current
|
||||
# command, so give all relevant completions for this context.
|
||||
incomplete = ""
|
||||
|
||||
if self.parsed_args != args:
|
||||
self.parsed_args = args
|
||||
try:
|
||||
self.parsed_ctx = _resolve_context(args, self.ctx)
|
||||
except Exception:
|
||||
return # autocompletion for nonexistent cmd can throw here
|
||||
self.ctx_command = self.parsed_ctx.command
|
||||
|
||||
if getattr(self.ctx_command, "hidden", False):
|
||||
return
|
||||
|
||||
try:
|
||||
choices.extend(
|
||||
self._get_completion_for_cmd_args(
|
||||
self.ctx_command, incomplete, self.parsed_ctx, args
|
||||
)
|
||||
)
|
||||
|
||||
if isinstance(self.ctx_command, click.MultiCommand):
|
||||
incomplete_lower = incomplete.lower()
|
||||
|
||||
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(
|
||||
name,
|
||||
-len(incomplete),
|
||||
display_meta=getattr(command, "short_help", ""),
|
||||
)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
click.echo("{}: {}".format(type(e).__name__, str(e)))
|
||||
|
||||
for item in choices:
|
||||
yield item
|
||||
Reference in New Issue
Block a user