This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
@@ -0,0 +1,12 @@
from ._completer import ClickCompleter as ClickCompleter # noqa: F401
from .core import pass_context as pass_context # noqa: F401
from ._repl import register_repl as register_repl # noqa: F401
from ._repl import repl as repl # noqa: F401
from .exceptions import CommandLineParserError as CommandLineParserError # noqa: F401
from .exceptions import ExitReplException as ExitReplException # noqa: F401
from .exceptions import ( # noqa: F401
InternalCommandException as InternalCommandException,
)
from .utils import exit as exit # noqa: F401
__version__ = "0.4.1"
@@ -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
@@ -0,0 +1,28 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .core import ReplContext
# To store the ReplContext objects generated throughout the Runtime.
_context_stack: list[ReplContext] = []
def _push_context(ctx: ReplContext) -> None:
"""
Pushes a new REPL context onto the current stack.
Parameters
----------
ctx
The :class:`~click_repl.core.ReplContext` object that should be
added to the REPL context stack.
"""
_context_stack.append(ctx)
def _pop_context() -> None:
"""Removes the top-level REPL context from the stack."""
_context_stack.pop()
@@ -0,0 +1,174 @@
from __future__ import annotations
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 .globals_ import ISATTY, get_current_repl_ctx
from .utils import (
_execute_internal_and_sys_cmds,
_get_protected_args,
_set_protected_args,
)
__all__ = ["bootstrap_prompt", "register_repl", "repl"]
def bootstrap_prompt(
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.MultiCommand object
:param prompt_kwargs: The user specified prompt kwargs.
"""
defaults = {
"history": InMemoryHistory(),
"completer": ClickCompleter(group, ctx=ctx),
"message": "> ",
}
defaults.update(prompt_kwargs)
return defaults
def repl(
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.
:param old_ctx: The current Click context.
:param prompt_kwargs: Parameters passed to
:py:func:`prompt_toolkit.PromptSession`.
If stdin is not a TTY, no prompt will be printed, but only commands read
from stdin.
"""
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.MultiCommand
):
group_ctx = old_ctx.parent
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
# 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 # type: ignore[index]
and not param.required
):
raise InvalidGroupFormat(
f"{type(group).__name__} '{group.name}' requires value for "
f"an optional argument '{param.name}' in REPL mode"
)
# Delete the REPL command from those available, as we don't want to allow
# 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
available_commands: MutableMapping[str, click.Command] = {}
if isinstance(group, click.CommandCollection):
available_commands = {
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)
}
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,
bootstrap_prompt(group, prompt_kwargs, group_ctx),
get_current_repl_ctx(silent=True),
)
if ISATTY:
# If stdin is a TTY, prompt the user for input using PromptSession.
def get_command() -> str:
return repl_ctx.session.prompt() # type: ignore
else:
# If stdin is not a TTY, read input from stdin directly.
def get_command() -> str:
inp = sys.stdin.readline().strip()
repl_ctx._history.append(inp)
return inp
with repl_ctx:
while True:
try:
command = get_command()
except KeyboardInterrupt:
continue
except EOFError:
break
if not command:
if ISATTY:
continue
else:
break
try:
args = _execute_internal_and_sys_cmds(
command, allow_internal_commands, allow_system_commands
)
if args is None:
continue
except CommandLineParserError:
continue
except ExitReplException:
break
try:
# The group command will dispatch based on args.
old_protected_args = _get_protected_args(group_ctx)
try:
_set_protected_args(group_ctx, args)
group.invoke(group_ctx)
finally:
_set_protected_args(group_ctx, old_protected_args)
except click.ClickException as e:
e.show()
except (ClickExit, SystemExit):
pass
except ExitReplException:
break
if original_command is not None:
available_commands[repl_command_name] = original_command # type: ignore[index]
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))
@@ -0,0 +1,214 @@
"""
Core functionalities for managing context of the click_repl app.
"""
from __future__ import annotations
from functools import wraps
from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, TypeVar
from click import Context
from prompt_toolkit import PromptSession
from typing_extensions import Concatenate, Final, ParamSpec, TypeAlias, TypedDict
from ._ctx_stack import _pop_context, _push_context
from .globals_ import ISATTY, get_current_repl_ctx
if TYPE_CHECKING:
from prompt_toolkit.formatted_text import AnyFormattedText
P = ParamSpec("P")
R = TypeVar("R")
F = TypeVar("F", bound=Callable[..., Any])
__all__ = ["ReplContext", "pass_context"]
_PromptSession: TypeAlias = PromptSession[Dict[str, Any]]
class ReplContextInfoDict(TypedDict):
group_ctx: Context
prompt_kwargs: dict[str, Any]
session: _PromptSession | None
parent: ReplContext | None
_history: list[str]
class ReplContext:
"""
Context object for the REPL sessions.
This class tracks the depth of nested REPLs, ensuring seamless navigation
between different levels. It facilitates nested REPL scenarios, allowing
multiple levels of interactive REPL sessions.
Each REPL's properties are stored inside this context class, allowing them to
be accessed and shared with their parent REPL.
All the settings for each REPL session persist until the session is terminated.
Parameters
----------
group_ctx
The click context object that belong to the CLI/parent Group.
prompt_kwargs
Extra keyword arguments for
:class:`~prompt_toolkit.shortcuts.PromptSession` class.
parent
REPL Context object of the parent REPL session, if exists. Otherwise, :obj:`None`.
"""
__slots__ = (
"group_ctx",
"prompt_kwargs",
"parent",
"session",
"_history",
)
def __init__(
self,
group_ctx: Context,
prompt_kwargs: dict[str, Any] = {},
parent: ReplContext | None = None,
) -> None:
"""
Initializes the `ReplContext` class.
"""
session: _PromptSession | None
if ISATTY:
session = PromptSession(**prompt_kwargs)
else:
session = None
self.group_ctx: Final[Context] = group_ctx
"""The click context object that belong to the CLI/parent Group."""
self.session = session
"""Object that's responsible for managing and executing the REPL."""
self._history: list[str] = []
"""
History of past executed commands.
Used only when :func:`~sys.stdin.isatty` is :obj:`False`.
"""
self.prompt_kwargs = prompt_kwargs
"""
Extra keyword arguments for
:class:`~prompt_toolkit.shortcuts.PromptSession` class.
"""
self.parent: Final[ReplContext | None] = parent
"""
REPL Context object of the parent REPL session, if exists.
Otherwise, :obj:`None`.
"""
def __enter__(self) -> ReplContext:
_push_context(self)
return self
def __exit__(self, *_: Any) -> None:
_pop_context()
@property
def prompt(self) -> AnyFormattedText:
"""
The prompt text of the REPL.
Returns
-------
prompt_toolkit.formatted_text.AnyFormattedText
The prompt object if :func:`~sys.stdin.isatty` is :obj:`True`,
else :obj:`None`.
"""
if ISATTY and self.session is not None:
return self.session.message
return None
@prompt.setter
def prompt(self, value: AnyFormattedText) -> None:
if ISATTY and self.session is not None:
self.session.message = value
def to_info_dict(self) -> ReplContextInfoDict:
"""
Provides a dictionary with minimal info about the current REPL.
Returns
-------
ReplContextInfoDict
A dictionary that has the instance variables and their values.
"""
res: ReplContextInfoDict = {
"group_ctx": self.group_ctx,
"prompt_kwargs": self.prompt_kwargs,
"session": self.session,
"parent": self.parent,
"_history": self._history,
}
return res
def session_reset(self) -> None:
"""
Resets values of :class:`~prompt_toolkit.session.PromptSession` to
the provided :attr:`~.prompt_kwargs`, discarding any changes done to the
:class:`~prompt_toolkit.session.PromptSession` object.
"""
if ISATTY and self.session is not None:
self.session = PromptSession(**self.prompt_kwargs)
def history(self) -> Generator[str, None, None]:
"""
Generates the history of past executed commands.
Yields
------
str
The executed command string from the history,
in chronological order from most recent to oldest.
"""
if ISATTY and self.session is not None:
yield from self.session.history.load_history_strings()
else:
yield from reversed(self._history)
def pass_context(
func: Callable[Concatenate[ReplContext | None, P], R],
) -> Callable[P, R]:
"""
Decorator that marks a callback function to receive the current
REPL context object as its first argument.
Parameters
----------
func
The callback function to pass context as its first parameter.
Returns
-------
Callable[P,R]
The decorated callback function that receives the current REPL
context object as its first argument.
"""
@wraps(func)
def decorator(*args: P.args, **kwargs: P.kwargs) -> R:
return func(get_current_repl_ctx(), *args, **kwargs)
return decorator
@@ -0,0 +1,23 @@
class InternalCommandException(Exception):
pass
class ExitReplException(InternalCommandException):
pass
class CommandLineParserError(Exception):
pass
class InvalidGroupFormat(Exception):
pass
# Handle click.exceptions.Exit introduced in Click 7.0
try:
from click.exceptions import Exit as ClickExit
except (ImportError, ModuleNotFoundError):
class ClickExit(RuntimeError): # type: ignore[no-redef]
pass
@@ -0,0 +1,56 @@
from __future__ import annotations
import sys
from typing import TYPE_CHECKING, NoReturn, overload
from ._ctx_stack import _context_stack
if TYPE_CHECKING:
from .core import ReplContext
ISATTY = sys.stdin is not None and sys.stdin.isatty()
@overload
def get_current_repl_ctx() -> ReplContext | NoReturn:
...
@overload
def get_current_repl_ctx(silent: bool = False) -> ReplContext | NoReturn | None:
...
def get_current_repl_ctx(silent: bool = False) -> ReplContext | NoReturn | None:
"""
Retrieves the current click-repl context.
This function provides a way to access the context from anywhere
in the code. This function serves as a more implicit alternative to the
:func:`~click.core.pass_context` decorator.
Parameters
----------
silent
If set to :obj:`True`, the function returns :obj:`None` if no context
is available. The default behavior is to raise a :exc:`~RuntimeError`.
Returns
-------
:class:`~click_repl.core.ReplContext` | None
REPL context object if available, or :obj:`None` if ``silent`` is :obj:`True`.
Raises
------
RuntimeError
If there's no context object in the stack and ``silent`` is :obj:`False`.
"""
try:
return _context_stack[-1]
except IndexError:
if not silent:
raise RuntimeError("There is no active click-repl context.")
return None
@@ -0,0 +1,238 @@
from __future__ import annotations
import os
import shlex
import typing as t
from collections import defaultdict
from typing import Callable, Generator, Iterator, NoReturn, Sequence
import click
from typing_extensions import TypeAlias
from .exceptions import CommandLineParserError, ExitReplException
T = t.TypeVar("T")
InternalCommandCallback: TypeAlias = Callable[[], None]
__all__ = [
"_execute_internal_and_sys_cmds",
"_exit_internal",
"_get_registered_target",
"_help_internal",
"_resolve_context",
"_register_internal_command",
"dispatch_repl_commands",
"handle_internal_commands",
"split_arg_string",
"exit",
]
def _get_protected_args(ctx: click.Context) -> list[str]:
# In click >= 8.2, protected_args is a read-only property over _protected_args.
if hasattr(ctx, "_protected_args"):
return t.cast(list[str], getattr(ctx, "_protected_args"))
return ctx.protected_args
def _set_protected_args(ctx: click.Context, args: list[str]) -> None:
if hasattr(ctx, "_protected_args"):
setattr(ctx, "_protected_args", args)
else:
ctx.protected_args = args
def _resolve_context(args: list[str], ctx: click.Context) -> click.Context:
"""Produce the context hierarchy starting with the command and
traversing the complete arguments. This only follows the commands,
it doesn't trigger input prompts or callbacks.
:param args: List of complete args before the incomplete value.
:param cli_ctx: `click.Context` object of the CLI group
"""
while args:
command = ctx.command
if isinstance(command, click.MultiCommand):
if not command.chain:
name, cmd, args = command.resolve_command(ctx, args)
if cmd is None:
return ctx
ctx = cmd.make_context(name, args, parent=ctx, resilient_parsing=True)
args = _get_protected_args(ctx) + ctx.args
else:
while args:
name, cmd, args = command.resolve_command(ctx, args)
if cmd is None:
return ctx
sub_ctx = cmd.make_context(
name,
args,
parent=ctx,
allow_extra_args=True,
allow_interspersed_args=False,
resilient_parsing=True,
)
args = sub_ctx.args
ctx = sub_ctx
args = [*_get_protected_args(sub_ctx), *sub_ctx.args]
else:
break
return ctx
_internal_commands: dict[str, tuple[InternalCommandCallback, str | None]] = {}
def split_arg_string(string: str, posix: bool = True) -> list[str]:
"""Split an argument string as with :func:`shlex.split`, but don't
fail if the string is incomplete. Ignores a missing closing quote or
incomplete escape sequence and uses the partial token as-is.
.. code-block:: python
split_arg_string("example 'my file")
["example", "my file"]
split_arg_string("example my\\")
["example", "my"]
:param string: String to split.
"""
lex = shlex.shlex(string, posix=posix)
lex.whitespace_split = True
lex.commenters = ""
out = []
try:
for token in lex:
out.append(token)
except ValueError:
# Raised when end-of-string is reached in an invalid state. Use
# the partial token as-is. The quote or escape character is in
# lex.state, not lex.token.
out.append(lex.token)
return out
def _register_internal_command(
names: str | Sequence[str] | Generator[str, None, None] | Iterator[str],
target: InternalCommandCallback,
description: str | None = None,
) -> None:
if not hasattr(target, "__call__"):
raise ValueError("Internal command must be a callable")
if isinstance(names, str):
names = [names]
elif not isinstance(names, (Sequence, Generator, Iterator)):
raise ValueError(
'"names" must be a string, or a Sequence of strings, but got "{}"'.format(
type(names).__name__
)
)
for name in names:
_internal_commands[name] = (target, description)
def _get_registered_target(
name: str, default: T | None = None
) -> InternalCommandCallback | T | None:
target_info = _internal_commands.get(name, None)
if target_info:
return target_info[0]
return default
def _exit_internal() -> NoReturn:
raise ExitReplException()
def _help_internal() -> None:
formatter = click.HelpFormatter()
formatter.write_heading("REPL help")
formatter.indent()
with formatter.section("External Commands"):
formatter.write_text('prefix external commands with "!"')
with formatter.section("Internal Commands"):
formatter.write_text('prefix internal commands with ":"')
info_table = defaultdict(list)
for mnemonic, target_info in _internal_commands.items():
info_table[target_info[1]].append(mnemonic)
formatter.write_dl( # type: ignore[arg-type]
( # type: ignore[arg-type]
", ".join(map(":{}".format, sorted(mnemonics))),
description,
)
for description, mnemonics in info_table.items()
)
print(formatter.getvalue())
_register_internal_command(["q", "quit", "exit"], _exit_internal, "exits the repl")
_register_internal_command(
["?", "h", "help"], _help_internal, "displays general help information"
)
def _execute_internal_and_sys_cmds(
command: str,
allow_internal_commands: bool = True,
allow_system_commands: bool = True,
) -> list[str] | None:
"""
Executes internal, system, and all the other registered click commands from the input
"""
if allow_system_commands and dispatch_repl_commands(command):
return None
if allow_internal_commands and command.startswith(":"):
handle_internal_commands(command)
return None
try:
return split_arg_string(command)
except ValueError as e:
raise CommandLineParserError("{}".format(e))
def exit() -> NoReturn:
"""Exit the repl"""
_exit_internal()
def dispatch_repl_commands(command: str) -> bool:
"""
Execute system commands entered in the repl.
System commands are all commands starting with "!".
"""
if command.startswith("!"):
os.system(command[1:])
return True
return False
def handle_internal_commands(command: str) -> None:
"""
Run repl-internal commands.
Repl-internal commands are all commands starting with ":".
"""
target = _get_registered_target(command[1:], default=None)
if target:
target()