Giter Club home page Giter Club logo

rich-argparse's Introduction

rich-argparse

python -m rich_argparse

tests pre-commit.ci status Downloads Python Version Release

Format argparse and optparse help using rich.

rich-argparse improves the look and readability of argparse's help while requiring minimal changes to the code.

Table of contents

Installation

Install from PyPI with pip or your favorite tool.

pip install rich-argparse

Usage

Simply pass formatter_class to the argument parser

import argparse
from rich_argparse import RichHelpFormatter

parser = argparse.ArgumentParser(..., formatter_class=RichHelpFormatter)
...

rich-argparse defines equivalents to all argparse's built-in formatters:

rich_argparse formatter equivalent in argparse
RichHelpFormatter HelpFormatter
RawDescriptionRichHelpFormatter RawDescriptionHelpFormatter
RawTextRichHelpFormatter RawTextHelpFormatter
ArgumentDefaultsRichHelpFormatter ArgumentDefaultsHelpFormatter
MetavarTypeRichHelpFormatter MetavarTypeHelpFormatter

Output styles

The default styles used by rich-argparse are carefully chosen to work in different light and dark themes.

Customize the colors

You can customize the colors of the output by modifying the styles dictionary on the formatter class. You can use any rich style as defined here. rich-argparse defines and uses the following styles:

{
    'argparse.args': 'cyan',  # for positional-arguments and --options (e.g "--help")
    'argparse.groups': 'dark_orange',  # for group names (e.g. "positional arguments")
    'argparse.help': 'default',  # for argument's help text (e.g. "show this help message and exit")
    'argparse.metavar': 'dark_cyan',  # for metavariables (e.g. "FILE" in "--file FILE")
    'argparse.prog': 'grey50',  # for %(prog)s in the usage (e.g. "foo" in "Usage: foo [options]")
    'argparse.syntax': 'bold',  # for highlights of back-tick quoted text (e.g. "`some text`")
    'argparse.text': 'default',  # for descriptions, epilog, and --version (e.g. "A program to foo")
    'argparse.default': 'italic',  # for %(default)s in the help (e.g. "Value" in "(default: Value)")
}

For example, to make the description and epilog italic, change the argparse.text style:

RichHelpFormatter.styles["argparse.text"] = "italic"

Customize the group name format

You can change how the names of the groups (like 'positional arguments' and 'options') are formatted by setting the RichHelpFormatter.group_name_formatter which is set to str.title by default. Any callable that takes the group name as an input and returns a str works:

RichHelpFormatter.group_name_formatter = str.upper  # Make group names UPPERCASE

Special text highlighting

You can highlight patterns in the arguments help and the description and epilog using regular expressions. By default, rich-argparse highlights patterns of --options-with-hyphens using the argparse.args style and patterns of `back tick quoted text` using the argparse.syntax style. You can control what patterns are highlighted by modifying the RichHelpFormatter.highlights list. To disable all highlights, you can clear this list using RichHelpFormatter.highlights.clear().

You can also add custom highlight patterns and styles. The following example highlights all occurrences of pyproject.toml in green:

# Add a style called `pyproject` which applies a green style (any rich style works)
RichHelpFormatter.styles["argparse.pyproject"] = "green"
# Add the highlight regex (the regex group name must match an existing style name)
RichHelpFormatter.highlights.append(r"\b(?P<pyproject>pyproject\.toml)\b")
# Pass the formatter class to argparse
parser = argparse.ArgumentParser(..., formatter_class=RichHelpFormatter)
...

Colors in the usage

The usage generated by the formatter is colored using the argparse.args and argparse.metavar styles. If you use a custom usage message in the parser, it will be treated as "plain text" and will not be colored by default. You can enable colors in user defined usage message through console markup by setting RichHelpFormatter.usage_markup = True. If you enable this option, make sure to escape any square brackets in the usage text.

Disable console markup

The text of the descriptions and epilog is interpreted as console markup by default. If this conflicts with your usage of square brackets, make sure to escape the square brackets or to disable markup globally with RichHelpFormatter.text_markup = False.

Similarly the help text of arguments is interpreted as markup by default. It can be disabled using RichHelpFormatter.help_markup = False.

Colors in --version

If you use the "version" action from argparse, you can use console markup in the version string:

parser.add_argument(
    "--version", action="version", version="[argparse.prog]%(prog)s[/] version [i]1.0.0[/]"
)

Note that the argparse.text style is applied to the version string similar to the description and epilog.

Rich descriptions and epilog

You can use any rich renderable in the descriptions and epilog. This includes all built-in rich renderables like Table and Markdown and any custom renderables defined using the Console Protocol.

import argparse
from rich.markdown import Markdown
from rich_argparse import RichHelpFormatter

description = """
# My program

This is a markdown description of my program.

* It has a list
* And a table

| Column 1 | Column 2 |
| -------- | -------- |
| Value 1  | Value 2  |
"""
parser = argparse.ArgumentParser(
    description=Markdown(description, style="argparse.text"),
    formatter_class=RichHelpFormatter,
)
...

Certain features are disabled for arbitrary renderables other than strings, including:

  • Syntax highlighting with RichHelpFormatter.highlights
  • Styling with the "argparse.text" style defined in RichHelpFormatter.styles
  • Replacement of %(prog)s with the program name

Working with subparsers

Subparsers do not inherit the formatter class from the parent parser by default. You have to pass the formatter class explicitly:

subparsers = parser.add_subparsers(...)
p1 = subparsers.add_parser(..., formatter_class=parser.formatter_class)
p2 = subparsers.add_parser(..., formatter_class=parser.formatter_class)

Generate help preview

