Click: Python Enum support for click.Choice

Created on 9 Jul 2016  路  11Comments  路  Source: pallets/click

I can work around it as described here it would be great if Choice supported Python Enum.

Unless of course I have missed something completely fundamental :-)

Most helpful comment

+1

Screw Python 2.

All 11 comments

What exactly is implied with supporting an enum?

To be able to use Enum instead of tuples for Choice values, i.e define it as such

from enum import Enum, unique

@unique
class ConfigFormat(Enum):
    yaml = 0
    json = 1
    plist = 2

and then use it in the decorator as follows

from . import const

@dispatch.command()
@click.option('--output', '-o', type=click.Choice(const.ConfigFormat),
                help='Sets default output format for configuration files')
@pass_project
def init(project, output, force):
    """Initialises a managed schema"""
    click.echo(project.schema_home)

enum is only available in Python 3 though (in Python 2 you have to use an external backport).

+1

Screw Python 2.

"Screw Python 2" is absolutely out of the question. If you want support for it, make a PR

I was being facetious, sorry. It would be a neat feature but it's nothing essential by any means. Loving this tool by the way.

I'm using something like this in my code, where I'm using Python 2.7 with enum34==1.1.6:

@click.option(
    '--enum-val', type=click.Choice(MyEnum.__members__),
    callback=lambda c, p, v: getattr(MyEnum, v) if v else None)

The callback provides enum_val as the actual enumeration instance rather than a string, or None if the option wasn't given.

Perhaps this could be wrapped into another decorator, like click.enum_option:

@click.enum_option('--enum-val', enum=MyEnum)

UPDATE: In review, others thought that callback was rather ugly, which it is, so I've removed it. If it was inside click, it might be OK.

The easiest way I can think of, is to use a custom type (Although I think this would be a nice feature for click):

class EnumType(click.Choice):
    def __init__(self, enum):
        self.__enum = enum
        super().__init__(enum.__members__)

    def convert(self, value, param, ctx):
        return self.__enum[super().convert(value, param, ctx)]

You might overwrite get_metavar to compute the metavar from class name (since the complete choice list might be to long for a cli printout):

...
    def get_metavar(self, param):
        # Gets metavar automatically from enum name
        word = self.__enum.__name__

        # Stolen from jpvanhal/inflection
        word = re.sub(r"([A-Z]+)([A-Z][a-z])", r'\1_\2', word)
        word = re.sub(r"([a-z\d])([A-Z])", r'\1_\2', word)
        word = word.replace("-", "_").lower().split("_")

        if word[-1] == "enum":
            word.pop()

        return ("_".join(word)).upper()
...

Since Enums are sometimes written uppercase, feel free to write a case insensitive version of the above code. E.g. one could start off with

class EnumType(click.Choice):
    def __init__(self, enum, casesensitive=True):
        if isinstance(enum, tuple):
            choices = (_.name for _ in enum)
        elif isinstance(enum, EnumMeta):
            choices = enum.__members__
        else:
            raise TypeError("`enum` must be `tuple` or `Enum`")

        if not casesensitive:
            choices = (_.lower() for _ in choices)

        self.__enum = enum
        self.__casesensitive = casesensitive

        # TODO choices do not have the save order as enum
        super().__init__(list(sorted(set(choices))))

    def convert(self, value, param, ctx):
        if not self.__casesensitive:
            value = value.lower()

        value = super().convert(value, param, ctx)

        if not self.__casesensitive:
            return next(_ for _ in self._EnumType__enum if _.name.lower() ==
                            value.lower())
        else:
            return next(_ for _ in self._EnumType__enum if _.name == value)

    def get_metavar(self, param):
        word = self.__enum.__name__

        # Stolen from jpvanhal/inflection
        word = re.sub(r"([A-Z]+)([A-Z][a-z])", r'\1_\2', word)
        word = re.sub(r"([a-z\d])([A-Z])", r'\1_\2', word)
        word = word.replace("-", "_").lower().split("_")

        if word[-1] == "enum":
            word.pop()

        return ("_".join(word)).upper()

This way you can either add a complete Enum or you can just use some values as a choice (listed as a tuple).

A bit old but I thought I add the nice idea of using str Enums for options (which is also great for old-school str-based settings w.r.t. comparisons).

class ChoiceType(click.Choice):
    def __init__(self, enum):
        super().__init__(map(str, enum))
        self.enum = enum

    def convert(self, value, param, ctx):
        value = super().convert(value, param, ctx)
        return next(v for v in self.enum if str(v) == value)

class Choice(str, Enum):
    def __str__(self):
        return str(self.value)

class MyChoice(Choice):
    OPT_A = 'opt-a'
    OPT_B = 'opt-b'

@click.option('--choice', type=MyChoiceType(MyChoice),
              default=MyChoice.OPT_B)
def func(choice):
    assert choice in ('opt-a', 'opt-b')
    assert choice in (MyChoice.OPT_A, MyChoice.OPT_B)


Here's my take on supporting this:

class EnumChoice(click.Choice):
    def __init__(self, enum, case_sensitive=False, use_value=False):
        self.enum = enum
        self.use_value = use_value
        choices = [str(e.value) if use_value else e.name for e in self.enum]
        super().__init__(choices, case_sensitive)

    def convert(self, value, param, ctx):
        if value in self.enum:
            return value
        result = super().convert(value, param, ctx)
        # Find the original case in the enum
        if not self.case_sensitive and result not in self.choices:
            result = next(c for c in self.choices if result.lower() == c.lower())
        if self.use_value:
            return next(e for e in self.enum if str(e.value) == result)
        return self.enum[result]

Allows using either the names or values of the enum items based on the use_value parameter. Should work whether values are strings or not.

I don't know if somebody posted it here, but that's how I dealt with it:

Language = Enum("Language", "pl en")

@click.option("--language", type=click.Choice(list(map(lambda x: x.name, Language)), case_sensitive=False))
def main():
...
Was this page helpful?
0 / 5 - 0 ratings