Skip to content
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 _missing_ handler overwrites values on singleton for forward-compatible enums #5671

Open
karimkhaleel opened this issue Jan 20, 2025 · 0 comments
Labels
language/python product/sdk-generator Fern's SDK Generator that outputs client libraries in 7 languages

Comments

@karimkhaleel
Copy link

Issue Description

The configuration options for pydantic models allow users to set forward_compatible_python_enums as an option which will generate a _missing_ handler in enum definitions that will get called on unrecognized values.

class BasePydanticModelCustomConfig(pydantic.BaseModel):
    ...
    enum_type: EnumTypes = "literals"
    """
    The type of enums to use in the generated models. Options are:
    - 'literals': Use Python Literal types, e.g. `MyEnum = Literal["foo", "bar"]`
    - 'forward_compatible_python_enums': Use Python Enum classes, with an `MyEnum._UNKNOWN member for forward compatibility, `MyEnum._UNKNOWN.value` contains the raw unrecognized value.
    - 'python_enums': Your vanilla Python enum class, with the members defined within your API.
    """

This missing handler returns the default instance of the enum value with a _value_ attribute pointing to the raw, unrecognized original value.

    @classmethod
    def _missing_(cls, value: object) -> Any:
        unknown = cls._UNKNOWN
        unknown._value_ = value
        return unknown

However, the _UNKNOWN instance of the enum is a singleton and the _value_ attribute on _UNKNOWN is overwritten each time an unrecognized value is converted into an enum representation.

This can lead to confusing behaviors like this:

import enum

class Color(enum.Enum):
    RED = "red"
    BLUE = "blue"
    _UNKNOWN = "__COLOR_UNKNOWN__"

    @classmethod
    def _missing_(cls, value: object) -> Any:
        unknown = cls._UNKNOWN
        unknown._value_ = value
        return unknown


colors = ["red", "blue", "green", "yellow", "purple"]
enum_colors: list[Color] = []

for c in colors:
    enum_colors.append(Color(c))

# Expected output:
# red
# blue
# green
# yellow
# purple

# Actual output:
# red
# blue
# purple
# purple
# purple

for c in enum_colors:
    if c == Color._UNKNOWN:
        print(c._value_)
    else:
        print(c.value)

The _missing_ hook should at least alert the user that an unexpected value was passed in.

import warnings

class Color(enum.Enum):
    RED = "red"
    BLUE = "blue"
    _UNKNOWN = "__COLOR_UNKNOWN__"

    @classmethod
    def _missing_(cls, value: object) -> Any:
        warnings.warn(f"Got an unexpected value: {value} for {cls.__name__}")
        unknown = cls._UNKNOWN
        # left for backwards compatibility, but perhaps the warning should state that there is potential for corrupted
        # data being present when checking the value
        unknown._value_ = value 
        return unknown

Additional Context (Optional)

No response

@dannysheridan dannysheridan added language/python product/sdk-generator Fern's SDK Generator that outputs client libraries in 7 languages labels Jan 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
language/python product/sdk-generator Fern's SDK Generator that outputs client libraries in 7 languages
Development

No branches or pull requests

2 participants