You can generate a preview of the help message for your CLI in SVG, HTML, or TXT formats using the HelpPreviewAction action. This is useful for including the help message in the documentation of your app. The action uses the rich exporting API internally.

import argparse
from rich.terminal_theme import DIMMED_MONOKAI
from rich_argparse import HelpPreviewAction, RichHelpFormatter

parser = argparse.ArgumentParser(..., formatter_class=RichHelpFormatter)
...
parser.add_argument(
    "--generate-help-preview",
    action=HelpPreviewAction,
    path="help-preview.svg",  # (optional) or "help-preview.html" or "help-preview.txt"
    export_kwds={"theme": DIMMED_MONOKAI},  # (optional) keywords passed to console.save_... methods
)

This action is hidden, it won't show up in the help message or in the parsed arguments namespace.

Use it like this:

python my_cli.py --generate-help-preview  # generates help-preview.svg (default path specified above)
# or
python my_cli.py --generate-help-preview my-help.svg  # generates my-help.svg
# or
COLUMNS=120 python my_cli.py --generate-help-preview  # force the width of the output to 120 columns

Working with third party formatters

rich-argparse can be used with other custom formatters through multiple inheritance. For example, django defines a custom help formatter for its built in commands as well as extension libraries and user defined commands. To use rich-argparse in your django project, change your manage.py file as follows:

diff --git a/my_project/manage.py b/my_project/manage.py
index 7fb6855..5e5d48a 100755
--- a/my_project/manage.py
+++ b/my_project/manage.py
@@ -1,22 +1,38 @@
 #!/usr/bin/env python
 """Django's command-line utility for administrative tasks."""
 import os
 import sys


 def main():
     """Run administrative tasks."""
     os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'my_project.settings')
     try:
         from django.core.management import execute_from_command_line
     except ImportError as exc:
         raise ImportError(
             "Couldn't import Django. Are you sure it's installed and "
             "available on your PYTHONPATH environment variable? Did you "
             "forget to activate a virtual environment?"
         ) from exc
+
+    from django.core.management.base import BaseCommand, DjangoHelpFormatter
+    from rich_argparse import RichHelpFormatter
+
+    class DjangoRichHelpFormatter(DjangoHelpFormatter, RichHelpFormatter):  # django first
+        """A rich-based help formatter for django commands."""
+
+    original_create_parser = BaseCommand.create_parser
+
+    def create_parser(*args, **kwargs):
+        parser = original_create_parser(*args, **kwargs)
+        parser.formatter_class = DjangoRichHelpFormatter  # set the formatter_class
+        return parser
+
+    BaseCommand.create_parser = create_parser
+
     execute_from_command_line(sys.argv)


 if __name__ == '__main__':
     main()

Now the output of all python manage.py <COMMAND> --help will be colored.

Optparse support

rich-argparse now ships with experimental support for optparse.

Import optparse help formatters from rich_argparse.optparse:

import optparse
from rich_argparse.optparse import IndentedRichHelpFormatter  # or TitledRichHelpFormatter

parser = optparse.OptionParser(formatter=IndentedRichHelpFormatter())
...

You can also generate a more helpful usage message by passing usage=GENERATE_USAGE to the parser. This is similar to the default behavior of argparse.

from rich_argparse.optparse import GENERATE_USAGE, IndentedRichHelpFormatter

parser = optparse.OptionParser(usage=GENERATE_USAGE, formatter=IndentedRichHelpFormatter())

Similar to argparse, you can customize the styles used by the formatter by modifying the RichHelpFormatter.styles dictionary. These are the same styles used by argparse but with the optparse. prefix instead:

RichHelpFormatter.styles["optparse.metavar"] = "bold magenta"

Syntax highlighting works the same as with argparse.

Colors in the usage are only supported when using GENERATE_USAGE.

Legacy Windows support

When used on legacy Windows versions like Windows 7, colors are disabled unless colorama is used:

import argparse
import colorama
from rich_argparse import RichHelpFormatter

colorama.init()
parser = argparse.ArgumentParser(..., formatter_class=RichHelpFormatter)
...

rich-argparse's People

Contributors

