0

As discussed one may reuse python click decorators from several scripts easily. However, with growing number of parameters

  • the main function parameter list gets crowded and voids pylint too-many-arguments
  • the code processing these many parameters end up in WET programming
  • if one has several scripts using these parameters, even multiple places of similar code have to be maintained

Hence, is there a way to create a class objects directly in the decorators to group parameters?

so, from a decorator function like this:

def common_options(mydefault=True):
    def inner_func(function):
        function = click.option('--unique-flag-1', is_flag=True)(function)
        function = click.option('--bar', is_flag=True)(function)
        function = click.option('--foo', is_flag=True, default=mydefault)(function)
        return function
    return inner_func

directly emit a class like this:

class CommonOptions:
    def __init__(unique_flag_1, bar, foo):
        self.unique_flag_1 = .... 

could be directly emitted to

@click.command
@common_options()
def main(common_options: CommonOptions):
  ...
stj
  • 9,037
  • 19
  • 33
dothebart
  • 5,972
  • 16
  • 40

1 Answers1

1

You can use **kwargs in a click command, so you could write something like:

import click
from dataclasses import dataclass


@dataclass
class CommonOptions:
    unique_flag_1: bool
    bar: bool
    foo: bool


def common_options(mydefault=True):
    def inner_func(function):
        function = click.option("--unique-flag-1", is_flag=True)(function)
        function = click.option("--bar", is_flag=True)(function)
        function = click.option("--foo", is_flag=True, default=mydefault)(function)
        return function

    return inner_func


@click.command()
@common_options()
def main(**kwargs):
    options = CommonOptions(**kwargs)
    print(options)


if __name__ == "__main__":
    main()

W/r/t to your comment, if we borrow an idea from here we can write such that we can have multiple option groups, but for each group we just pass all of **kwargs and let the receiver sort it out:

import click

from dataclasses import dataclass


class OptionGroup:
    @classmethod
    def from_dict(cls, **options):
        return cls(
            **{k: v for k, v in options.items() if k in cls.__dataclass_fields__}
        )


@dataclass
class OptionGroup1(OptionGroup):
    unique_flag_1: bool
    bar: bool
    foo: bool


@dataclass
class OptionGroup2(OptionGroup):
    count: int
    size: int


def option_group_1(mydefault=True):
    def _(function):
        function = click.option("--unique-flag-1", is_flag=True)(function)
        function = click.option("--bar", is_flag=True)(function)
        function = click.option("--foo", is_flag=True, default=mydefault)(function)
        return function

    return _


def option_group_2():
    def _(function):
        function = click.option("--count", type=int)(function)
        function = click.option("--size", type=int)(function)
        return function

    return _


@click.command()
@option_group_2()
@option_group_1()
def main(**kwargs):
    o1 = OptionGroup1.from_dict(**kwargs)
    o2 = OptionGroup2.from_dict(**kwargs)
    print("group1:", o1)
    print("group2:", o2)


if __name__ == "__main__":
    main()

Some example output:

$ python example.py
group1: OptionGroup1(unique_flag_1=False, bar=False, foo=True)
group2: OptionGroup2(count=None, size=None)
$ python example.py --count=3 --bar
group1: OptionGroup1(unique_flag_1=False, bar=True, foo=True)
group2: OptionGroup2(count=3, size=None)
larsks
  • 277,717
  • 41
  • 399
  • 399
  • Thanks for your sugestion! however, since I've got several param groups and several of these argument containers, it seems I must put kwargs into them, and from there only pick the ones relevant to this very object? or is there a more elegant solution? – dothebart Feb 16 '22 at 09:54
  • 1
    I've updated the answer to handle the situation you're asking about (if I understood the comment correctly). – larsks Feb 16 '22 at 16:54