hamdanal / rich-argparse

A rich help formatter for argparse
https://pypi.org/project/rich-argparse/
MIT License
130 stars 12 forks source link

Could rich-argparse adopt some of the typer visual style? #136

Open peterjc opened 2 days ago

peterjc commented 2 days ago

I have been very impressed with https://typer.tiangolo.com/ as an argparse alternative for three reasons:

  1. Pretty and colorful help - something rich-argparse also does
  2. Pretty and colorful errors for invalid command lines - something rich-argparse also does
  3. Elegant type based API definition, out of scope here - but sadly it inherits the limitation from click that it can't handle argparse's nargs="+" or nargs="*" functionality https://docs.python.org/dev/library/argparse.html#nargs which some of my old code relies on.

Given I am sticking with argparse to maintain an existing API, I'd like to see how far I can push on points (1) and (2) alone. Using rich-argparse out-of-the-box gives an immediate and very easy improvement by adding color the argparse output - thank you!

Is extending rich-argparse to deviate further from the current formatting something you'd consider in-scope (to mimic typer output or otherwise)?

Example help output (point 1)

Screenshot 2024-11-05 at 20 04 57

So rich-argparse makes the help text much nicer, but doesn't change the layout. Typer goes further with ASCII box art.

Example error in command (point 2)

Screenshot 2024-11-05 at 19 55 14

Again rich-argparse makes the help text nicer, but doesn't change the layout. Typer goes further with ASCII box art.

Code for the above example

Demo using argparse only:

from argparse import ArgumentParser

def say_hello(name: str = "World"):
    print(f"Hello {name}")

parser = ArgumentParser()
parser.add_argument("--name", default="World", help="Who or what to greet?")
parsed_args = parser.parse_args()
say_hello(name=parsed_args.name)

Demo using argparse with rich-argparse only:

from argparse import ArgumentParser
from rich_argparse import RichHelpFormatter

def say_hello(name: str = "World"):
    print(f"Hello {name}")

parser = ArgumentParser(formatter_class=RichHelpFormatter)
parser.add_argument("--name", default="World", help="Who or what to greet?")
parsed_args = parser.parse_args()
say_hello(name=parsed_args.name)

 Demo using typer:

import typer
from typing import Annotated

def say_hello(
    name: Annotated[
        str, typer.Option(help="Who or what to greet?", metavar="NAME")
    ] = "World",
):
    print(f"Hello {name}")

if __name__ == "__main__":
    typer.run(say_hello)
peterjc commented 2 days ago

Bonus example combining yapx 0.5.2 https://github.com/fresh2dev/yapx with rich-argparse:

from typing import Annotated
from rich_argparse import RichHelpFormatter
import yapx

def say_hello(
    name: Annotated[
        str, yapx.arg(help="Who or what to greet?", metavar="NAME")
    ] = "World",
):
    print(f"Hello {name}")

yapx.run(say_hello, formatter_class=RichHelpFormatter)
Screenshot 2024-11-05 at 20 33 21

(Minor bug in yapx which I will report - the horizontal rule from yapx is too wide for the terminal)

This is interesting to me as it combines visual improvements over plain argparse (thanks to rich-argparse) with a type annotation based API definition a bit like typer.

Update: You don't even need to use use RichHelpFormatter explicitly, yapx does this automatically if rich-argparse is installed! See https://github.com/fresh2dev/yapx/issues/2