atteggiani avatar dependabot[bot] avatar flying-sheep avatar hamdanal avatar kianmeng avatar novitae avatar pre-commit-ci[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

rich-argparse's Issues

add default values to help

Hey! Really nice extension you have made here, thanks a lot 👍

In the example screenshot in the README I see a default value being displayed at the end of the help text. However, when I used the RichHelpFormatter, the default values did not show up at the end of the respective help texts.

Is there an easy way to add it to your formatter?

Change the default of `group_name_formatter` to `str.title`

Long group names formatted in upper-case look weird and are harder to read than lower-case or title-case. The default of group_name_formatter must be "friendlier" with both long and short group names. str.title looks like a more convenient default.

  • str.upper:
    upper

  • str.title:
    title

Changing this could break tests that depend on the default format but it is better done now than after a 1.0 release.

Questions re: bracket and brace rendering

Consider the following argument parser:

import argparse
import rich.console
from rich_argparse import RichHelpFormatter

console = rich.console.Console(markup=False, emoji=False, highlight=False)
parser = argparse.ArgumentParser(
    prog="list",
    description="Show all installed applications",
    add_help=False,
    formatter_class=RichHelpFormatter,
)
parser.add_argument(
    "config_name",
    nargs="?",
    help="a configuration name",
)
parser.add_argument(
    "-r",
    "--raw",
    action="store_true",
    required=True,
    help="show apps without formatting",
)
mutex = parser.add_mutually_exclusive_group()
mutex.add_argument(
    "--rich",
    action="store_true",
    help="Rich and poor are mutually exclusive. Choose either one but not both.",
)
mutex.add_argument(
    "--poor", action="store_false", dest="rich", help="Does poor mean --not-rich 😉?"
)
parser.add_argument(
    "-s",
    "--state",
    choices=["running", "stopped"],
    help="only show apps in a given state",
)
console.print(parser.format_help())

Note the inconsistent highlighting of brackets and braces.

  • options (i.e. -s) that are not required are surrounded in unstyled brackets
  • positional arguments that are not required (i.e. config_name) are surrounded by brackets styled with argparse.args
  • the mutually exclusive options --rich and --poor are surrounded by unstyled brackets and separated by an unstyled pipe character
  • the choices for the -s option are also mutually exclusive. They are surrounded by braces, and separated by a comma, but the braces and comma are styled with argparse.metavar

Observation: The brackets around optional arguments and the pipe separating the mutually exclusive options are the only text output which can not be styled using RichHelpFormatter.styles.

Suggestion: choices for an option (i.e. -s) of which you can only choose one, and mutually exclusive options (i.e. --rich and --poor), of which you can only choose one, should be rendered consistently.

Questions:

  • Are these behaviors with [brackets|braces|commas|pipes] intentional?
  • If not, would you consider rendering all of these symbols (braces, brackets, commas, pipes) with a new style, perhaps something like "argparse.symbols"?

[Feature request] Reuse application console for argparse output

First and foremost, I love the tool, kudos to you.

Is there any out-of-the-box way to reuse the main application console for the argparse output?

I would like to generate SVGs using the save_svg() method of the console, but the RichHelpFormatter instantiates its own rich.Console when instantiated by argparse.

In the meantime, I came up with a not-so-clean solution by injecting my console as a class variable, and using that when instantiating
the formatter:

from rich_argparse import RichHelpFormatter
from rich.console import Console
from rich.theme import ThemeStack, Theme

class CustomHelpFormatter(RichHelpFormatter):
    _CUSTOM_CONSOLE: Console = None
    _CUSTOM_CONSOLE_THEME: ThemeStack = None

    @classmethod
    def set_custom_console(cls, console: Console):
        """Store custom console and backup the theme stack"""
        cls._CUSTOM_CONSOLE = console
        cls._CUSTOM_CONSOLE_THEME = console._thread_locals.theme_stack

    def __init__(self, prog: str, indent_increment: int = 2, max_help_position: int = 24, width: int = None) -> None:
        super().__init__(prog, indent_increment, max_help_position, width)

        if self._CUSTOM_CONSOLE:
            # Init console and apply theme
            self._console = self._CUSTOM_CONSOLE
            self._console._thread_locals.theme_stack = ThemeStack(Theme(self.styles))

    def __del__(self):
        if self._CUSTOM_CONSOLE:
            # Revert theme
            self._CUSTOM_CONSOLE._thread_locals.theme_stack = self._CUSTOM_CONSOLE_THEME
        return super().__del__()

And then use it like this:

import argparse
from rich.console import Console

def main():
    my_console = Console(record=True)

    # Inject my console
    CustomHelpFormatter.set_custom_console(my_console)

    # Set parser formatter
    parser = argparse.ArgumentParser(formatter_class=CustomHelpFormatter)

    # Do stuff
    ... 

    # Save SVG
    my_console.save_svg('console.svg')

[Feature Request] Showing defaults

Seeing default values in the help is very useful. argparse ships with ArgumentDefaultsHelpFormatter but ArgumentDefaultsHelpFormatter is annoying because it shows you default: None (or False) for everything, including things where the default is obviously None/False. A workaround I found a few places and have been using is something like this:

# Class to enable defaults to only be printed when they are not None or False
class ExplicitDefaultsHelpFormatter(ArgumentDefaultsHelpFormatter):
    def _get_help_string(self, action):
        if 'default' in vars(action) and action.default in (None, False):
            return action.help
        else:
            return super()._get_help_string(action)

which patches ArgumentDefaultsHelpFormatter to only show explicitly set defaults. Is this something rich-argparse could support?

TypeError: must be real number, not str

I get this error when I use richHelpFormatter with this code

from argparse import ArgumentParser
from rich_argparse import RichHelpFormatter
_p = ArgumentParser(prog= 'Comparator' ,
                    formatter_class = RichHelpFormatter)
_p.add_argument('resources',nargs='+')
_p.add_argument('-p','--precision', type=float , default=0.0005,
    help='The precision of the comparisons, %(default).10f by default.')
_a=vars( _p.parse_args() )
print( _a )

Crash when defining multiple highlight regexes with the same style

Since version 0.4.0
$ cat t.py

import argparse
from rich_argparse import RichHelpFormatter
RichHelpFormatter.highlights.append(r"'(?P<syntax>[^']*)'")
parser = argparse.ArgumentParser(formatter_class=RichHelpFormatter)
parser.print_help()

$python -m t

Traceback (most recent call last):
  File "/usr/lib/python3.10/runpy.py", line 196, in _run_module_as_main
    return _run_code(code, main_globals, None,
  File "/usr/lib/python3.10/runpy.py", line 86, in _run_code
    exec(code, run_globals)
  File "/tmp/t/t.py", line 7, in <module>
    parser.print_help()
  File "/usr/lib/python3.10/argparse.py", line 2568, in print_help
    self._print_message(self.format_help(), file)
  File "/usr/lib/python3.10/argparse.py", line 2545, in format_help
    formatter.add_arguments(action_group._group_actions)
  File "/usr/lib/python3.10/argparse.py", line 289, in add_arguments
    self.add_argument(action)
  File "/tmp/t/venv/lib/python3.10/site-packages/rich_argparse.py", line 135, in add_argument
    self._current_section.rich_actions.extend(self._rich_format_action(action))
  File "/tmp/t/venv/lib/python3.10/site-packages/rich_argparse.py", line 239, in _rich_format_action
    help = self._rich_expand_help(action) if action.help and action.help.strip() else None
  File "/tmp/t/venv/lib/python3.10/site-packages/rich_argparse.py", line 221, in _rich_expand_help
    rich_help.highlight_regex("|".join(self.highlights), style_prefix="argparse.")
  File "/tmp/t/venv/lib/python3.10/site-packages/rich/text.py", line 579, in highlight_regex
    for match in re.finditer(re_highlight, plain):
  File "/usr/lib/python3.10/re.py", line 247, in finditer
    return _compile(pattern, flags).finditer(string)
  File "/usr/lib/python3.10/re.py", line 303, in _compile
    p = sre_compile.compile(pattern, flags)
  File "/usr/lib/python3.10/sre_compile.py", line 788, in compile
    p = sre_parse.parse(p, flags)
  File "/usr/lib/python3.10/sre_parse.py", line 955, in parse
    p = _parse_sub(source, state, flags & SRE_FLAG_VERBOSE, 0)
  File "/usr/lib/python3.10/sre_parse.py", line 444, in _parse_sub
    itemsappend(_parse(source, state, verbose, nested + 1,
  File "/usr/lib/python3.10/sre_parse.py", line 838, in _parse
    raise source.error(err.msg, len(name) + 1) from None
re.error: redefinition of group name 'syntax' as group 3; was group 2 at position 61

Formatting error when printing via rich.console

When printing the results of format_help() from an argparser that uses RichHelpFormatter() as a formatter, the output is not formatted correctly if those results are printed using rich.console.Console. I've created a program which reproduces this problem:

# reproduce a formatting bug in RichHelpFormatter()

import argparse
import rich_argparse
import rich.console

console = rich.console.Console(markup=False, emoji=False, highlight=False)
prsr = argparse.ArgumentParser(
    prog="config",
    description="Edit or show the location of the user configuration file.",
    formatter_class=rich_argparse.RichHelpFormatter,
)
prsr.add_argument(
    "action",
    choices=["edit", "file", "convert"],
    help="""'edit' edits the configuration file in your preferred editor.
            'file' shows the name of the configuration file.
            'convert' writes a new .toml configuration file with the same settings as the old .ini configuration file.""",
)

console.rule("printing help with print()")
print(prsr.format_help())

console.rule("printing help with rich.console.print")
console.print(prsr.format_help())

Here's a screenshot of the output:
screenshot

Note that when the output is printed using console.print(prsr.format_help()) the help text for the single positional argument does not wrap correctly.

My current (untested) hypothesis is that this has something to do with having nested rich.console objects. The current implementation of format_help() uses the .capture() context manager to collect the output before returning.

Add Poetry installation instructions

Firstly thanks for putting this together; it works perfectly 👍.

Since the repo has a nice pyproject.toml it's very easy to include into a Poetry based project, and I just want to propose adding additional installation instructions to the readme:

Installation

Copy the file rich_argparse.py to your project, or...

Poetry

poetry add git+https://github.com/hamdanal/rich-argparse#main

(Sorry that this isn't a full pull-request; figured its a small enough change.)

Request for collaboration

Hey, I'm one of the maintainers of cmd2, and we are exploring ways we can either incorporate rich into our library as a dependency, and/or make it easier for users of cmd2 to utilize rich in their own applications. Our issue capturing the discussion so far is at python-cmd2/cmd2#1251.

In my early exploration of how best to do this in cmd2, I found this excellent project, which solves an important part of the problem for us. I have identified an initial list of changes/improvements to rich-argparse which would really benefit our work. Here's a brief list:

  • we would like more styles and more granular styles for different elements in the help/usage output
  • the current implementation creates it's own rich.console objects, we would be interested in exploring changes to rich-argparse which could make it easier for a user/subclasser of RichHelpFormatter() to provide their own rich.console object (which I get will be tricky or maybe impossible since argparse.ArgumentParser instantiates this class.
  • the current implementation of RichHelpFormatter.format_help() uses rich.console.capture which seems to create some formatting problems if you have "embedded" rich.console objects, ie one console object processing the output of format_help() while another console is capturing inside of format_help(). Not sure yet if this is a rich-argparse problem or a rich problem.

If you are open to collaboration/discussion/pull requests of our changes/enhancements, we would love to work with you. That would be our preferred approach if that works for you. If not, that's fine too, the MIT license for this project is compatible with the cmd2 license and we could fork your work to make it work better for our use case.

Are you open to collaboration, discussion, and pull requests from me?

Required options not coloured in the usage

$ cat t.py

import argparse
from rich_argparse import RichHelpFormatter
parser = argparse.ArgumentParser(formatter_class=RichHelpFormatter)
parser.add_argument("--optional")
parser.add_argument("--required", required=True)
parser.print_help()

$python -m t
Screenshot of a required option not colored in the usage

RichHelpFormatter does not replace `%(prog)s` in text

$ cat t.py
import argparse

from rich_argparse import RichHelpFormatter

parser = argparse.ArgumentParser(
    "awesome_program",
    description="This is the %(prog)s program.",
    formatter_class=RichHelpFormatter,
)
parser.add_argument("--version", action="version", version="%(prog)s 1.0.0")
parser.parse_args()
$ python t.py --version
%(prog)s 1.0.0
$ python t.py --help
usage: awesome_program [-h] [--version]

This is the %(prog)s program.

OPTIONS
  -h, --help  show this help message and exit
  --version   show program's version number and exit

Expected behaviour: replace %(prog)s by awesome_program in the printed text

Use a custom text wrapper so it keeps new lines and adds additional new lines if needed.

test code:

#!/usr/bin/env python3

import argparse
from rich_argparse import RichHelpFormatter
import re
import sys
from pathlib import Path

from rich import print


prog_name = "sub-full-to-forced"
prog_version = "1.0.0"


RichHelpFormatter.styles["argparse.metavar"] = "bold color(231)"
RichHelpFormatter.styles["argparse.prog"] = "bold cyan"
RichHelpFormatter.styles["argparse.args"] = "green"
RichHelpFormatter.styles["argparse.metavar"] = "bold color(231)"
RichHelpFormatter.group_name_formatter = str.upper

parser = argparse.ArgumentParser(
    prog=prog_name,
    add_help=False,
    formatter_class=lambda prog: RichHelpFormatter(prog, width=80, max_help_position=23),
)

parser.add_argument("-h", "--help",
                    action="help",
                    default=argparse.SUPPRESS,
                    help="show this help message.")
parser.add_argument("-v", "--version",
                    action="version",
                    version=f"[bold cyan]{prog_name}[/bold cyan] [not bold white]{prog_version}[/not bold white]",
                    help="show version.")
parser.add_argument("-i", "--input",
                    type=str,
                    default="",
                    help="input path")
parser.add_argument("-o", "--output",
                    type=str,
                    default="",
                    help="""Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer et massa odio. Proin vel varius ipsum, ac tincidunt sapien.

Proin ut placerat sem. Nulla tristique placerat leo, pulvinar dignissim nisl mattis quis.

Quisque id risus quis risus cursus mattis id nec quam. Nullam luctus sem non augue malesuada, eu fringilla nibh malesuada. Nullam gravida nibh rhoncus ligula tristique convallis.""")
args = parser.parse_args()


def main():
    if len(sys.argv) == 1:
        parser.print_help(sys.stderr)
        sys.exit(1)

Current output:

py -m sub_full_to_forced -h
USAGE: sub-full-to-forced [-h] [-v] [-i INPUT] [-o OUTPUT]

OPTIONS:
  -h, --help           show this help message.
  -v, --version        show version.
  -i, --input INPUT    input path
  -o, --output OUTPUT  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                       Integer et massa odio. Proin vel varius ipsum, ac
                       tincidunt sapien. Proin ut placerat sem. Nulla tristique
                       placerat leo, pulvinar dignissim nisl mattis quis.
                       Quisque id risus quis risus cursus mattis id nec quam.
                       Nullam luctus sem non augue malesuada, eu fringilla nibh
                       malesuada. Nullam gravida nibh rhoncus ligula tristique
                       convallis.

Proposed output:

py -m sub_full_to_forced -h
USAGE: sub-full-to-forced [-h] [-v] [-i INPUT] [-o OUTPUT]

OPTIONS:
  -h, --help           show this help message.
  -v, --version        show version.
  -i, --input INPUT    input path
  -o, --output OUTPUT  Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                       Integer et massa odio. Proin vel varius ipsum, ac
                       tincidunt sapien.

                       Proin ut placerat sem. Nulla tristique placerat leo,
                       pulvinar dignissim nisl mattis quis.

                       Quisque id risus quis risus cursus mattis id nec quam.
                       Nullam luctus sem non augue malesuada, eu fringilla
                       nibh malesuada. Nullam gravida nibh rhoncus ligula
                       tristique convallis.

text wrapper codes that i've written for another project:
(takes a list of strings as input and the output is also a list of strings but can be modified easily)

def text_justify(text, width):
    words = text.split()

    if len(words) < 2:
        return text.ljust(width)

    if len(text) < width * 0.75:
        return text.ljust(width)

    spaces_needed = width - sum(len(word) for word in words)
    spaces_between_words = spaces_needed // (len(words) - 1)
    extra_spaces = spaces_needed % (len(words) - 1)

    justified_words = []
    for i in range(len(words) - 1):
        word = words[i]
        spaces_to_add = spaces_between_words + (1 if i < extra_spaces else 0)
        justified_words.append(word + ' ' * spaces_to_add)
    justified_words.append(words[-1])

    return ''.join(justified_words)


def text_wrapper(source_text, width, separator_chars=[" "], justify="left"):
    output = ""
    if type(source_text) != list:
        source_text = [source_text]

    if justify == "left":
        source_text = [s.rstrip() for s in source_text]
    if justify in ["middle", "justified"]:
        source_text = [s.strip() for s in source_text]
    if justify == "right":
        source_text = [s.lstrip() for s in source_text]

    for line in source_text:
        current_length = 0
        latest_separator = -1
        current_chunk_start = 0
        char_index = 0
        while char_index < len(line):
            if line[char_index] in separator_chars:
                latest_separator = char_index
            output += line[char_index]
            current_length += 1
            if current_length == width:
                if (latest_separator > current_chunk_start and char_index + 1 < len(line) and
                line[char_index + 1] not in [c for c in separator_chars if c == " "]):
                    cutting_length = char_index - latest_separator
                    if cutting_length:
                        output = output[:-cutting_length]
                    output += "\n"
                    current_chunk_start = latest_separator + 1
                    char_index = current_chunk_start
                else:
                    output += "\n"
                    current_chunk_start = char_index + 1
                    latest_separator = current_chunk_start - 1
                    char_index += 1
                if char_index < len(line) and line[char_index] in " ":
                    char_index += 1
                current_length = 0
            else:
                char_index += 1
        output += "\n"

    output = [o.rstrip().rstrip("\n") for o in output.rstrip("\n").split("\n")]

    if justify == "middle":
        output = [f"{' ' * math.ceil((width - len(o)) / 2)}{o}" for o in output]
        output = [o.rstrip() for o in output]
    if justify == "right":
        output = [o.rjust(width) for o in output]
    if justify == "justified":
        output = [text_justify(o, width) for o in output]
        output = [o.rstrip() for o in output]
    return output

In work:
img

Minor: Pretty formatting of default values

Awesome plugin.

When using formatter_class=ArgumentDefaultsRichHelpFormatter

Currently the default values display fine but it would be nice to pretty-print them if possible.
Example of current:
image

I don't mind attempting a PR for it.

Do not print help output

Return help output as a string instead of printing it in the formatter.

Currently this does not work:

s = parser.format_help()  # this line prints on the screen, it shouldn't
print(s.upper())  # prints empty string

PR #56 introduced a formatting bug

After further experimenting, I think I found a bug which was introduced by #56. Consider the following:

import argparse
import rich.console
from rich_argparse import RichHelpFormatter

console = rich.console.Console(markup=False, emoji=False, highlight=False)
parser = argparse.ArgumentParser(
    prog="list",
    description="Show all installed applications",
    add_help=False,
    formatter_class=RichHelpFormatter,
)
parser.add_argument(
    "config_name",
    nargs="?",
    help="a configuration name",
)
parser.add_argument(
    "-r",
    "--raw",
    action="store_true",
    required=True,
    help="show apps without formatting",
)
parser.add_argument(
    "-s",
    "--state",
    choices=["running", "stopped"],
    help="only show apps in a given state",
)
console.print(parser.format_help())

In the released 1.0.0 version, the output is:

Usage: list -r [-s {running,stopped}] [config_name]

Show all installed applications

Positional Arguments:
  config_name           a configuration name

Options:
  -r, --raw             show apps without formatting
  -s, --state {running,stopped}
                        only show apps in a given state

In the unreleased main branch, the output is:

Usage: list -r [-s {running,stopped}]
[config_name]

Show all installed applications

Positional Arguments:
  config_name           a configuration name

Options:
  -r, --raw             show apps without formatting
  -s, --state {running,stopped}
                        only show apps in a given state

Incorrect line breaks put metavars on a new row all alone, detached from the argument

enjoying this library but ran into a situation where regular argparse does line breaks correctly and this does them in a way that makes it hard to understand the options. Here's the output of rich_argparse that's an issue. The example is from for the help text of the pdfalyzer, a PDF analysis tool i just released. (thess are just a snippet to show the issue but the full help text can be viewed in the pdfalyzer README here and the argparse code that creates them can be viewed here if either of those things is helpful).

Take a look at where the PCT_CONFIDENCE and STREAM_DUMP_DIR metavars end up:

FINE TUNING:
  Settings that affect aspects of the analyis and output

  --force-display-threshold            chardet.detect() scores encodings from 0-100 pct but only above this are displayed                                                                              
PCT_CONFIDENCE                                                                                                                                                                                         
  --force-decode-threshold             extremely high (AKA 'above this number') PCT_CONFIDENCE scores from chardet.detect() as to the likelihood some binary data was written with a particular        
PCT_CONFIDENCE                         encoding will cause  the pdfalyzer to do a force decode of that with that encoding. (chardet is a sophisticated libary; this is pdfalyzer's way of harnessing   
                                       that intelligence)                                                                                                                                              
FILE EXPORT:
  Export to various kinds of files
                                                                         
  -str, --extract-streams-to           extract all binary streams in the PDF to files in STREAM_DUMP_DIR then exit (requires pdf-parser.py)                                                            
STREAM_DUMP_DIR                                                                                                                                                                                        

PCT_CONFIDENCE and STREAM_DUMP_DIR are all alone in the universe with no map home.

For comparison this is how regular argparse handles it, placing the metavars on the same line as the argument they relate to.

FINE TUNING:
  Settings that affect aspects of the analyis and output

  --force-display-threshold PCT_CONFIDENCE
                        chardet.detect() scores encodings from 0-100 pct but only above this are displayed (default: 20.0)
  --force-decode-threshold PCT_CONFIDENCE
                        extremely high (AKA 'above this number') PCT_CONFIDENCE scores from chardet.detect() as to the likelihood some binary data was written with a particular encoding will cause
                        the pdfalyzer to do a force decode of that with that encoding. (chardet is a sophisticated libary; this is pdfalyzer's way of harnessing that intelligence) (default: 50.0)

FILE EXPORT:
  Export to various kinds of files

  -txt OUTPUT_DIR, --txt-output-to OUTPUT_DIR
                        write analysis to uncolored text files in OUTPUT_DIR (in addition to STDOUT)
  -svg OUTPUT_DIR, --export-svgs OUTPUT_DIR
                        export SVG images of the analysis to OUTPUT_DIR (in addition to STDOUT)
  -html OUTPUT_DIR, --export-html OUTPUT_DIR
                        export SVG images of the analysis to OUTPUT_DIR (in addition to STDOUT)
  -str STREAM_DUMP_DIR, --extract-streams-to STREAM_DUMP_DIR
                        extract all binary streams in the PDF to files in STREAM_DUMP_DIR then exit (requires pdf-parser.py)
  -pfx PREFIX, --file-prefix PREFIX
                        optional string to use as the prefix for exported files of any kind

Remove colour for metavars optional square brackets in usage

Description

Currently the usage text is formatted so that all the text in the metavar string gets coloured, including any spaces and square brackets to indicate optional metavars (I will refer to them as optional square brackets).

Example output

This is an example of a current usage coloured output.
Screenshot 2024-06-28 at 1 36 27 PM

This is the same usage output but a background colour has been added to the style for both argparse.args and argparse.metavars to better illustrate the behaviour.
Screenshot 2024-06-28 at 2 26 43 PM

Suggestion for improvement

I am raising this issue to scope for interest in changing the behaviour of rich-argparse to only colour actual metavars in the usage (leaving spaces and optional square brackets with the default colour).
The usage output with the new behaviour would look like this:
Screenshot 2024-06-28 at 1 36 38 PM
And the same usage output with the background colours:
Screenshot 2024-06-28 at 2 26 26 PM

This new behaviour would not break/change any current implementation apart from the usage output itself.
A full working implementation with test and 100% coverage is available in the usage_highlight_strict_metavars branch of my forked rich-argparse repo.

If this is something of interest, I can open a PR.

Thank you for the amazing module @hamdanal! 🚀

`RichHelpFormatter` does not work with `argparse.ArgumentDefaultsHelpFormatter`

When the two formatters are used together, the defaults are not printed.
Reproducer:

import argparse
from rich_argparse import RichHelpFormatter

class ArgDefRichHelpFormatter(RichHelpFormatter, argparse.ArgumentDefaultsHelpFormatter): ...

parser = argparse.ArgumentParser(formatter_class=ArgDefRichHelpFormatter)
parser.add_argument("--option", default="def", help="help of option")
parser.parse_args()

produces:

usage: t.py [-h] [--option OPTION]

OPTIONAL ARGUMENTS:
  -h, --help       show this help message and exit
  --option OPTION  help of option

Adding line breaks?

Of all aspects challenging the readability of an argparse output for the 95% of us, or making people avoid reading too much, perhaps the density of the text is one of the worst sticking points. This is because the chunking of the content is not very clear even with colors used: each option's description will begin either at the same line as the option name or on the next line ― yielding a complex layout which can IMHO be alleviated by an empty line between every pair of options, bestowing clear visual separation between the options being displayed.

Could be nice adding the option to add a line space between every pair of arguments (as well as before and after the whole output as a lesser concern).

Suppressed group titles should not be printed

To reproduce:

import argparse
from rich_argparse import RichHelpFormatter

parser = argparse.ArgumentParser("PROG", formatter_class=RichHelpFormatter)
group = parser.add_argument_group(title=argparse.SUPPRESS)
group.add_argument("foo")
parser.print_help()

Produces:

Usage: PROG [-h] foo

Options:
  -h, --help  show this help message and exit

==Suppress==:
  foo

Expected:

Usage: PROG [-h] foo

Options:
  -h, --help  show this help message and exit

  foo

Add raise.error as red-bold type

Feature request

I think would be nice if the parser.error can be of type:

error_message = f"[bold red]Error:[/bold red] {error}"
console.print(box(error_message, style="bold red"), highlight=False)

Context:

def parse_arguments():
    parser = argparse.ArgumentParser(formatter_class=RichHelpFormatter, (...))
    # adding argument
    parser.add_argument('-a','--auto', action='store_true', help='Do something in automatic mode (...)')
    . . .
    args = parser.parse_args()
    # Validation:
    ## check emptyargs
    if len(sys.argv) == 1:
        raise parser.error('This program needs at least one argument to run. Use -h to check for help')
   . . . 

Desired state:
That the messages from raise.error would be in some red-color using the default RichHelpFormatter. The traceback exception should be fixed (reason of the request)
image

Group titles should not wrap

import argparse
from rich_argparse import RichHelpFormatter

parser = argparse.ArgumentParser(
    prog="PROG", formatter_class=lambda prog: RichHelpFormatter(prog, width=4)
)
parser.print_help()

produces
image

[BUG] Usage without highlighting if any metavar spans within multiple lines

Decription of the bug

The Usage: ... string does not get coloured with proper highlighting styles if any metavar string spans within multiple lines.
This can happen, for example, when a mutual exclusive option is defined and the terminal window is not wide enough to display the whole metavar arguments within a single line.

How to reproduce the error

To reproduce the error, you can run the following script:

import argparse
from rich_argparse import RichHelpFormatter
import sys

# Create the format class
class SmallWidthFormmater(RichHelpFormatter):
    """
    Class to format argparse Help and Description using the raw help and description classes from rich_argparse
    """
    def __init__(self, prog):
        super().__init__(prog, width=4)

# Create parser with smaller width
smallwidthparser = argparse.ArgumentParser(formatter_class=SmallWidthFormmater)

for parser in [smallwidthparser]:
    # Add mutually exclusive group
    meg = parser.add_mutually_exclusive_group()
    # Add arguments to mutually exclusive group
    meg.add_argument(
        "--option1",
        metavar="metavar1",
        nargs="*",
    )
    meg.add_argument(
        "--option2",
        nargs=5,
    )
smallwidthparser.print_usage()

Error output

This is the error-reproduction-script output :
Screenshot 2024-06-28 at 10 42 09 AM
As you can see the usage text does not get coloured.

Reasons behind the error

The issue comes from line 291 the find_span function:

_start = text.index(stripped, pos)

Specifically, the text variable might have newline characters and spaces/tabs (I will refer to them as indentation characters) to properly indent the usage string.
However, if the indentation characters are inserted within a metavar string (like it happens in the error reproduction case above, where the indentation characters are added between the OPTION2 metavars) the text.index(stripped, pos) command will raise a ValueError because it will not find the exact metavar string.

Solution

Instead of finding the index with text.index(stripped, pos) we need to use regex to find the index of "any string that matches stripped with indentation characters in it".
This can be achieved by replacing stripped with the proper regex pattern, and find the start and end point through re.finditer.
I opened a PR with the implemented the solution.

Implement `argparse.prog` style

Add a new style to highlight the name of the program. This might look like

RichHelpFormatter.styles["argparse.prog"] = "deep_sky_blue1"

This style would be applied to the prog parameter of the argparse.ArgumentParser() constructor. In the example below, the prog is connect:

prsr = argparse.ArgumentParser(
    prog="connect",
    description="Edit or show the location of the user configuration file.",
    usage="%(prog)s [-h] config_name\n       %(prog)s [-h] url [user] [password]",
    formatter_class=rich_argparse.RichHelpFormatter,
)

This formatting should be applied whether the usage parameter is given, as it is in the example above, or if the usage parameter is absent.

How to disable styles?

Discussed in #120

Originally posted by bulletmark May 17, 2024
I have a number of Python CLI programs, some of which use subparsers, so I thought I'd try this to see how it looks. However, I find that it interprets rich styles in my code which I did not intend. E.g. it seems I have commonly written "applications[s]" to indicate one or more applications but I am getting a strike-through on all those lines. How can I disable style interpretation?

I already tried RichHelpFormatter.highlights.clear() and RichHelpFormatter.usage_markup = False but no difference.

[Request] Some additional stylings

Hi!

First of all, thank you for this library. It's really promising. This is what I currently use:

prog_name = 'substoforced'
prog_version = '1.0.1'


class RParse(argparse.ArgumentParser):
    def _print_message(self, message, file=None):
        if message:
            if message.startswith('usage'):
                message = f'[bold cyan]{prog_name}[/bold cyan] {prog_version}\n\n{message}'
                message = re.sub(r'(-[a-z]+\s*|\[)([A-Z]+)(?=]|,|\s\s|\s\.)', r'\1[{}]\2[/{}]'.format('bold color(231)', 'bold color(231)'), message)
                message = re.sub(r'((-|--)[a-z]+)', r'[{}]\1[/{}]'.format('green', 'green'), message)
                message = message.replace('usage', f'[yellow]USAGE[/yellow]')
                message = message.replace('options', f'[yellow]FLAGS[/yellow]', 1)
                message = message.replace(self.prog, f'[bold cyan]{self.prog}[/bold cyan]')
            message = f'[not bold white]{message.strip()}[/not bold white]'
            print(message)


class CustomHelpFormatter(argparse.RawTextHelpFormatter):
    def _format_action_invocation(self, action):
        if not action.option_strings or action.nargs == 0:
            return super()._format_action_invocation(action)
        default = self._get_default_metavar_for_optional(action)
        args_string = self._format_args(action, default)
        return ', '.join(action.option_strings) + ' ' + args_string


parser = RParse(
    add_help=False,
    formatter_class=lambda prog: CustomHelpFormatter(prog)
)
parser.add_argument('-h', '--help',
                    action='help',
                    default=argparse.SUPPRESS,
                    help='show this help message.')
parser.add_argument('-v', '--version',
                    action='version',
                    version=f'[bold cyan]{prog_name}[/bold cyan] [not bold white]{prog_version}[/not bold white]',
                    help='show version.')
parser.add_argument('-s', '--sub',
                    default=argparse.SUPPRESS,
                    help='specifies srt input')
parser.add_argument('-f', '--folder',
                    metavar='DIR',
                    default=None,
                    help='specifies a folder where [bold color(231)]SubsMask2Img[/bold color(231)] generated timecodes (optional)\nyou should remove the junk from there manually')
args = parser.parse_args()

With this I get a result like this:
img
I don't think all of these is currently possible with rich-argparse, but it would be great. These are the missing pieces compared to this (at least I think):

  • no styling for the comma between long and short option (I think this should be the default but an option would be nice)
  • missing colon after groups
  • separate styling for metavar
  • uppercase "usage" to match groups
  • same styling for usage options as in groups
  • rename default group (options)
  • option to print version before help or other additional texts (I guess this is possible but I don't know how)

Broken support of Windows Powershell

Hello and thank you for your great module!

I'm encountering a strange problem when using the module in a native Windows Powershell window.

Here's the sample code I'm using:

import argparse
from rich_argparse import RichHelpFormatter
from rich import print as rprint

def main():
    rprint("[italic blue]It's working[/]")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(formatter_class=RichHelpFormatter)
    parser.parse_args()
    main()

Here's the output in a native Windows Powershell window, the output from rich_argparse is broken, however the output from rich is ok.
ps

The output from Windows Terminal shows that both rich_argparse and rich work.
wt

allow newlines in `"version"`

When using

parser = argparse.ArgumentParser(
    description="foo",
    # formatter_class=argparse.RawTextHelpFormatter,
    formatter_class=RichHelpFormatter,
)

parser.add_argument(
    "--version",
    "-v",
    action="version",
    version=get_version_text(),
    help="display version information",
)

def get_version_text():
    return "a\nb\n\n\nc"

newlines in the output are suppressed. Would be great to allow for those.

Brackets around optional positional args and options are a different color

I get a help message like this from the code below.

screenshot

The square brackets around the optional positional argument are red, while those around the options are not. I am not sure whether this is intentional, but it looks incorrect. I would expect all sets of brackets to be the same color (default, not red in this example).

import argparse
from rich_argparse import RichHelpFormatter


def main() -> None:
    parser = argparse.ArgumentParser(
        formatter_class=RichHelpFormatter,
    )

    RichHelpFormatter.styles["argparse.args"] = "red"
    parser.add_argument(
        "positional",
        nargs=argparse.OPTIONAL,
    )
    parser.add_argument(
        "--option",
    )

    args = parser.parse_args()


if __name__ == "__main__":
    main()

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.