Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added typehints and mypy test env in tox #120

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 71 additions & 60 deletions click_repl/_completer.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from __future__ import unicode_literals
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 Completion, Completer
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
from prompt_toolkit.document import Document

from .utils import _resolve_context, split_arg_string

Expand All @@ -26,30 +29,32 @@
AUTO_COMPLETION_PARAM = "autocompletion"


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


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

def __init__(self, cli, ctx, show_only_unused=False, shortest_only=False):
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 = []
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,
autocomplete_ctx,
args,
incomplete,
):
param_choices = []
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)
Expand All @@ -62,7 +67,7 @@ def _get_completion_from_autocompletion_functions(
if isinstance(autocomplete, tuple):
param_choices.append(
Completion(
text_type(autocomplete[0]),
str(autocomplete[0]),
-len(incomplete),
display_meta=autocomplete[1],
)
Expand All @@ -71,46 +76,48 @@ 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(str(autocomplete), -len(incomplete)))

return param_choices

def _get_completion_from_choices_click_le_7(self, param, incomplete):
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(
text_type(choice),
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 # type: ignore[attr-defined]
if choice.lower().startswith(incomplete)
]

else:
return [
Completion(
text_type(choice),
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 # type: ignore[attr-defined]
if choice.startswith(incomplete)
]

def _get_completion_for_Path_types(self, param, args, incomplete):
def _get_completion_for_Path_types(
self, param: click.Parameter, args: list[str], incomplete: str
) -> list[Completion]:
if "*" in incomplete:
return []

choices = []
choices: list[Completion] = []
_incomplete = os.path.expandvars(incomplete)
search_pattern = _incomplete.strip("'\"\t\n\r\v ").replace("\\\\", "\\") + "*"
quote = ""
Expand All @@ -134,29 +141,36 @@ 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):
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(
text_type(k), -len(incomplete), display_meta=text_type("/".join(v))
)
for k, v in {
"true": ("1", "true", "t", "yes", "y", "on"),
"false": ("0", "false", "f", "no", "n", "off"),
}.items()
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, args, param, incomplete):

choices = []
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

# shell_complete method for click.Choice is intorduced in click-v8
Expand Down Expand Up @@ -185,12 +199,12 @@ def _get_completion_from_params(self, autocomplete_ctx, args, param, incomplete)

def _get_completion_for_cmd_args(
self,
ctx_command,
incomplete,
autocomplete_ctx,
args,
):
choices = []
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:
Expand Down Expand Up @@ -229,9 +243,9 @@ def _get_completion_for_cmd_args(
elif option.startswith(incomplete) and not hide:
choices.append(
Completion(
text_type(option),
option,
-len(incomplete),
display_meta=text_type(param.help or ""),
display_meta=param.help or "",
)
)

Expand All @@ -250,12 +264,14 @@ def _get_completion_for_cmd_args(

return choices

def get_completions(self, document, complete_event=None):
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 = []
choices: list[Completion] = []
cursor_within_command = (
document.text_before_cursor.rstrip() == document.text_before_cursor
)
Expand All @@ -277,7 +293,7 @@ def get_completions(self, document, complete_event=None):
try:
self.parsed_ctx = _resolve_context(args, self.ctx)
except Exception:
return [] # autocompletion for nonexistent cmd can throw here
return # autocompletion for nonexistent cmd can throw here
self.ctx_command = self.parsed_ctx.command

if getattr(self.ctx_command, "hidden", False):
Expand All @@ -301,7 +317,7 @@ def get_completions(self, document, complete_event=None):
elif name.lower().startswith(incomplete_lower):
choices.append(
Completion(
text_type(name),
name,
-len(incomplete),
display_meta=getattr(command, "short_help", ""),
)
Expand All @@ -310,10 +326,5 @@ def get_completions(self, document, complete_event=None):
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

for item in choices:
yield item
58 changes: 34 additions & 24 deletions click_repl/_repl.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
from __future__ import with_statement
from __future__ import annotations

import click
import sys
from typing import Any, MutableMapping, cast

import click
from prompt_toolkit.history import InMemoryHistory

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

from .utils import _execute_internal_and_sys_cmds

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


def bootstrap_prompt(
group,
prompt_kwargs,
ctx=None,
):
group: click.MultiCommand,
prompt_kwargs: dict[str, Any],
ctx: click.Context,
) -> dict[str, Any]:
"""
Bootstrap prompt_toolkit kwargs or use user defined values.

:param group: click Group
:param group: click.MultiCommand object
:param prompt_kwargs: The user specified prompt kwargs.
"""

Expand All @@ -38,8 +39,11 @@ def bootstrap_prompt(


def repl(
old_ctx, prompt_kwargs={}, allow_system_commands=True, allow_internal_commands=True
):
old_ctx: click.Context,
prompt_kwargs: dict[str, Any] = {},
allow_system_commands: bool = True,
allow_internal_commands: bool = True,
) -> None:
"""
Start an interactive shell. All subcommands are available in it.

Expand All @@ -54,10 +58,12 @@ def repl(
group_ctx = 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):
if old_ctx.parent is not None and not isinstance(
old_ctx.command, click.MultiCommand
):
group_ctx = old_ctx.parent

group = group_ctx.command
group = cast(click.MultiCommand, 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
Expand All @@ -66,7 +72,7 @@ def repl(
for param in group.params:
if (
isinstance(param, click.Argument)
and group_ctx.params[param.name] is None
and group_ctx.params[param.name] is None # type: ignore[index]
and not param.required
):
raise InvalidGroupFormat(
Expand All @@ -78,16 +84,20 @@ def repl(
# nesting REPLs (note: pass `None` to `pop` as we don't want to error if
# REPL command already not present for some reason).
repl_command_name = old_ctx.command.name
if isinstance(group_ctx.command, click.CommandCollection):

available_commands: MutableMapping[str, click.Command] = {}

if isinstance(group, click.CommandCollection):
available_commands = {
cmd_name: cmd_obj
for source in group_ctx.command.sources
for cmd_name, cmd_obj in source.commands.items()
cmd_name: source.get_command(group_ctx, cmd_name) # type: ignore[misc]
for source in group.sources
for cmd_name in source.list_commands(group_ctx)
}
else:
available_commands = group_ctx.command.commands

original_command = available_commands.pop(repl_command_name, None)
elif isinstance(group, click.Group):
available_commands = group.commands

original_command = available_commands.pop(repl_command_name, None) # type: ignore

repl_ctx = ReplContext(
group_ctx,
Expand Down Expand Up @@ -152,9 +162,9 @@ def get_command() -> str:
break

if original_command is not None:
available_commands[repl_command_name] = original_command
available_commands[repl_command_name] = original_command # type: ignore[index]


def register_repl(group, name="repl"):
def register_repl(group: click.Group, name="repl") -> None:
"""Register :func:`repl()` as sub-command *name* of *group*."""
group.command(name=name)(click.pass_context(repl))
Loading
Loading