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

Color part of the help massage #192

Closed
OriBenHur-akeyless opened this issue Apr 28, 2024 · 8 comments
Closed

Color part of the help massage #192

OriBenHur-akeyless opened this issue Apr 28, 2024 · 8 comments
Labels
bug Something isn't working

Comments

@OriBenHur-akeyless
Copy link

I would like to color only part of the helm message using click.style but for some reason, the spacing got broken

The reason I would do such a thing is to outline a note, in the help message

Class Mutex(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if: list = kwargs.pop("not_required_if")

        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs["help"] = f'{kwargs.get("help", "")}, NOTE: Mutually exclusive with: {", ".join(self.not_required_if) if len(self.not_required_if) < 2 else  ", ".join(self.not_required_if[:-1])+ " and " + self.not_required_if[-1]}'
        super(Mutex, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        current_opt: bool = self.name in opts
        for mutex_opt in self.not_required_if:
            if mutex_opt in opts:
                if current_opt:
                    raise click.UsageError(f'Illegal usage: {str(self.name)} '
                                           f'is mutually exclusive with {str(mutex_opt)}.')
                else:
                    self.required = None
        return super(Mutex, self).handle_parse_result(ctx, opts, args)

result with
image

but using click.style

class Mutex(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if: list = kwargs.pop("not_required_if")

        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs["help"] = f'''{kwargs.get("help", "")} {click.style("NOTE:", bold=True, fg="magenta")} {click.style(f'Mutually exclusive with: {", ".join(self.not_required_if) if len(self.not_required_if) < 2 else ", ".join(self.not_required_if[:-1]) + " and " + self.not_required_if[-1]}', fg='magenta')}'''.strip()
        super(Mutex, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        current_opt: bool = self.name in opts
        for mutex_opt in self.not_required_if:
            if mutex_opt in opts:
                if current_opt:
                    raise click.UsageError(f'Illegal usage: {str(self.name)} '
                                           f'is mutually exclusive with {str(mutex_opt)}.')
                else:
                    self.required = None
        return super(Mutex, self).handle_parse_result(ctx, opts, args)

result with this

image

any way to keep the spacing using click.style or you can subject any other way to do it?

@dwreeves
Copy link
Collaborator

dwreeves commented Apr 28, 2024

Hi @OriBenHur-akeyless,

That's an interesting error!

If I had to guess what's happening:

  • rich is counting the number of characters to set the border for the panel.
  • click.style() adds ANSI escape codes to color the text.
  • rich includes the ANSI escape codes as part of the count.
  • The ANSI escape code characters do not display in your terminal, but they are being counted when padding the borders, which causes the spacing issue you see here.

So part of the issue here is, I think this is actually a bug in Rich, not a bug with rich-click.

I would check the Rich open issues and see if this is a known issue.

I understand that, in this specific case, it's unfortunate that Rich may have this bug because click.style() is technically part of the rich-click API. So you'd expect them to kind of work together. I totally understand the motivation for wanting this to be fixed.

I can do a little bit of sleuthing as well, and if there is a quick fix then I can put it in as a patch update, but I will say that I'm not totally prioritizing this, because... (read the next section of this post 👀)


Fear not though, there is a workaround!

image
import rich_click as click

class OriginalMutexOption(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if: list = kwargs.pop("not_required_if")

        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs["help"] = f'''{kwargs.get("help", "")} {click.style("NOTE:", bold=True, fg="magenta")} {click.style(f'Mutually exclusive with: {", ".join(self.not_required_if) if len(self.not_required_if) < 2 else ", ".join(self.not_required_if[:-1]) + " and " + self.not_required_if[-1]}', fg='magenta')}'''.strip()
        super(OriginalMutexOption, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        current_opt: bool = self.name in opts
        for mutex_opt in self.not_required_if:
            if mutex_opt in opts:
                if current_opt:
                    raise click.UsageError(f'Illegal usage: {str(self.name)} '
                                           f'is mutually exclusive with {str(mutex_opt)}.')
                else:
                    self.required = None
        return super(OriginalMutexOption, self).handle_parse_result(ctx, opts, args)


class UpdatedMutexOption(click.Option):
    def __init__(self, *args, **kwargs):
        self.not_required_if: list = kwargs.pop("not_required_if")

        assert self.not_required_if, "'not_required_if' parameter required"
        kwargs["help"] = f'''{kwargs.get("help", "")} [magenta][bold]NOTE:[/bold] Mutually exclusive with: {", ".join(self.not_required_if) if len(self.not_required_if) < 2 else ", ".join(self.not_required_if[:-1]) + " and " + self.not_required_if[-1]}[/magenta]'''.strip()
        super(UpdatedMutexOption, self).__init__(*args, **kwargs)

    def handle_parse_result(self, ctx, opts, args):
        current_opt: bool = self.name in opts
        for mutex_opt in self.not_required_if:
            if mutex_opt in opts:
                if current_opt:
                    raise click.UsageError(f'Illegal usage: {str(self.name)} '
                                           f'is mutually exclusive with {str(mutex_opt)}.')
                else:
                    self.required = None
        return super(UpdatedMutexOption, self).handle_parse_result(ctx, opts, args)

@click.command
@click.option("--foo", cls=OriginalMutexOption, help="foo option", not_required_if=["a"])
@click.option("--bar", cls=UpdatedMutexOption, help="bar option", not_required_if=["a"])
@click.rich_config(help_config={"use_rich_markup": True})
def cli(foo):
    """my app"""

if __name__ == "__main__":
    cli()

TLDR of the differences:

  • Add use_rich_markup=True to the CLI's config.
    • (NOTE: for rich-click 1.8, which releases in 2 days, we are encouraging users to use text_markup="rich" instead, but use_rich_markup will be supported indefinitely into the future.)
  • Use [magenta] and [bold] instead of click.style

@dwreeves dwreeves added the bug Something isn't working label Apr 28, 2024
@dwreeves
Copy link
Collaborator

Also, if you don't mind, even if the above fixes your issue, I'd still like to keep this one open. I think it's a genuine problem, even if there's an idiomatic workaround.

@OriBenHur-akeyless
Copy link
Author

it's not working I'm afraid, it prints the control signs
image

@dwreeves
Copy link
Collaborator

Did you include use_rich_markup=True in the config as well? That part is necessary, otherwise it will just render as plain text.

@OriBenHur-akeyless
Copy link
Author

it's working

@dwreeves
Copy link
Collaborator

@OriBenHur-akeyless You can see in the above example how to make use of it.

Basically, add @click.rich_config(help_config={"use_rich_markup": True}) as a decorator for your command.

@click.command()
@click.rich_config(help_config={"use_rich_markup": True})
def cli():
    ...
    # code goes here

@dwreeves
Copy link
Collaborator

Yay, I'm happy to hear it's working! 😄

Again, let's keep this issue open, if you don't mind! This is a genuinely strange behavior, and it's intuitive that you believed it would work without causing an error. Ideally your code would have worked just fine.

@dwreeves
Copy link
Collaborator

Fixed via #193. Thanks for flagging this issue; going forward in 1.8, click.style() will work 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants