-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Python Enum support for click.Choice #605
Comments
What exactly is implied with supporting an enum? |
To be able to use 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) |
|
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
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 @click.option(
'--enum-val', type=click.Choice(MyEnum.__members__),
callback=lambda c, p, v: getattr(MyEnum, v) if v else None) The callback provides Perhaps this could be wrapped into another decorator, like @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 |
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 ...
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). |
Code provided by skycaptain in pallets/click#605
Code provided by skycaptain in pallets/click#605
A bit old but I thought I add the nice idea of using 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 |
I don't know if somebody posted it here, but that's how I dealt with it:
|
The recipe above no longer works properly with click 8. When the help text is generated, an option type's I looked through the code and found no easy way (for library authors) to fix this. |
This is an ugly hack, for now I am using a slightly modified version of @allanlewis: class MyEnum(Enum):
a = "a"
b = "b"
@click.option("-m", "--method", type=click.Choice(MyEnum.__members__),
callback=lambda c, p, v: getattr(MyEnum, v) if v else None, default="a") |
PropositionLess ugly solution (but not perfect, seems have some problems with typing (especially inside pycharm)): from enum import Enum
import click
MyEnum = Enum("my_enum", ("a", "b"))
@click.option(
"-m", "--method",
type=click.Choice(list(map(lambda x: x.name, MyEnum)), case_sensitive=False),
default="a"
) Remove the (ugly) usage of Documentation |
@yoyonel you could replace the |
This is easy to implement, using custom type import click
from enum import Enum
class Test(Enum):
test = "test"
another_option = "another_option"
class EnumType(click.Choice):
def __init__(self, enum, case_sensitive=False):
self.__enum = enum
super().__init__(choices=[item.value for item in enum], case_sensitive=case_sensitive)
def convert(self, value, param, ctx):
converted_str = super().convert(value, param, ctx)
return self.__enum(converted_str)
@click.option("-m", "--method", type=EnumType(Test), default = "test")
@click.command()
def test(method):
print(type(method))
print(method)
print(method.value)
if __name__ == '__main__':
test() Test: $ python test.py --method wrong
Usage: test.py [OPTIONS]
Try 'test.py --help' for help.
Error: Invalid value for '-m' / '--method': invalid choice: wrong. (choose from test, another_option)
$ python test.py --method test
<enum 'Test'>
Test.test
test |
@yashrathi-git a minor change in the code you provided, from enum import Enum
from click import Choice
class Test(Enum):
test = "test"
another_option = "another_option"
def __str__(self):
return self.value
class EnumType(Choice):
def __init__(self, enum: Enum, case_sensitive=False):
self.__enum = enum
super().__init__(choices=[item.value for item in enum], case_sensitive=case_sensitive)
def convert(self, value, param, ctx):
if value is None or isinstance(value, Enum)::
return value
converted_str = super().convert(value, param, ctx)
return self.__enum(converted_str) But I feel this should be done under the hood instead of importing EnumType. In other words, it would be much more desirable if we could write, @click.option("-m", "--method", type=Test, default = 'test') # or default = Test.test |
I know this thread is pretty old, but I've been using this pattern for import click
import enum
class MyEnum(str, enum.Enum):
A = "a"
B = "b"
C = "c"
@click.command("my-cmd")
@click.argument("which_option", type=click.Choice(MyEnum))
def my_cmd(which_option: MyEnum):
print(which_option)
print(type(which_option))
if __name__ == "__main__":
my_cmd()
|
Hello @jerluc, your pattern works functionally, but it fails type checking |
Does it @rdbisme? I don't see any issues using latest mypy (0.931) on Python 3.7:
|
Well,
Still Gives:
|
I'm at the PyCon sprint now and could work on this |
@dzcode thanks, but there's already an open PR for this. |
Let's try and finally get this in 8.2.0. If anyone has any feedback, I've put up #2796. Looking for feedback as I'm going to continue to look through the other open issues / PRs that are on the list to be included in 8.2.0, see the 8.2.0 Release Plan. It might be a better and it might be worse than #2210, let me know which one it is! |
Regarding the typecheck of @click.argument("which_option", type=click.Choice(MyEnum)) this is what works for me and the typecheck passes @click.argument("which_option", type=click.Choice(list(MyEnum))) |
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 :-)
The text was updated successfully, but these errors were encountered: