add
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user