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

[FEATURE] REPL mode #185

Open
mbande opened this issue Nov 7, 2020 · 11 comments
Open

[FEATURE] REPL mode #185

mbande opened this issue Nov 7, 2020 · 11 comments
Labels
feature New feature, enhancement or request

Comments

@mbande
Copy link

mbande commented Nov 7, 2020

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.

@mbande mbande added the feature New feature, enhancement or request label Nov 7, 2020
@mgielda
Copy link
Contributor

mgielda commented May 3, 2021

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)

@Mattie
Copy link

Mattie commented Aug 31, 2021

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.

@tirkarthi
Copy link

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()

@afparsons
Copy link

afparsons commented Aug 17, 2022

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

@FergusFettes
Copy link

FergusFettes commented May 8, 2023

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.

@FergusFettes
Copy link

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:

  • click-repl seems to have better terminal integration (can use ! to run eg. ls) and better completion
  • click-shell starts automatically? this could probably be easily fixed in click-repl though.

@FergusFettes
Copy link

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

@FergusFettes
Copy link

FergusFettes commented May 10, 2023

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

@FergusFettes
Copy link

i just made this, pretty simple really but very nice results:

https://github.com/FergusFettes/typer-shell

vrajat added a commit to vrajat/nl2sql that referenced this issue Aug 14, 2023
@melsabagh
Copy link

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()

@WilliamDEdwards
Copy link

An 'interactive' mode would be extremely useful:

  • One of our Typer apps requires an external service.
  • The first call is extremely slow.
  • Follow-up calls are fast (cached).
  • Users often run subsequent CLI commands.
  • As Typer is re-initialised for every command, every CLI command is slow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature New feature, enhancement or request
Projects
None yet
Development

No branches or pull requests

8 participants