Skip to content
Snippets Groups Projects
Commit 60b80d6c authored by Alexandre Detiste's avatar Alexandre Detiste
Browse files

New upstream version 0.3.0

parent 9d393628
No related branches found
No related tags found
No related merge requests found
Copyright (c) 2014-2015 Markus Unterwaditzer & contributors
Copyright (c) 2014-2015 Markus Unterwaditzer & contributors.
Copyright (c) 2016-2026 Asif Saif Uddin & contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
......
global-exclude *.py[cdo] __pycache__ *.so *.pyd
include LICENSE
Metadata-Version: 1.0
Metadata-Version: 2.1
Name: click-repl
Version: 0.2.0
Version: 0.3.0
Summary: REPL plugin for Click
Home-page: https://github.com/untitaker/click-repl
Author: Markus Unterwaditzer
Author-email: markus@unterwaditzer.net
License: MIT
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Provides-Extra: testing
License-File: LICENSE
click-repl
===
[![Tests](https://github.com/click-contrib/click-repl/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/click-contrib/click-repl/actions/workflows/tests.yml)
[![License](https://img.shields.io/pypi/l/click-repl?label=License)](https://github.com/click-contrib/click-repl/LICENSE)
![Python - version](https://img.shields.io/badge/python-3%20%7C%203.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)
[![PyPi - version](https://img.shields.io/badge/pypi-v0.2.0-blue)](https://pypi.org/project/click-repl/)
![wheels](https://img.shields.io/piwheels/v/click-repl?label=wheel)
![PyPI - Status](https://img.shields.io/pypi/status/click)
![PyPI - Downloads](https://img.shields.io/pypi/dm/click-repl)
Installation
===
Installation is done via pip:
```
pip install click-repl
```
Usage
===
In your [click](http://click.pocoo.org/) app:
```py
import click
from click_repl import register_repl
@click.group()
def cli():
pass
@cli.command()
def hello():
click.echo("Hello world!")
register_repl(cli)
cli()
```
In the shell:
```
$ my_app repl
> hello
Hello world!
> ^C
$ echo hello | my_app repl
Hello world!
```
**Features not shown:**
- Tab-completion.
- The parent context is reused, which means `ctx.obj` persists between
subcommands. If you're keeping caches on that object (like I do), using the
app's repl instead of the shell is a huge performance win.
- `!` - prefix executes shell commands.
You can use the internal `:help` command to explain usage.
Advanced Usage
===
For more flexibility over how your REPL works you can use the `repl` function
directly instead of `register_repl`. For example, in your app:
```py
import click
from click_repl import repl
from prompt_toolkit.history import FileHistory
@click.group()
def cli():
pass
@cli.command()
def myrepl():
prompt_kwargs = {
'history': FileHistory('/etc/myrepl/myrepl-history'),
}
repl(click.get_current_context(), prompt_kwargs=prompt_kwargs)
cli()
```
And then your custom `myrepl` command will be available on your CLI, which
will start a REPL which has its history stored in
`/etc/myrepl/myrepl-history` and persist between sessions.
Any arguments that can be passed to the [`python-prompt-toolkit`](https://github.com/prompt-toolkit/python-prompt-toolkit) [Prompt](http://python-prompt-toolkit.readthedocs.io/en/stable/pages/reference.html?prompt_toolkit.shortcuts.Prompt#prompt_toolkit.shortcuts.Prompt) class
can be passed in the `prompt_kwargs` argument and will be used when
instantiating your `Prompt`.
click-repl
===
[![Tests](https://github.com/click-contrib/click-repl/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/click-contrib/click-repl/actions/workflows/tests.yml)
[![License](https://img.shields.io/pypi/l/click-repl?label=License)](https://github.com/click-contrib/click-repl/LICENSE)
![Python - version](https://img.shields.io/badge/python-3%20%7C%203.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)
[![PyPi - version](https://img.shields.io/badge/pypi-v0.2.0-blue)](https://pypi.org/project/click-repl/)
![wheels](https://img.shields.io/piwheels/v/click-repl?label=wheel)
![PyPI - Status](https://img.shields.io/pypi/status/click)
![PyPI - Downloads](https://img.shields.io/pypi/dm/click-repl)
Installation
===
Installation is done via pip:
```
pip install click-repl
```
Usage
===
In your [click](http://click.pocoo.org/) app:
```py
import click
from click_repl import register_repl
@click.group()
def cli():
pass
@cli.command()
def hello():
click.echo("Hello world!")
register_repl(cli)
cli()
```
In the shell:
```
$ my_app repl
> hello
Hello world!
> ^C
$ echo hello | my_app repl
Hello world!
```
**Features not shown:**
- Tab-completion.
- The parent context is reused, which means `ctx.obj` persists between
subcommands. If you're keeping caches on that object (like I do), using the
app's repl instead of the shell is a huge performance win.
- `!` - prefix executes shell commands.
You can use the internal `:help` command to explain usage.
Advanced Usage
===
For more flexibility over how your REPL works you can use the `repl` function
directly instead of `register_repl`. For example, in your app:
```py
import click
from click_repl import repl
from prompt_toolkit.history import FileHistory
@click.group()
def cli():
pass
@cli.command()
def myrepl():
prompt_kwargs = {
'history': FileHistory('/etc/myrepl/myrepl-history'),
}
repl(click.get_current_context(), prompt_kwargs=prompt_kwargs)
cli()
```
And then your custom `myrepl` command will be available on your CLI, which
will start a REPL which has its history stored in
`/etc/myrepl/myrepl-history` and persist between sessions.
Any arguments that can be passed to the [`python-prompt-toolkit`](https://github.com/prompt-toolkit/python-prompt-toolkit) [Prompt](http://python-prompt-toolkit.readthedocs.io/en/stable/pages/reference.html?prompt_toolkit.shortcuts.Prompt#prompt_toolkit.shortcuts.Prompt) class
can be passed in the `prompt_kwargs` argument and will be used when
instantiating your `Prompt`.
==========
click-repl
==========
.. image:: https://travis-ci.org/click-contrib/click-repl.svg?branch=master
:target: https://travis-ci.org/click-contrib/click-repl
In your click_ app:
.. code:: python
import click
from click_repl import register_repl
@click.group()
def cli():
pass
@cli.command()
def hello():
click.echo("Hello world!")
register_repl(cli)
cli()
In the shell::
$ my_app repl
> hello
Hello world!
> ^C
$ echo hello | my_app repl
Hello world!
Features not shown:
* Tab-completion.
* The parent context is reused, which means ``ctx.obj`` persists between
subcommands. If you're keeping caches on that object (like I do), using the
app's repl instead of the shell is a huge performance win.
* ``!``-prefix executes shell commands.
You can use the internal ``:help`` command to explain usage.
PyPI: `<https://pypi.python.org/pypi/click-repl>`_
.. _click: http://click.pocoo.org/
How to install
==============
Installation is done with pip:
$ pip install click-repl
Advanced Usage
==============
For more flexibility over how your REPL works you can use the ``repl`` function
directly instead of ``register_repl``. For example, in your app:
.. code:: python
import click
from click_repl import repl
from prompt_toolkit.history import FileHistory
@click.group()
def cli():
pass
@cli.command()
def myrepl():
prompt_kwargs = {
'history': FileHistory('/etc/myrepl/myrepl-history'),
}
repl(click.get_current_context(), prompt_kwargs=prompt_kwargs)
cli()
And then your custom ``myrepl`` command will be available on your CLI, which
will start a REPL which has its history stored in
``/etc/myrepl/myrepl-history`` and persist between sessions.
Any arguments that can be passed to the ``python-prompt-toolkit`` Prompt_ class
can be passed in the ``prompt_kwargs`` argument and will be used when
instantiating your ``Prompt``.
.. _Prompt: http://python-prompt-toolkit.readthedocs.io/en/stable/pages/reference.html?prompt_toolkit.shortcuts.Prompt#prompt_toolkit.shortcuts.Prompt
License
=======
Licensed under the MIT, see ``LICENSE``.
Metadata-Version: 1.0
Metadata-Version: 2.1
Name: click-repl
Version: 0.2.0
Version: 0.3.0
Summary: REPL plugin for Click
Home-page: https://github.com/untitaker/click-repl
Author: Markus Unterwaditzer
Author-email: markus@unterwaditzer.net
License: MIT
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Provides-Extra: testing
License-File: LICENSE
click-repl
===
[![Tests](https://github.com/click-contrib/click-repl/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/click-contrib/click-repl/actions/workflows/tests.yml)
[![License](https://img.shields.io/pypi/l/click-repl?label=License)](https://github.com/click-contrib/click-repl/LICENSE)
![Python - version](https://img.shields.io/badge/python-3%20%7C%203.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)
[![PyPi - version](https://img.shields.io/badge/pypi-v0.2.0-blue)](https://pypi.org/project/click-repl/)
![wheels](https://img.shields.io/piwheels/v/click-repl?label=wheel)
![PyPI - Status](https://img.shields.io/pypi/status/click)
![PyPI - Downloads](https://img.shields.io/pypi/dm/click-repl)
Installation
===
Installation is done via pip:
```
pip install click-repl
```
Usage
===
In your [click](http://click.pocoo.org/) app:
```py
import click
from click_repl import register_repl
@click.group()
def cli():
pass
@cli.command()
def hello():
click.echo("Hello world!")
register_repl(cli)
cli()
```
In the shell:
```
$ my_app repl
> hello
Hello world!
> ^C
$ echo hello | my_app repl
Hello world!
```
**Features not shown:**
- Tab-completion.
- The parent context is reused, which means `ctx.obj` persists between
subcommands. If you're keeping caches on that object (like I do), using the
app's repl instead of the shell is a huge performance win.
- `!` - prefix executes shell commands.
You can use the internal `:help` command to explain usage.
Advanced Usage
===
For more flexibility over how your REPL works you can use the `repl` function
directly instead of `register_repl`. For example, in your app:
```py
import click
from click_repl import repl
from prompt_toolkit.history import FileHistory
@click.group()
def cli():
pass
@cli.command()
def myrepl():
prompt_kwargs = {
'history': FileHistory('/etc/myrepl/myrepl-history'),
}
repl(click.get_current_context(), prompt_kwargs=prompt_kwargs)
cli()
```
And then your custom `myrepl` command will be available on your CLI, which
will start a REPL which has its history stored in
`/etc/myrepl/myrepl-history` and persist between sessions.
Any arguments that can be passed to the [`python-prompt-toolkit`](https://github.com/prompt-toolkit/python-prompt-toolkit) [Prompt](http://python-prompt-toolkit.readthedocs.io/en/stable/pages/reference.html?prompt_toolkit.shortcuts.Prompt#prompt_toolkit.shortcuts.Prompt) class
can be passed in the `prompt_kwargs` argument and will be used when
instantiating your `Prompt`.
LICENSE
MANIFEST.in
README.rst
README.md
pyproject.toml
setup.cfg
setup.py
click_repl/__init__.py
click_repl/_completer.py
click_repl/_repl.py
click_repl/exceptions.py
click_repl/utils.py
click_repl.egg-info/PKG-INFO
click_repl.egg-info/SOURCES.txt
click_repl.egg-info/dependency_links.txt
click_repl.egg-info/not-zip-safe
click_repl.egg-info/requires.txt
click_repl.egg-info/top_level.txt
\ No newline at end of file
click
prompt_toolkit
six
click>=7.0
prompt_toolkit>=3.0.36
[testing]
pytest-cov>=4.0.0
pytest>=7.2.1
tox>=4.4.3
from collections import defaultdict
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.history import InMemoryHistory
from prompt_toolkit.shortcuts import prompt
import click
import click.parser
import os
import shlex
import sys
import six
from .exceptions import InternalCommandException, ExitReplException # noqa
# Handle backwards compatibility between Click 7.0 and 8.0
try:
import click.shell_completion
HAS_C8 = True
except ImportError:
import click._bashcomplete
HAS_C8 = False
# Handle click.exceptions.Exit introduced in Click 7.0
try:
from click.exceptions import Exit as ClickExit
except ImportError:
class ClickExit(RuntimeError):
pass
PY2 = sys.version_info[0] == 2
if PY2:
text_type = unicode # noqa
else:
text_type = str # noqa
__version__ = "0.2.0"
_internal_commands = dict()
def _register_internal_command(names, target, description=None):
if not hasattr(target, "__call__"):
raise ValueError("Internal command must be a callable")
if isinstance(names, six.string_types):
names = [names]
elif not isinstance(names, (list, tuple)):
raise ValueError('"names" must be a string or a list / tuple')
for name in names:
_internal_commands[name] = (target, description)
def _get_registered_target(name, default=None):
target_info = _internal_commands.get(name)
if target_info:
return target_info[0]
return default
def _exit_internal():
raise ExitReplException()
def _help_internal():
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 six.iteritems(_internal_commands):
info_table[target_info[1]].append(mnemonic)
formatter.write_dl(
(
", ".join((":{0}".format(mnemonic) for mnemonic in sorted(mnemonics))),
description,
)
for description, mnemonics in six.iteritems(info_table)
)
return formatter.getvalue()
_register_internal_command(["q", "quit", "exit"], _exit_internal, "exits the repl")
_register_internal_command(
["?", "h", "help"], _help_internal, "displays general help information"
from ._completer import ClickCompleter as ClickCompleter # 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
class ClickCompleter(Completer):
def __init__(self, cli):
self.cli = cli
def get_completions(self, document, complete_event=None):
# Code analogous to click._bashcomplete.do_complete
try:
args = shlex.split(document.text_before_cursor)
except ValueError:
# Invalid command, perhaps caused by missing closing quotation.
return
cursor_within_command = (
document.text_before_cursor.rstrip() == document.text_before_cursor
)
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 = ""
# Resolve context based on click version
if HAS_C8:
ctx = click.shell_completion._resolve_context(self.cli, {}, "", args)
else:
ctx = click._bashcomplete.resolve_ctx(self.cli, "", args)
if ctx is None:
return
choices = []
for param in ctx.command.params:
if isinstance(param, click.Option):
for options in (param.opts, param.secondary_opts):
for o in options:
choices.append(
Completion(
text_type(o), -len(incomplete), display_meta=param.help
)
)
elif isinstance(param, click.Argument):
if isinstance(param.type, click.Choice):
for choice in param.type.choices:
choices.append(Completion(text_type(choice), -len(incomplete)))
if isinstance(ctx.command, click.MultiCommand):
for name in ctx.command.list_commands(ctx):
command = ctx.command.get_command(ctx, name)
choices.append(
Completion(
text_type(name),
-len(incomplete),
display_meta=getattr(command, "short_help"),
)
)
for item in choices:
if item.text.startswith(incomplete):
yield item
def bootstrap_prompt(prompt_kwargs, group):
"""
Bootstrap prompt_toolkit kwargs or use user defined values.
:param prompt_kwargs: The user specified prompt kwargs.
"""
prompt_kwargs = prompt_kwargs or {}
defaults = {
"history": InMemoryHistory(),
"completer": ClickCompleter(group),
"message": u"> ",
}
for key in defaults:
default_value = defaults[key]
if key not in prompt_kwargs:
prompt_kwargs[key] = default_value
return prompt_kwargs
def repl( # noqa: C901
old_ctx,
prompt_kwargs=None,
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.shortcuts.prompt`.
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
group = group_ctx.command
isatty = sys.stdin.isatty()
# 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
if isinstance(group_ctx.command, click.CommandCollection):
available_commands = {
cmd_name: cmd_obj
for source in group_ctx.command.sources
for cmd_name, cmd_obj in source.commands.items()
}
else:
available_commands = group_ctx.command.commands
available_commands.pop(repl_command_name, None)
prompt_kwargs = bootstrap_prompt(prompt_kwargs, group)
if isatty:
def get_command():
return prompt(**prompt_kwargs)
else:
get_command = sys.stdin.readline
while True:
try:
command = get_command()
except KeyboardInterrupt:
continue
except EOFError:
break
if not command:
if isatty:
continue
else:
break
if allow_system_commands and dispatch_repl_commands(command):
continue
if allow_internal_commands:
try:
result = handle_internal_commands(command)
if isinstance(result, six.string_types):
click.echo(result)
continue
except ExitReplException:
break
try:
args = shlex.split(command)
except ValueError as e:
click.echo("{}: {}".format(type(e).__name__, e))
continue
try:
with group.make_context(None, args, parent=group_ctx) as ctx:
group.invoke(ctx)
ctx.exit()
except click.ClickException as e:
e.show()
except ClickExit:
pass
except SystemExit:
pass
except ExitReplException:
break
def register_repl(group, name="repl"):
"""Register :func:`repl()` as sub-command *name* of *group*."""
group.command(name=name)(click.pass_context(repl))
def exit():
"""Exit the repl"""
_exit_internal()
def dispatch_repl_commands(command):
"""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):
"""Run repl-internal commands.
Repl-internal commands are all commands starting with ":".
"""
if command.startswith(":"):
target = _get_registered_target(command[1:], default=None)
if target:
return target()
__version__ = "0.3.0"
from __future__ import unicode_literals
import os
from glob import iglob
import click
from prompt_toolkit.completion import Completion, Completer
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"
def text_type(text):
return "{}".format(text)
class ClickCompleter(Completer):
__slots__ = ("cli", "ctx", "parsed_args", "parsed_ctx", "ctx_command")
def __init__(self, cli, ctx):
self.cli = cli
self.ctx = ctx
self.parsed_args = []
self.parsed_ctx = ctx
self.ctx_command = ctx.command
def _get_completion_from_autocompletion_functions(
self,
param,
autocomplete_ctx,
args,
incomplete,
):
param_choices = []
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(
text_type(autocomplete[0]),
-len(incomplete),
display_meta=autocomplete[1],
)
)
elif HAS_CLICK_V8 and isinstance(
autocomplete, click.shell_completion.CompletionItem
):
param_choices.append(
Completion(text_type(autocomplete.value), -len(incomplete))
)
else:
param_choices.append(
Completion(text_type(autocomplete), -len(incomplete))
)
return param_choices
def _get_completion_from_choices_click_le_7(self, param, incomplete):
if not getattr(param.type, "case_sensitive", True):
incomplete = incomplete.lower()
return [
Completion(
text_type(choice),
-len(incomplete),
display=text_type(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(
text_type(choice),
-len(incomplete),
display=text_type(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, args, incomplete):
if "*" in incomplete:
return []
choices = []
_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(
text_type(path),
-len(incomplete),
display=text_type(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))
)
for k, v in {
"true": ("1", "true", "t", "yes", "y", "on"),
"false": ("0", "false", "f", "no", "n", "off"),
}.items()
if any(i.startswith(incomplete) for i in v)
]
def _get_completion_from_params(self, autocomplete_ctx, args, param, incomplete):
choices = []
param_type = param.type
# shell_complete method for click.Choice is intorduced 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,
incomplete,
autocomplete_ctx,
args,
):
choices = []
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):
for option in param.opts + param.secondary_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 args[param.nargs * -1 :]: # noqa: E203
param_called = True
break
elif option.startswith(incomplete):
choices.append(
Completion(
text_type(option),
-len(incomplete),
display_meta=text_type(param.help or ""),
)
)
if param_called:
choices = self._get_completion_from_params(
autocomplete_ctx, args, param, incomplete
)
elif isinstance(param, click.Argument):
choices.extend(
self._get_completion_from_params(
autocomplete_ctx, args, param, incomplete
)
)
return choices
def get_completions(self, document, complete_event=None):
# Code analogous to click._bashcomplete.do_complete
args = split_arg_string(document.text_before_cursor, posix=False)
choices = []
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
self.parsed_ctx = _resolve_context(args, self.ctx)
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(
text_type(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
for item in choices:
yield item
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, InvalidGroupFormat
from .utils import _execute_internal_and_sys_cmds
__all__ = ["bootstrap_prompt", "register_repl", "repl"]
def bootstrap_prompt(
group,
prompt_kwargs,
ctx=None,
):
"""
Bootstrap prompt_toolkit kwargs or use user defined values.
:param group: click Group
: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, 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`.
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.Group):
group_ctx = old_ctx.parent
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
# 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 = {
cmd_name: cmd_obj
for source in group_ctx.command.sources
for cmd_name, cmd_obj in source.commands.items()
}
else:
available_commands = group_ctx.command.commands
original_command = available_commands.pop(repl_command_name, None)
if isatty:
prompt_kwargs = bootstrap_prompt(group, prompt_kwargs, group_ctx)
session = PromptSession(**prompt_kwargs)
def get_command():
return session.prompt()
else:
get_command = sys.stdin.readline
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 = 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):
pass
except ExitReplException:
break
if original_command is not None:
available_commands[repl_command_name] = original_command
def register_repl(group, name="repl"):
"""Register :func:`repl()` as sub-command *name* of *group*."""
group.command(name=name)(click.pass_context(repl))
......@@ -4,3 +4,20 @@ class InternalCommandException(Exception):
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
import click
import os
import shlex
import sys
from collections import defaultdict
from .exceptions import CommandLineParserError, ExitReplException
__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",
]
# Abstract datatypes in collections module are moved to collections.abc
# module in Python 3.3
if sys.version_info >= (3, 3):
from collections.abc import Iterable, Mapping # noqa: F811
else:
from collections import Iterable, Mapping
def _resolve_context(args, ctx=None):
"""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 = ctx.protected_args + 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 = [*sub_ctx.protected_args, *sub_ctx.args]
else:
break
return ctx
_internal_commands = {}
def split_arg_string(string, posix=True):
"""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, target, description=None):
if not hasattr(target, "__call__"):
raise ValueError("Internal command must be a callable")
if isinstance(names, str):
names = [names]
elif isinstance(names, Mapping) or not isinstance(names, Iterable):
raise ValueError(
'"names" must be a string, or an iterable object, but got "{}"'.format(
type(names).__name__
)
)
for name in names:
_internal_commands[name] = (target, description)
def _get_registered_target(name, default=None):
target_info = _internal_commands.get(name)
if target_info:
return target_info[0]
return default
def _exit_internal():
raise ExitReplException()
def _help_internal():
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()
)
val = formatter.getvalue() # type: str
return val
_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,
allow_internal_commands=True,
allow_system_commands=True,
):
"""
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:
result = handle_internal_commands(command)
if isinstance(result, str):
click.echo(result)
return None
try:
return split_arg_string(command)
except ValueError as e:
raise CommandLineParserError("{}".format(e))
def exit():
"""Exit the repl"""
_exit_internal()
def dispatch_repl_commands(command):
"""
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):
"""
Run repl-internal commands.
Repl-internal commands are all commands starting with ":".
"""
if command.startswith(":"):
target = _get_registered_target(command[1:], default=None)
if target:
return target()
[build-system]
requires = ["setuptools", "wheel"]
build-backend = "setuptools.build_meta"
[tool.pytest.ini_options]
addopts = [
"--cov=click_repl"
]
testpaths = [
"tests",
]
[metadata]
name = click-repl
version = attr: click_repl.__version__
description = REPL plugin for Click
description-file = README.md
long_description_content_type = text/markdown
long_description = file: README.md
url = https://github.com/untitaker/click-repl
author = Markus Unterwaditzer
author_email = markus@unterwaditzer.net
license = MIT
classifiers =
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
[options]
packages =
click_repl
install_requires =
click>=7.0
prompt_toolkit>=3.0.36
python_requires = >=3.6
zip_safe = no
[options.extras_require]
testing =
pytest>=7.2.1
pytest-cov>=4.0.0
tox>=4.4.3
[flake8]
ignore = E203, E266, W503, E402, E731, C901
max-line-length = 90
max-complexity = 18
select = B,C,E,F,W,T4,B9
[egg_info]
tag_build =
tag_date = 0
......
#!/usr/bin/env python
#!/usr/bin/env python3
import ast
import re
from setuptools import setup
_version_re = re.compile(r"__version__\s+=\s+(.*)")
with open("click_repl/__init__.py", "rb") as f:
version = str(
ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1))
)
setup(
name="click-repl",
version=version,
description="REPL plugin for Click",
author="Markus Unterwaditzer",
author_email="markus@unterwaditzer.net",
url="https://github.com/untitaker/click-repl",
license="MIT",
packages=["click_repl"],
install_requires=["click", "prompt_toolkit", "six"],
)
if __name__ == '__main__':
setup()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment