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

Command Aliases? #101

Open
justquick opened this issue Jan 26, 2023 · 4 comments
Open

Command Aliases? #101

justquick opened this issue Jan 26, 2023 · 4 comments
Milestone

Comments

@justquick
Copy link

Would be great to have 1) a new option or 2) an option inside the click.rich_click.COMMAND_GROUPS dict to give commands aliases.

Right now I can use click-aliases with rich-click to do this but requires me to make my own command class:

from click_aliases import ClickAliasedGroup
from rich_click.rich_command import RichCommand

class MyCMD(ClickAliasedGroup, RichCommand):
    pass
    
@click.group('mycli', cls=MyCMD)
...

Problem is that the aliases work but do not show in the docs and breaks the formatting of the individual command help. It's far from ideal but id like the best of both worlds. Would be great and i think reasonably straightforward to incorporate this into rich-click

@ewels
Copy link
Owner

ewels commented Mar 28, 2023

Happy to take a look at a pull-request 👍🏻 🙃

@kdeldycke
Copy link

@justquick You can use Cloup for command aliases, which works great and is actively maintained: https://github.com/janluke/cloup

Now if you want to have these command aliases properly formatted and coloured in your help screen, you can use Click Extra: https://github.com/kdeldycke/click-extra#readme

@dwreeves
Copy link
Collaborator

dwreeves commented Oct 4, 2023

There is a more interesting discussion here to be had eventually about how rich-click should support common click extensions (one I am personally fond of being click-didyoumean).

I think, ideally, you'd want rich-click to have the following two things be true:

  • rich-click is a drop-in replacement for click, that is very faithful to click's behaviors.
  • rich-click supports lots of popular extensions, or at least their functionality, in some way shape or form.

These two objectives can be in conflict, but they don't necessarily need to be.

The most blunt way to do it is something like this:

def command(name=None, cls=RichCommand, **attrs) -> Callable[..., RichCommand]:
    def wrapper(fn):
        # some stuff goes here
        if not issubclass(cls, RichBaseCommand):
            class CustomCommandClass(RichBaseCommand, cls):
                pass
            cls = CustomCommandClass
        # some stuff goes here
    return wrapper

def group(name=None, cls=RichGroup, **attrs) -> Callable[..., RichCommand]:
    def wrapper(fn):
        # some stuff goes here
        if not issubclass(cls, RichBaseCommand):
            class CustomGroupClass(RichBaseCommand, cls):
                pass
            cls = CustomGroupClass
        if issubclass(cls, Group):
            class CustomCommandClass(RichBaseCommand, cls.command_class):
                pass
            cls.command_class = CustomCommandClass
        # some stuff goes here
    return wrapper

This means that a user can do something like this and still get rich formatting out of it:

import rich_click as click
from click_didyoumean import DYMGroup

@click.group(cls=DYMGroup)
def cli():
    ...

@cli.command()
def foo():
    """Run a command."""
    click.echo('foo')


cli()

But, it is a little magical, would require extensive testing, and would require some feedback from users on whether an approach like this would be a good idea. Also, it would not work for all extensions (see below).

Another option is to just bake common click extensions into rich-click directly. This is more work, more stuff to maintain on rich-click's end, we'd need to think about what the API is for enabling extensions, and we'd probably miss a few things, and overall it extends rich-click's scope as a project quite a bit. There is an upside though, which is that chaining together multiple "extensions" becomes a little easier for users, since we would just support everything out of the box and we'd be able to test to make sure the extension behavior all plays nicely with each other.


For example, right now what you would need to do to combine rich-click and click-didyoumean:

import rich_click as click
from click_didyoumean import DYMGroup

class MyGroup(click.RichGroup, DYMGroup):
    command_class = click.RichCommand


@click.group(cls=MyGroup)
def cli():
    ...

@cli.command()
def foo():
    """Run a command."""
    click.echo('foo')


cli()

It works fine?

>> python foo.py fooo
 Usage: foo.py [OPTIONS] COMMAND [ARGS]...         
                                                   
 Try 'foo.py --help' for help.                     
╭─ Error ─────────────────────────────────────────╮
│ No such command 'fooo'.                         │
│                                                 │
│ Did you mean one of these?                      │
│     foo                                         │
╰─────────────────────────────────────────────────╯

But it's also pretty generically formatted, and could be more "rich." By adding click-didyoumean functionality directly into rich-click we get more control over it.

click-aliases is more concerning, because this multiple inheritance approach doesn't even work:

import rich_click as click
from click_aliases import ClickAliasedGroup

class MyGroup(click.RichGroup, ClickAliasedGroup):
    command_class = click.RichCommand

@click.group(cls=MyGroup)
def cli():
    pass

@cli.command(aliases=['bar', 'baz', 'qux'])
def foo():
    """Run a command."""
    click.echo('foo')

cli()

Output:

                                                                
 Usage: foo.py [OPTIONS] COMMAND [ARGS]...                      
                                                                
╭─ Options ────────────────────────────────────────────────────╮
│ --help      Show this message and exit.                      │
╰──────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────╮
│ foo           Run a command.                                 │
╰──────────────────────────────────────────────────────────────╯

Oh no! Even if rich-click does not bake in its own aliases functionality (I'm not against it), we definitely should be trying our best to support it in the context of it being inherited! It is possible that the lack of support for click-aliases when being inherited suggests we are taking some sort of shortcut in rich-click's abstractions, and that there is some other better abstraction available to us that would support click-aliases (and possibly other click extensions as well).


Overall, there is a lot to think about here. click-aliases has a fairly unobtrusive API, in that it is just a single kwarg in @click.command(), so it is relatively easy to support without getting in the way or expanding the scope of rich-click too much. So I'm not against any PRs to add this feature. I just think there is a more interesting discussion lurking behind this request of how we can better integrate into the broader click ecosystem.

@justquick
Copy link
Author

that's quite a bit of gymnastics to get all the extensions implemented to play nicely with each other. I think the responsibility of managing these things is on click itself to implement something like pluggy instead of having to deal with subclass hackery which might change with any of the associated packages changes to their APIs.

adamghill added a commit to adamghill/coltrane that referenced this issue Mar 1, 2024
@dwreeves dwreeves added this to the 1.9 milestone Apr 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants