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 :-)
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():
...
Most helpful comment
+1
Screw Python 2.