1

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):
  ...
0

3 Answers 3

2

I got so frustrated with the complexity involved in managing kwargs, I created dataclass-click as a bolt-on to Click that uses PEP 593 Anotated. Just:

pip install dataclass-click
from dataclasses import dataclass
from typing import Annotated

import click
from dataclass_click import dataclass_click, option


@dataclass
class OptionGroup1:
    unique_flag_1: Annotated[bool, option("--unique_flag-1", is_flag=True)]
    bar: Annotated[bool, option("--bar", is_flag=True)]
    foo: Annotated[bool, option("--foo", is_flag=True, default=True)]


@dataclass
class OptionGroup2:
    # Inferences are cool
    # @click.option("--count", type=click.INT, required=False)
    count: Annotated[int | None, option()]
    # @click.option("--size", type=click.INT, required=True)
    size: Annotated[int, option()]


@click.command()
@dataclass_click(OptionGroup1, kw_name="o1")
@dataclass_click(OptionGroup2, kw_name="o2")
def main(o1: OptionGroup1, o2: OptionGroup2):
    print("group1:", o1)
    print("group2:", o2)


if __name__ == "__main__":
    main()

This also supports inheritence. Whether you use two dataclasses or one inheriting from another would depend on the structure of your code.

Sign up to request clarification or add additional context in comments.

Comments

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)

3 Comments

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?
I've updated the answer to handle the situation you're asking about (if I understood the comment correctly).
You missed that count and size are not required and have no default. Your dataclass will recieve None unexpectedly ;-)
1

I was not satisfied with how dataclass-click handles the arguments using only annotations in the PEP style and I also needed to support older python 3.7 that I still have to use at work. So I created my own package clickdc that uses dataclass.field(metadata= to transfer the information about the field and parses it to apply the click.* arguments and extract the fields to construct a dataclass. The usage looks like the following:

from dataclasses import dataclass
import clickdc
import click

@dataclass
class CommonOptions:
   unique_flag_1: bool = clickdc.option()
   bar: bool = clickdc.option()
   foo: bool = clickdc.option()

@click.command()
@clickdc("common_options", CommonOptions)
def cli(common_options: CommonOptions):
   print(common_options)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.