-
Notifications
You must be signed in to change notification settings - Fork 2
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
Add support for Enums #29 #44
base: main
Are you sure you want to change the base?
Conversation
Thanks for your contribution! I'll look into getting CI to run on pull requests as well. You're the first, so this part of the infrastructure hadn't been tested before. It's good to see elaborate test cases as well. One thing that seems to be missing from the tests though is checking the error handling. For example, does the library produce a useful error message if the TOML value isn't a string, or if it is a string but it doesn't exist as a member of the enum? Should the mapping from TOML string to Python enum member be done case-sensitive or case-insensitive? From your test case, it seems you expect case-insensitive matching, but I don't see anything in the code that implements that. |
Actually, it seems you're trying to match enum members by their value ( I tend to think of the names as the interface of the enum and the value as something that is mainly used for debugging. In that perspective, it would be better to match on the names instead of values. But I don't know whether everyone uses enums in Python like that. When enum.auto() is used, the value does not have any meaning, so we should be matching those on name. However, I don't think there is any way to determine whether Then the question remains whether we also want to support mapping by value. I'm leaning towards "no", to keep things simple. But if there is a compelling use case that can't be handled with mapping by name, I can be persuaded otherwise. |
Thanks for the review! I'll look into the requested changes later or tomorrow!
Mapping by name you mean to always expose the key as a string inside the TOML? One use case I often face and the primary reason why I wanted enum support in the first place is a toml where you have a pre defined set of values (could probably also be solved with typing.Literal) aka # this only accepts pre defined values
# I am only consuming this (or maybe also updating) but this is defined by a different app
state = 3
# same concept
mode = "install" In this case having dataclass binder use values like state = "OFF"
mode = "INSTALL" Is not helping me at least. The structure/values of this toml are pre defined I merely don't want to handle strings or ints on my end. If I am both consumer and producer of the TOML I am also not sure if using the enum key names is useful because if I'd want that I could just name them this way in my enum values IMHO |
Yes. Let's modify the color enum from the unit test to make the name and value more distinct: class Color(Enum):
RED = "#f00"
GREEN = "#0f0"
BLUE = "#00f" In this case, matching by name would mean mapping the TOML string
Adding support for
Ah, this is a use case I hadn't considered: if the TOML format is fixed and uses integers or strings that are not valid Python identifiers, then indeed matching by value is useful. Maybe we could use For example with this code: from dataclasses import dataclass
from enum import Enum, IntEnum, auto
class Verbosity(Enum):
QUIET = auto()
NORMAL = auto()
DETAILED = auto()
class IntVerbosity(IntEnum):
QUIET = 0
NORMAL = 1
DETAILED = 2
@dataclass
class Settings:
verbose_name: Verbosity
verbose_value: IntVerbosity The corresponding TOML would look like this: verbose-name = 'quiet'
verbose-value = 2 Note that
There are scenarios in which name matching is useful:
Since both name matching and value matching are useful, the question becomes how to support both while keeping the behavior of |
That would indeed work for me and I agree having the ability to use keys seems very useful! I'm just not sure yet as to what is more intuitive for the user because I don't think using the specialized sub classes to differentiate between keys/values is something I'd expect, they also have some other implications in their usage that might be unwanted. The values of auto are also highly predictable (counting upwards starting with 1 or in case of string enums just the key in lower case), probably wouldn't want to use this when you actively care about the values (consuming foreign toml). I think we should decide for one of these two cases to be the default (key vs value) and have the other available through either a different base class or a decorator making this choice deliberate. For example if values where default (names are just examples) class Color(Enum, BindEnumKey):
RED = "#f00"
#....
# or
@bind_enum_key(case_sensisitive=True)
class Color(Enum):
RED = "#f00"
#.... I do like being able to configure the behavior via decorators. We could also force users to annotate all enums and reject them if they haven't defined this behavior explicitly @bind_enum(as="key", case_sensisitive=True)
class Color(Enum):
RED = "#f00"
#.... Downside is that this is special behavior for dataclass binder, although some way of annotating this seems sensible 🤔 |
With
Indeed: while the values are predictable, by using
So far I've managed to avoid introducing any new syntax for dataclasses in
In some cases the behavior comes naturally, such as dataclass fields without defaults being mandatory in TOML. However, in the case of selecting name versus value matching for enumerations, I don't think a very intuitive solution exists. I hope that " I'm not certain that using conventions rather than explicit annotations is the right way to go, but it is consistent with the way the library has been designed so far, so I'd like to give it a try. |
I actually very much appreciate this. I pushed the changes, StrEnum, IntEnum and IntFlag all inherit ReprEnum so this seemed like a good candidate Edit: Also added some more tests which test the case insensitivity working and some error cases |
@mthuurne Any more changes you would like to see? |
Sorry for the delay.
I couldn't get CI to work on this PR, but it should now be enabled for future PRs. So I ran the tests locally and got one failing test case and two lines missing test coverage. I'll add comments on the specific lines. |
Sorry for the delay, I was a bit busy. Edit: Fixed all the open changes! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Almost there, but the error handling still needs a bit of work.
if not isinstance(value, str): | ||
raise TypeError(f"'{value}' is not a valid key for enum '{field_type}', must be of type str") | ||
for enum_value in field_type: | ||
if enum_value.name.lower() == value.lower(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small optimization: you could store value.lower()
in a local variable, to avoid converting the same string multiple times.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since there is no other case where this is used in this function I'm not sure this would help 🤔
Hey! Thanks for the review, I have added the requested changes :) |
@mthuurne Is there a timeline when this PR can be merged? As far I can see, there will be no blockers curently? |
I like to use enums in Python so this seemed important to me, I added a few ways to use enums to the tests.
The only thing missing would be being able to use enums as keys for dicts that seemed a lot more complicated so I didn't implement this. Although right now I think only strings and ints can be used for that either way.
I was waiting in line for a coffee for like 30m so I hacked this together on my phone, I hope the tests actually pass 😅