Open mbande opened 3 years ago
This would be similar to what plac's interactive mode (-i
) does. While plac has some great and nifty features, in my experience the out-of-the-box ease of use for regular, batch CLI apps is nowhere near as good as Typer's. Still, having an interactive shell capability would be great, for example for scripts that fetch some data or e.g. ask for some input at startup. Also, it makes it so much easier to use Typer as a go-to solution for all sorts of CLI apps.
Click seems to have a package for this? click-shell (although did not investigate if/how it works)
Definitely would love to see this. Was looking to see if the feature was supported and stumbled upon this request, so I assume it isn't. Would be quite handy when I want to interact with the app a lot.
You can do this with click-repl package : https://github.com/click-contrib/click-repl
import click
import typer
from click_repl import repl
app = typer.Typer()
remote = typer.Typer()
app.add_typer(remote, name="remote")
@remote.command()
def add(origin: str):
typer.echo(f"Setting origin : {origin}")
@remote.command()
def remove(origin: str):
typer.echo(f"Removing origin : {origin}")
@app.command()
def myrepl(ctx: typer.Context):
repl(ctx)
if __name__ == "__main__":
app()
After some brief experimentation, I've concluded that I prefer click-shell
for my use case (whether it will be further developed and supported is a different question). click-shell
immediately launches a subshell without the user needing to provide a repl
command.
Here's how I believe we integrate it with typer
:
from typer import Context, Typer
from click_shell import make_click_shell
app: Typer = Typer()
@app.command()
def foobar():
print("foobar")
@app.callback(invoke_without_command=True)
def launch(ctx: Context):
shell = make_click_shell(ctx, prompt="<shell_prompt>", intro="<shell_introduction>")
shell.cmdloop()
if __name__ == "__main__":
app()
Nota bene: I'm new to all three projects (typer
, click
, click-shell
); I've only been using them within the last hour. Please forgive any mistakes!
Edit, five days later: here's a quick project I threw together using click-shell
and typer
. It won't work without a DALL·E 2 account, but you can still look at the code! https://github.com/afparsons/albaretto
This demo is good but one thing that took me a while was figuring out how to take advantage of typers nice help printing:
from typer import Context, Typer
from click_shell import make_click_shell
from rich import print
app: Typer = Typer()
@app.command()
def foobar():
print("foobar")
@app.callback(invoke_without_command=True)
def launch(ctx: Context):
shell = make_click_shell(ctx, prompt="<shell_prompt>", intro="<shell_introduction>")
shell.cmdloop()
@app.command(hidden=True)
def help(ctx: Context):
print("\n Type 'command --help' for help on a specific command.")
ctx.parent.get_help()
if __name__ == "__main__":
app()
maybe there is a better way to do it. The default click-shell help is pretty sad by comparison.
btw i also checked out click-repl, was hard to choose between them cause neither of them are very well documented and both have stregths:
Actually let me do you one better even. For all future generations, I leave this here:
from typer import Context, Typer, Argument
from typing import Optional
from typing_extensions import Annotated
@app.command(hidden=True)
def help(ctx: Context, command: Annotated[Optional[str], Argument()] = None):
print("\n Type 'command --help' or 'help <command>' for help on a specific command.")
if command:
command = ctx.parent.command.get_command(ctx, command)
command.get_help(ctx)
else:
ctx.parent.get_help()
enjoy
Okay let me do you one more, typer_shell.py
:
from typing_extensions import Annotated
from typing import Optional, Callable
from click_shell import make_click_shell
from typer import Context, Typer, Argument
from rich import print
def make_typer_shell(
app: Typer,
prompt: str = ">> ",
intro: str = "\n Welcome to typer-shell! Type help to see commands.\n",
default: Optional[Callable] = None
) -> None:
@app.command(hidden=True)
def help(ctx: Context, command: Annotated[Optional[str], Argument()] = None):
print("\n Type 'command --help' or 'help <command>' for help on a specific command.")
if not command:
ctx.parent.get_help()
return
ctx.parent.command.get_command(ctx, command).get_help(ctx)
@app.command(hidden=True)
def _default(args: Annotated[Optional[str], Argument()] = None):
"""Default command"""
if default:
default(args)
else:
print("Command not found. Type 'help' to see commands.")
@app.callback(invoke_without_command=True)
def launch(ctx: Context):
if ctx.invoked_subcommand is None:
shell = make_click_shell(ctx, prompt=prompt, intro=intro)
shell.default = _default
shell.cmdloop()
and test.py
:
#!/usr/bin/env python
from rich import print
from typer import Typer
from typer_shell import make_typer_shell
app: Typer = Typer()
make_typer_shell(app, prompt="🔥: ")
subapp: Typer = Typer()
default = lambda x: print(f"Inner Default, args: {x}")
make_typer_shell(
subapp,
prompt="🌲: ",
intro="\n Welcome to the inner shell! Type help to see commands.\n",
default=default
)
app.add_typer(subapp, name="inner")
@app.command()
def foobar():
"Foobar command"
print("foobar")
@subapp.command(name="foobar")
def _foobar():
"Foobar command"
print("foobar")
if __name__ == "__main__":
app()
and some footage: https://asciinema.org/a/C1fUrJ8yvBoBRgn0XftkcF4cm
Shall I make this a PR? Would you want this? @mgielda
i just made this, pretty simple really but very nice results:
A simple REPL with Typer can be rolled as follows:
import shlex
import typer
class SampleRepl:
def __init__(self):
self.x = 0
def drop_to_shell(self):
app = typer.Typer(no_args_is_help=True, add_completion=False)
app.command(help="set x")(self.set)
app.command(help="get x")(self.get)
typer.echo("Type --help to show help.")
typer.echo("Type 'exit' to quit.")
while True:
args = shlex.split(typer.prompt("> "))
if not args:
continue
if args == ["exit"]:
typer.echo("bye!")
break
app(args, standalone_mode=False)
def set(self, value: int):
self.x = value
def get(self) -> int:
typer.echo(f"x = {self.x}")
return self.x
def main():
SampleRepl().drop_to_shell()
if __name__ == "__main__":
main()
Is your feature request related to a problem
I want to keep context (e.g. session) between different CLI invocations.
The solution you would like
by activaticating REPL mode, e.g. with a
--repl
flag, user gets in interactive shell that he can communicate multiple commands and share the same context between them.