click-contrib/click-option-group

Modularization of OptionGroups from the Group/Command/MultiCommand

jsonvillanueva opened this issue ยท 10 comments

In click it's possible to split the cli entry point function into several modules/files with the following sample:

from .subcommand1 import subcommand1

@click.group('...')
@click.pass_context
def main(ctx):
    """The main entry point for the CLI."""
    pass

main.add_command(subcommand1) # <--- Adding command to Group from separate module

I'm wondering if it's possible/if there are plans to integrate similar modularization. In my specific use case, I'm refactoring from argparse to click but have been struggling to modularize the optgroups into separate files. I'm trying to shoot for something like this:

from .global_option_group import global_options

@click.group()
#...
def subcommand1('...'):

subcommand1.add_optgroup(global_options) 

If I understand correctly, you can make a decorator function and reuse it.

For example:

import click
from click_option_group import optgroup

def global_option_group(command):
    group = optgroup.group('Global group')

    n_option = optgroup.option('-n', '--names')
    f_option = optgroup.option('-f', '--file')

    f_option(command)
    n_option(command)
    group(command)

    return command

@click.command()
@global_option_group
def cli1(names, file):
    pass

@click.command()
@global_option_group
def cli2(names, file):
    pass

Thanks for the very quick response and example! This isn't exactly what I was looking for but it helped me learn how I had been failing earlier in creating a custom decorator for this purpose. I have one long file filled with 200+ lines of decorator/option logic. This will certainly help split the my optgroups into smaller, more easily understood, files :

    n_option = optgroup.option('-n', '--names')
    f_option = optgroup.option('-f', '--file')

    f_option(command)
    n_option(command)

In reality, I was looking for something within this library/repo that might not exist. In the main click package's core.py, Group has an add_command method that allows the addition of commands to the group as described in my inital example. I was thinking click-option-group's might have a method that functions similarly to allow the additions of OptionGroup to the Command/MultiCommand/Group... perhaps a method like add_optgroup.

click-option-group does not extend click.Command class. The package extends only click.Option class and adds OptionGroup classes.

Also you may have seen cloup package. Maybe in your case cloup package will be more suitable.

I've seen janLuke mention it in one of the issues in your README, but haven't seriously looked into it yet. I'll try refactoring an example with cloup ( this line looks promising) to see if it's satisfactory and report back here. In any case, are there plans/any interest in inheriting from/extending the click core classes to add some similar functionality to add_command -- but for optgroups -- outside of the decorators/function declaration?

In any case, are there plans/any interest in inheriting from/extending the click core classes to add some similar functionality to add_command

In Click core we also cannot add options to the command via some command methods. Click design implies using decorators for adding options and arguments. Group/MultiCommand class in Click just makes commands hierarchy and add_command method is useful to develop plugin-based applications.

click-option-group trying to be as close as possible to Click decorator-based design without creating custom Command subclasses. I'm not sure this design will change anytime soon, but I'm not saying this is the best design. cloup was originally designed differently. It uses own custom Command classes and extends Click from this side. This is closer to what you probably want. So I suggested you try cloup and compare with click-option-group and select better solution according to your code. :)

if it's satisfactory and report back here

That would be great.

@jsonvillanueva Cloup doesn't support anything like that for options. Group.add_section is for adding multiple subcommands that share a common (titled) help section. It's not for options. As @espdev explained, Click commands don't have methods for adding parameters; parameters are passed all at once to the constructor.

The solution provided by @espdev is the idiomatic way to share parameters between commands in Click. The equivalent in Cloup would be:

#======================
# shared_option_group.py
from cloup import option, option_group

shared_option_group = option_group(
    "Name of the option group",
    option('-n', '--names'),
    option('-f', '--file'),
)

#======================
# some_command.py
from shared_option_group import shared_option_group

@command()
@shared_option_group
def some_command(**kwargs):
    pass

The method you are asking for has no benefits over decorators in terms of modularity and I can't see any advantage in having it (can you explain?). It has a drawback though; even if it was implementable respecting Click's interface (and it isn't), you would lose the ability of positioning your global/shared option groups with respect to the command-specific option groups: shared option groups would be shown all before (or all after) the command-specific option groups (depending on the implementation).

Notice that if you want to share options with all subcommands of a Group, maybe you should ask yourself if those options don't belong to the Group itself instead. But you need to remember that Group options must be specified before the subcommand in the command line (notice how --global is before subcommand):

group --global 123 subcommand --local 321

My main issue with the code @espdev provided for the Click "idiomatic" method:

def global_option_group(command):
    group = optgroup.group('Global group')

    n_option = optgroup.option('-n', '--names')
    f_option = optgroup.option('-f', '--file') # Needs to create f_option

    f_option(command) # Then maintain proper ordering by supplying function wrappers in reverse order
    n_option(command)
    group(command)

    return command

is that the developer needs to think about both creating the option and then ordering it properly amongst the wrappers. It's not very intuitive for new developers/contributors of a package to add/remove/tinker around. cloup handles both:

shared_option_group = option_group(
    "Name of the option group",
    option('-n', '--names'),
    option('-f', '--file'),
)

Creating the option and the group is done very similarly to providing it directly in the decorator as normal. The developer doesn't have to think about ordering within the shared_option_group example. It's done within the creation of the option_group(...)

I'm not interested in global/shared options with the package I'm transferring to Click (manim) because this package doesn't have shared options in Argparse. I'm mainly interested in the organizational aspect these packages (cloup/click-option-group) provide for options under specific subcommands ("Sections"/"Optgroups"). That said, cloup will do nicely as far as modularization is concerned; however, it does seem very strange that the Click API has no analog for Group.add_command() such as Command.add_option() which can be supplied outside of the function without the use for decorators.

Oh, yeah, you're right; written in that way is not very elegant but one can automate that behavior (as Cloup does) instead of requiring the developer to do that manually every time.

I guess a follow-up question for @espdev is whether click-option-group should be responsible for automating this behavior natively, or not. Being able to supply an arbitrary number of options within optgroup.group(...) would be a neat enhancement allowing for cleaner modularization.

In cloup this is done via: https://github.com/janLuke/cloup/blob/e82c4db493c3a24b6251faaa1b1577f81424857b/cloup/_option_groups.py#L186-L206

whereas click-option-group isn't designed with this in mind: https://github.com/click-contrib/click-option-group/blob/master/click_option_group/_decorators.py#L89-L92

In the PR I've been working on for manim, I'm already depending on click-contrib's click-default-group package. I'm not exactly sure how/if cloup + click could coexist... it may be too late for this release cycle to incorporate cloup logic into manim's design. But I would be interested in seeing click-option-group support a similar syntax for the sake of clean modularization out of the box.

Cloup is a click extension, not a replacement. Cloup uses the same pattern of other click-contrib extensions (mixins). Combining extension is not always as trivial as chaining the mixins, unfortunately...

To answer your question, yes, you can certainly use cloup and click-default-group together. I can't write now but I can do it later if you want...