ewels / rich-click

Format click help output nicely with rich.
https://ewels.github.io/rich-click/
MIT License
604 stars 34 forks source link

Send usage information to stderr when something fails. #166

Closed Julian closed 5 months ago

Julian commented 6 months ago

Matches click's behavior.

Closes: #164

dwreeves commented 6 months ago

I'll admit I am a little confused about something with your original report and this PR, I'm hoping there is a way we can resolve it.

I have a file called foo.py with the following contents:

# import rich_click as click
import click

@click.group("my-command")
@click.argument("foo")
@click.option("--bar", "-b", help="Lorem ipsum", show_default="someval")
def cli(foo, bar):
    """Help text for CLI"""

@cli.command("subcommand")
def subcommand(foo, bar):
    """Help text for subcommand"""

if __name__ == "__main__":
    cli()

And I use this script to execute foo.py and examine the outputs:

from subprocess import Popen, PIPE
process = Popen(["python", "foo.py", "--help"], stdout=PIPE, stderr=PIPE)
stdout, stderr = process.communicate()
print(f"{len(stdout)=}", f"{len(stderr)=}")

When I run it with import click, I get the following: len(stdout)=225 len(stderr)=0

When I run with import rich_click as click, I get the following: len(stdout)=3447 len(stderr)=0

When I run with import rich_click as click, and also use your code change, I get the following: len(stdout)=1 len(stderr)=3446

Which is to say, the default behavior of writing to stdout seems correct and in line with what Click does, but this PR seems to change it so the default behavior is that it writes to stderr.


What I thought you had in mind when you said we aren't printing to stderr was more something like this. The following code adds a --fake-flag to trigger an error:

from subprocess import Popen, PIPE
process = Popen(["python", "foo.py", "--fake-flag"], stdout=PIPE, stderr=PIPE)
stdout, stderr = process.communicate()
print(f"{len(stdout)=}", f"{len(stderr)=}")

When I run it with import click, I get the following: len(stdout)=0 len(stderr)=117

When I run with import rich_click as click, I get the following: len(stdout)=2074 len(stderr)=0

^ This to me is erroneous behavior on rich-click's part. By default, rich-click should write to stdout, it's just in error-handling situations that it should be writing to stderr, but it isn't.

Do you agree with everything I've stated? Let me know if anything here looks wrong.


In 1.8.0, which I can potentially release an alpha version of soon (mostly, I'm just waiting to finish the docs, aaah), you'll notice the __init__ for RichHelpFormatter looks different than in 1.7.x:

    def __init__(
        self,
        indent_increment: int = 2,
        width: Optional[int] = None,
        max_width: Optional[int] = None,
        *args: Any,
        console: Optional["Console"] = None,
        config: Optional[RichHelpConfiguration] = None,
        file: Optional[IO[str]] = None,
        **kwargs: Any,
    ) -> None:

TLDR, 1.7.0 was a massive shake-up in rich-click's internals, but some of the internals were not fully fleshed out, and so a big part of what 1.8.0 is doing is making those internals better.

There is potential, somewhere in the code, to initialize RichHelpFormatter() with file=sys.stderr in error handling contexts, and this would resolve the issue.

A related issue (which is not a prerequisite for contributing a fix; this can be worked on separately as it's a bigger matter) is that the test suite right now does not distinguish between stderr or stdout, but it should. Hence why your tests are passing. I do believe that the tests should distinguish between those two things.


I will say, in the dev branch for 1.8.0a, we are currently not writing to stderr for error messages. So the erroneous behavior you've flagged is still existent in 1.8.0.

This is something we could potentially address in 1.8.0, and it may be difficult to address in 1.7.x. And in that case, I'm really sorry I encouraged you to push a change to 1.7.x 😦.


TLDR of everything:

Julian commented 6 months ago

Do you agree with everything I've stated? Let me know if anything here looks wrong.

Sorry, it's me who was clearly quite lazy and didn't look carefully after getting my own suite to pass! I indeed only have test cases for the error behavior and didn't even check the "normal" one.

Everything you wrote looks correct to me!

This is something we could potentially address in 1.8.0, and it may be difficult to address in 1.7.x. And in that case, I'm really sorry I encouraged you to push a change to 1.7.x 😦.

Don't worry about it, clearly I didn't spend long for better or worse, I mostly gave up after I couldn't get the epilog change fixed. I'll try to file a separete issue for that one. But I'm more than happy either to wait for 1.8, or else to give this another shot when I have more spare time than I do at the minute, if you haven't gotten to it yourself (right now is super busy as I'm busy with Google Summer of Code, but hopefully once that gets started properly I may have more time...)