edornd / argdantic

Typed command line interfaces with argparse and pydantic
MIT License
38 stars 4 forks source link

Make enum handling configurable #43

Open fennerm opened 6 months ago

fennerm commented 6 months ago

I'd be down to submit a PR for this if you're open to it. Pointers on how you'd like the interface to be defined would be helpful though.

Is your feature request related to a problem? Please describe. I'm working on a tool which autogenerates github actions from python scripts. Argdantic uses the enum name to define the allowed values. The problem is that this is inconsistent with pydantic, which uses the enum ~key~ value. This means that pydantic's schema cannot be used to determine the allowed values for an argument.

Describe the solution you'd like Add a configuration option to the arg parser which toggles whether the enum key or value is used.

Describe alternatives you've considered Personally I think the enum value would be a better default in general because the enum key is less flexible. E.g the key cannot contain certain characters. But figure this probably isn't worth a breaking change

edornd commented 6 months ago

Hi @fennerm, I'm not against a PR, I'd just like to discuss this further, because honestly I don't understand the issue here 🤔 Using the key in my opinion is arguably more intuitive, since its value could be arbitrarily more complex. As far as I understand, in argdantic enums are used without any particular difference from pydantic: at their core they are basically a fancier dict, so the cli uses the key to retrieve the actual enum item and returns that.

I honestly fail to see the problem, except for the character limit. Also, if I understood correctly, and you'd like to use the values as cli choice, literals should already be a good fit for this, probably.

fennerm commented 6 months ago

Hey @edornd as far as I'm aware pydantic does indeed use the enum value, here's an example:

from enum import Enum
from pydantic import BaseModel

class MyEnum(Enum):
    KEY = "value"

class MyModel(BaseModel):
    foo: MyEnum

# Works
json_data = '{"foo": "value"}'
print(MyModel.model_validate_json(json_data))

# Validation error
json_data = '{"foo": "KEY"}'
print(MyModel.model_validate_json(json_data))

Converting to a typing Literal is not an option in my case because the enum is already used many other places in the codebase.

fennerm commented 6 months ago

Sorry just realized I had a typo in my original message which probably confused things. Fixed now.

edornd commented 2 months ago

I see now, yes pydantic uses the value for the actual content, but so does argdantic to be fair. Here's how it works:

  1. argdantic finds an enum type
  2. argdantic parses it using a custom ChoiceArgument
  3. The argument, when converted into help, uses the enum names (keys) as inputs
  4. The user provides the key it needs: when parsed, that key is used to retrieve the actual enum instance ( see here)
  5. The choice argument basically keeps the original Enum class as lookup dictionary: in case of literals it returns the value itself, otherwise it returns the item of the enum (which contains name and value), but the lookup is done by key, which is the name part.

In practical terms, using your example:

from enum import Enum

from argdantic import ArgParser
from pydantic import BaseModel

class MyEnum(Enum):
    key1 = "value1"
    key2 = "value2"

class MyModel(BaseModel):
    foo: MyEnum

cli = ArgParser()

@cli.command()
def main(cfg: MyModel):
    print(type(cfg.foo))
    print(cfg.foo)
    print(cfg.foo.name)
    print(cfg.foo.value)

In this case the help command will provide you: [foo [key1|key2]](usage: cli.py [-h] --cfg.foo [key1|key2]), but if you provide cli.py --cfg.foo key1 the actual content printed on the parsed model will be:

<enum 'MyEnum'>
MyEnum.key1
key1
value1

The switch is just done internally by the CLI, because it just makes more sense to use the name field of enums as an actual name :) but the returned value, once parsed, is the enum object, which contains the value.

I still don't get what you need to accomplish, so apologies if this is not what you wanted!