Skip to content

omnipy.util.literal_enum

CLASS DESCRIPTION
LiteralEnum

Base class for creating enums with defined literal choices.

LiteralEnumMeta

A metaclass for LiteralEnum that contains the logic for iteration.

ATTRIBUTE DESCRIPTION
LiteralEnumInnerTypes

LiteralInnerTypeT

LiteralEnumInnerTypes module-attribute

LiteralEnumInnerTypes = bool | str | int | bytes | None

LiteralInnerTypeT module-attribute

LiteralInnerTypeT = TypeVar('LiteralInnerTypeT', bound=LiteralEnumInnerTypes)

LiteralEnum

Bases: Generic[LiteralInnerTypeT]

Base class for creating enums with defined literal choices.

LiteralEnum supports the main static type checkers (tested with mypy and pyright). Unlike standard Enums, LiteralEnum supports multiple inheritance and the use the enum attribute names and underlying values directly in type hints and function signatures. At the same time, LiteralEnum maintains the main benefits of traditional Enum types: a clearly defined and namespaced set of choices with possibilities for flexible naming and per-item documentation.

Subclasses must define a Literals class attribute that specify the valid choices as a Literal type. Each choice must also be defined as a separate class attribute with a Literal type. The LiteralEnum superclass will ensure that all choices are defined according to these rules.

Example LiteralEnum:

from typing import Literal
from omnipy import LiteralEnum

class ClearBoolChoices(LiteralEnum[bool]):
    Literals = Literal[True, False]

    POSITIVE: Literal[True] = True
    """
    Documentation for the positive choice.
    """

    NEGATIVE: Literal[False] = False
    """
    Documentation for the negative choice.
    """

Specializing the LiteralEnum class with a specific inner type allows for more precise type checking for iteration and other methods. If no specialization is provided, the default inner types are defined as:

LiteralEnumInnerTypes =bool | str | int | bytes | None`

Example usage in a function signature:

def i_need_a_clear_choice(choice: ClearBoolChoices.Literals) -> str:
    match choice:
        case ClearBoolChoices.POSITIVE:
            return 'You chose a positive response'
        case ClearBoolChoices.NEGATIVE:
            return 'You chose a negative response'

All the following calls will work at runtime, but with different static type checking results:

>>> i_need_a_clear_choice(ClearBoolChoices.POSITIVE)  # Will pass static type checking
'You chose a positive response'

>>> i_need_a_clear_choice(False)  # Unlike Enums, passing the value also works
'You chose a negative response'

>>> response = i_need_a_clear_choice('maybe')  # This will fail static type checking
>>> response is None
True

LiteralEnum supports multiple inheritance, allowing you to combine different sets of choices into a single enum, e.g.:

class ClearStrChoices(LiteralEnum[str]):
    Literals = Literal['yes', 'no']

    POSITIVE: Literal['yes'] = 'yes'
    NEGATIVE: Literal['no'] = 'no'

class UnclearStrChoices(LiteralEnum):
    Literals = Literal['maybe', 'possibly']

    MAYBE: Literal['maybe'] = 'maybe'
    POSSIBLY: Literal['possibly'] = 'possibly'

class AllStrChoices(ClearStrChoices, UnclearStrChoices):
    Literals = Literal[ClearStrChoices.Literals, UnclearStrChoices.Literals]

The AllStrChoices enum will have all the choices from both ClearStrChoices and UnclearStrChoices. The Literals type of AllStrChoices will be a union of the literals from both enums, allowing for flexible type checking and usage in function signatures.

When there are many enum values, you can avoid manually specifying all the choices by making use of a type narrowing function. As a bonus, mypy (as of v1.16.1) supports exhaustiveness checking for TypeIs narrowing in the same way as with explicit pattern matching.

Example:

# For Python versions < 3.13
# In any case:
# Otherwise, use the built-in versions:
from typing import get_args, TypeIs

from typing_extensions import TypeIs

def is_unclear_choice(choice: AllStrChoices.Literals) -> TypeIs[UnclearStrChoices.Literals]:
    """
    A type narrowing function that checks if the choice is a member of UnclearStrChoices.
    The `TypeIs` return type narrows the type of choice to `UnclearStrChoices.Literals` if the
    check succeeds. Otherwise, the type of choice is negatively narrowed to the remaining
    choices in `AllStrChoices.Literals`.
    """
    return choice in UnclearStrChoices

# This will produce a static type error on the return value, as we forgot to handle the
# case of AllStrChoices.NEGATIVE
def most_choices_are_ok(choice: AllStrChoices.Literals) -> str:
    match choice:
        case AllStrChoices.POSITIVE:
            return "You chose yes"
        case x if is_unclear_choice(x):
            return f"You chose an unclear option: {x}"

Note that there are still no runtime checks:

>>> most_choices_are_ok('maybe')  # No static type checking errors
'You chose an unclear option: maybe'

>>> response = most_choices_are_ok('whatever')  # Static type checking error here
>>> response is None
True
Note: pyright (as of v1.1.402) does not support TypeIs narrowing of exhaustiveness checks:

https://github.com/microsoft/pyright/issues/10680

In contrast to i_need_a_clear_choice(), the most_choices_are_ok() function also checks the provided value at runtime. This is due to the use of the assert_never() function:

For runtime checks, there are several options:

  1. Make use of the new assert_never() function, which also provides exhaustiveness checks when there are no return values:
# For Python versions < 3.11
# Otherwise, use the built-in versions:
from typing import assert_never

from typing_extensions import assert_never

def most_choices_are_still_ok(choice: AllStrChoices.Literals) -> None:
    match choice:
        case AllStrChoices.POSITIVE:
            print("You chose yes")
        case x if is_unclear_choice(x):
            print(f"You chose an unclear option: {x}")
        case _ as never:
            assert_never(never)  # This will raise an error if the case is not handled

The following call will fail both at static type checking and at runtime:

>>> most_choices_are_still_ok('whatever')
(...)
AssertionError: Expected code to be unreachable, but got: 'whatever'
  1. Use e.g. pydantic to validate (and transform) the input:
class MyModel(BaseModel):
    clear: ClearBoolChoices.Literals
    confused: AllStrChoices.Literals

As with i_need_a_clear_choice(), any combination of matching enum attributes and values will work at runtime and for static type checking:

>>> abc = MyModel(clear=True is False, confused=AllStrChoices.MAYBE)
>>> abc
MyModel(clear=False, confused='maybe')

Incorrect values will fail static type checking and (in contrast to i_need_a_clear_choice()) also raise a validation error at runtime:

>>> abc = MyModel(clear=ClearBoolChoices.POSITIVE, confused='whatever')
(…)
confused
  unexpected value; permitted: 'yes', 'no', 'maybe', 'possibly' (…)
METHOD DESCRIPTION
is_random_choice_value

Checks whether the value is valid as random choice for this enum.

name_for_value

Get the name of the enum attribute for the given value.

names

Get an iterator of all attribute names defined in the enum.

random_choice

Select a random choice from all available enum values.

ATTRIBUTE DESCRIPTION
ALLOWED_LITERAL_INNER_TYPES

TYPE: tuple[type, ...]

Literals

A class variable that specifies the valid choices as a Literal type.

TYPE: ClassVar

Source code in src/omnipy/util/literal_enum.py
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
class LiteralEnum(Generic[LiteralInnerTypeT], metaclass=LiteralEnumMeta):
    """Base class for creating enums with defined literal choices.

    LiteralEnum supports the main static type checkers (tested with `mypy`
    and `pyright`). Unlike standard Enums, LiteralEnum supports multiple
    inheritance and the use the enum attribute names and underlying values
    directly in type hints and function signatures. At the same time,
    LiteralEnum maintains the main benefits of traditional Enum types: a
    clearly defined and namespaced set of choices with possibilities for
    flexible naming and per-item documentation.

    Subclasses must define a `Literals` class attribute that specify the
    valid choices as a Literal type. Each choice must also be defined as a
    separate class attribute with a Literal type. The LiteralEnum superclass
    will ensure that all choices are defined according to these rules.

    Example LiteralEnum:

    ```python
    from typing import Literal
    from omnipy import LiteralEnum

    class ClearBoolChoices(LiteralEnum[bool]):
        Literals = Literal[True, False]

        POSITIVE: Literal[True] = True
        \"\"\"
        Documentation for the positive choice.
        \"\"\"

        NEGATIVE: Literal[False] = False
        \"\"\"
        Documentation for the negative choice.
        \"\"\"
    ```

    Specializing the `LiteralEnum` class with a specific inner type allows
    for more precise type checking for iteration and other methods. If no
    specialization is provided, the default inner types are defined as:

    `LiteralEnumInnerTypes = `bool | str | int | bytes | None`

    Example usage in a function signature:

    ```python
    def i_need_a_clear_choice(choice: ClearBoolChoices.Literals) -> str:
        match choice:
            case ClearBoolChoices.POSITIVE:
                return 'You chose a positive response'
            case ClearBoolChoices.NEGATIVE:
                return 'You chose a negative response'
    ```

    All the following calls will work at runtime, but with different static
    type checking results:

    ```pycon
    >>> i_need_a_clear_choice(ClearBoolChoices.POSITIVE)  # Will pass static type checking
    'You chose a positive response'

    >>> i_need_a_clear_choice(False)  # Unlike Enums, passing the value also works
    'You chose a negative response'

    >>> response = i_need_a_clear_choice('maybe')  # This will fail static type checking
    >>> response is None
    True
    ```

    LiteralEnum supports multiple inheritance, allowing you to combine
    different sets of choices into a single enum, e.g.:

    ```python
    class ClearStrChoices(LiteralEnum[str]):
        Literals = Literal['yes', 'no']

        POSITIVE: Literal['yes'] = 'yes'
        NEGATIVE: Literal['no'] = 'no'

    class UnclearStrChoices(LiteralEnum):
        Literals = Literal['maybe', 'possibly']

        MAYBE: Literal['maybe'] = 'maybe'
        POSSIBLY: Literal['possibly'] = 'possibly'

    class AllStrChoices(ClearStrChoices, UnclearStrChoices):
        Literals = Literal[ClearStrChoices.Literals, UnclearStrChoices.Literals]
    ```

    The `AllStrChoices` enum will have all the choices from both
    `ClearStrChoices` and `UnclearStrChoices`. The `Literals` type of
    `AllStrChoices` will be a union of the literals from both enums,
    allowing for flexible type checking and usage in function signatures.

    When there are many enum values, you can avoid manually specifying all
    the choices by making use of a type narrowing function. As a bonus,
    `mypy` (as of v1.16.1) supports exhaustiveness checking for `TypeIs`
    narrowing in the same way as with explicit pattern matching.

    Example:

    ```python
    # For Python versions < 3.13
    # In any case:
    # Otherwise, use the built-in versions:
    from typing import get_args, TypeIs

    from typing_extensions import TypeIs

    def is_unclear_choice(choice: AllStrChoices.Literals) -> TypeIs[UnclearStrChoices.Literals]:
        \"\"\"
        A type narrowing function that checks if the choice is a member of UnclearStrChoices.
        The `TypeIs` return type narrows the type of choice to `UnclearStrChoices.Literals` if the
        check succeeds. Otherwise, the type of choice is negatively narrowed to the remaining
        choices in `AllStrChoices.Literals`.
        \"\"\"
        return choice in UnclearStrChoices

    # This will produce a static type error on the return value, as we forgot to handle the
    # case of AllStrChoices.NEGATIVE
    def most_choices_are_ok(choice: AllStrChoices.Literals) -> str:
        match choice:
            case AllStrChoices.POSITIVE:
                return "You chose yes"
            case x if is_unclear_choice(x):
                return f"You chose an unclear option: {x}"
    ```

    Note that there are still no runtime checks:

    ```pycon
    >>> most_choices_are_ok('maybe')  # No static type checking errors
    'You chose an unclear option: maybe'

    >>> response = most_choices_are_ok('whatever')  # Static type checking error here
    >>> response is None
    True
    ```
    Note: pyright (as of v1.1.402) does not support `TypeIs` narrowing of
    exhaustiveness checks:

        https://github.com/microsoft/pyright/issues/10680

    In contrast to `i_need_a_clear_choice()`, the `most_choices_are_ok()`
    function also checks the provided value at runtime. This is due to the
    use of the `assert_never()` function:

    For runtime checks, there are several options:

    1. Make use of the new `assert_never()` function, which also provides
    exhaustiveness checks when there are no return values:

    ```python
    # For Python versions < 3.11
    # Otherwise, use the built-in versions:
    from typing import assert_never

    from typing_extensions import assert_never

    def most_choices_are_still_ok(choice: AllStrChoices.Literals) -> None:
        match choice:
            case AllStrChoices.POSITIVE:
                print("You chose yes")
            case x if is_unclear_choice(x):
                print(f"You chose an unclear option: {x}")
            case _ as never:
                assert_never(never)  # This will raise an error if the case is not handled
    ```

    The following call will fail both at static type checking and at runtime:

    ```pycon
    >>> most_choices_are_still_ok('whatever')
    (...)
    AssertionError: Expected code to be unreachable, but got: 'whatever'
    ```

    2. Use e.g. pydantic to validate (and transform) the input:

    ```python
    class MyModel(BaseModel):
        clear: ClearBoolChoices.Literals
        confused: AllStrChoices.Literals
    ```

    As with `i_need_a_clear_choice()`, any combination of matching enum attributes and values will
    work at runtime and for static type checking:

    ```pycon
    >>> abc = MyModel(clear=True is False, confused=AllStrChoices.MAYBE)
    >>> abc
    MyModel(clear=False, confused='maybe')
    ```

    Incorrect values will fail static type checking and (in contrast to `i_need_a_clear_choice()`)
    also raise a validation error at runtime:

    ```pycon
    >>> abc = MyModel(clear=ClearBoolChoices.POSITIVE, confused='whatever')
    (…)
    confused
      unexpected value; permitted: 'yes', 'no', 'maybe', 'possibly' (…)
    ```
    """

    ALLOWED_LITERAL_INNER_TYPES: tuple[type, ...] = cast(
        tuple[type, ...],
        all_type_variants(LiteralEnumInnerTypes),
    )

    _RESERVED_PUBLIC_ATTRS = set(('Literals', 'ALLOWED_LITERAL_INNER_TYPES'))
    _RESERVED_PUBLIC_METHODS = set(('names', 'name_for_value'))
    _RESERVED_PUBLIC_NAMES = _RESERVED_PUBLIC_ATTRS | _RESERVED_PUBLIC_METHODS

    Literals: ClassVar
    """A class variable that specifies the valid choices as a Literal type.

    Each choice must also be defined as a separate class attribute with a
    Literal type.
    """
    def __init_subclass__(cls) -> None:
        """
        This method is called when a subclass of LiteralEnum is created. It
        makes sure all values of the `Literals` class variable are also
        defined as members of the subclass. The method also checks that the
        types of the class attributes correctly defined.
        """
        if is_package_editable('omnipy'):
            # Check cls.Literals
            all_cls_literal_vals = cls._check_literals_outer_type()
            cls._check_literals_inner_types_are_allowed(all_cls_literal_vals)

            # Check all defined attributes
            defined_attr_literal_vals = cls._check_attributes(all_cls_literal_vals)

            # Check that all values in cls.Literals are defined as attributes
            cls._check_missing_attributes(all_cls_literal_vals, defined_attr_literal_vals)

    @classmethod
    def _check_literals_outer_type(cls) -> set[LiteralEnumInnerTypes]:
        """
        Checks that the `Literals` class variable is defined as a Literal
        type and returns the set of all possible values defined in it.

        Returns:
            The set of all values defined in `cls.Literals`.
        """
        if not hasattr(cls, 'Literals'):
            raise TypeError(f'{cls.__name__} must define a Literals property.')

        assert is_literal_type(cls.Literals), \
            f'{cls.__name__}.Literals must be defined as a Literal type.'

        return set(get_args(cls.Literals))

    @classmethod
    def _check_literals_inner_types_are_allowed(
            cls, all_cls_literal_vals: set[LiteralEnumInnerTypes]) -> None:
        """
        Checks that all values in cls.Literals are of the allowed inner
        types. For a generic class, the checks are performed against
        LiteralEnumInnerTypes. For a specialized class, the checks are
        performed against the specialized inner types.
        """
        specialized_inner_types: tuple[type, ...] = cls._get_specialized_literal_inner_types()

        if specialized_inner_types:
            literal_inner_types = specialized_inner_types
        else:
            literal_inner_types = cls.ALLOWED_LITERAL_INNER_TYPES

        non_matching_inner_types = []

        for literal_val in all_cls_literal_vals:
            if not isinstance(literal_val, literal_inner_types):
                non_matching_inner_types.append(literal_val)

        if non_matching_inner_types:
            plurality_text = 'value does' if len(non_matching_inner_types) == 1 else 'values do'
            raise TypeError(
                f'{cls.__name__}: Literal {plurality_text} not match the specialization '
                f"({', '.join(_.__name__ for _ in specialized_inner_types)}): "
                + ', '.join(f'{_!r}' for _ in non_matching_inner_types))

    @classmethod
    def _get_specialized_literal_inner_types(cls) -> tuple[type, ...]:
        """
        Extract the specialized inner types from the Generics machinery.
        """
        bases_names = [_.__name__ for _ in cls.__orig_bases__]  # type: ignore[attr-defined]
        try:
            lit_enum_idx = bases_names.index('LiteralEnum')
            return cast(
                tuple[type, ...],
                tuple(
                    _typ for _typ in all_type_variants(
                        get_args(cls.__orig_bases__[lit_enum_idx])[0]  # type: ignore[attr-defined]
                    )))
        except ValueError:
            # Not a specialized LiteralEnum, so return an empty tuple
            return tuple()

    @classmethod
    def _check_attributes(
            cls, all_cls_literal_vals: set[LiteralEnumInnerTypes]) -> set[LiteralEnumInnerTypes]:
        """
        Checks that all public attributes are defined:
        - with an uppercase name
        - annotated as Literal types and with a matching value
        - match the inner types defined in `Literals`

        Returns:
            The set of all correctly defined attribute values
        """

        defined_attr_vals: set[LiteralEnumInnerTypes] = set()

        for member_name in dir(cls):
            member = getattr(cls, member_name)

            if cls._is_public_attr(member_name, member):
                cls._check_uppercase_attr_name(member_name)
                cls._check_attr_annotation_and_value_are_matching_literals(member_name, member)
                cls._check_attr_matches_cls_literals(all_cls_literal_vals, member)

                defined_attr_vals.add(member)

        return defined_attr_vals

    @classmethod
    def _check_uppercase_attr_name(cls, attr):
        assert attr.isupper(), f'{cls.__name__}.{attr} must be an uppercase attribute name.'

    @classmethod
    def _check_attr_annotation_and_value_are_matching_literals(
        cls,
        attr: str,
        value: Any,
    ):
        """
        Checks that the attribute is annotated as a Literal type and that
        the value matches the annotation.
        """
        cls_type_hints: dict[str, TypeForm] = get_type_hints(cls)

        assert attr in cls_type_hints and is_literal_type(cls_type_hints[attr]), \
            f'{cls.__name__}.{attr} must be annotated as a Literal type.'

        annotation_args = get_args(cls_type_hints[attr])
        assert annotation_args == (value,), \
            (f'The value of the Literal annotation must match the assigned value for '
             f'{attr}: {annotation_args} != {(value,)}')

    @classmethod
    def _check_attr_matches_cls_literals(
        cls,
        all_cls_literal_vals: set[LiteralEnumInnerTypes],
        value: Any,
    ):
        """
        Checks that the value of the attribute is one of the defined
        literals in `cls.Literals`.
        """
        if value not in all_cls_literal_vals:
            raise TypeError(f'{value} is not defined in {cls.Literals}.')

    @classmethod
    def _is_public_attr(cls, attr: str, value: Any) -> bool:
        return (
            # Check if the attribute is not private
            not attr.startswith('_')
            # Check if the attribute is not a reserved public attribute
            and attr not in cls._RESERVED_PUBLIC_ATTRS
            # Check if the attribute is not a method
            and not cls._is_method(value))

    @classmethod
    def _is_method(cls, val: Any) -> bool:
        return hasattr(val, '__func__')

    @classmethod
    def _check_missing_attributes(
        cls,
        all_cls_literal_vals: set[LiteralEnumInnerTypes],
        defined_attrs: set[LiteralEnumInnerTypes],
    ):
        """
        Checks that all choices in `cls.Literals` are defined as class
        attributes.
        """
        literal_missing_attrs = all_cls_literal_vals - defined_attrs
        if literal_missing_attrs:
            raise TypeError(f'Not all choices in {cls.__name__}.Literals are defined as members. '
                            f'Missing members: {literal_missing_attrs}')

    @classmethod
    def _get_all_mro_dicts(cls) -> ChainMap[str, Any]:
        return ChainMap(*(dict(base.__dict__)
                          for base in cls.__mro__
                          if base is not LiteralEnum and issubclass(base, LiteralEnum)))

    @classmethod
    def names(cls) -> Iterator[str]:
        """Get an iterator of all attribute names defined in the enum.

        Returns:
            An iterator of attribute names defined in the enum.
        """
        for attr_name in cls._get_all_mro_dicts().keys():
            if cls._is_public_attr(attr_name, getattr(cls, attr_name)):
                yield attr_name

    @classmethod
    def name_for_value(cls: 'type[LiteralEnum[LiteralInnerTypeT]]',
                       value: LiteralInnerTypeT) -> str:
        """Get the name of the enum attribute for the given value.

        Parameters:
            value: The value to look up in the enum

        Returns:
            The name of the enum attribute that corresponds to the value,
            or raise ValueError if the value is not found.
        """
        for attr_name, attr_value in cls._get_all_mro_dicts().items():
            if attr_value == value:
                return attr_name
        raise ValueError(f'Value {value!r} not found in {cls.__name__}')

    @classmethod
    def random_choice(cls) -> LiteralInnerTypeT:
        """Select a random choice from all available enum values.
        """
        exclude_prefixes = [RANDOM_PREFIX, AUTO_VALUE]
        choice = ''
        while choice == '' or any(choice.startswith(_) for _ in exclude_prefixes):
            choice = random.choice(get_args(cls.Literals))

        return cast(LiteralInnerTypeT, choice)

    @classmethod
    def is_random_choice_value(cls, value: object) -> bool:
        """Checks whether the value is valid as random choice for this enum.

        Parameters:
            value: The value to check.

        Returns:
            True if the value is a valid random choice value for this enum,
            False otherwise.
        """
        return (isinstance(value, str) and cast(LiteralInnerTypeT, value) in cls
                and value.startswith(RANDOM_PREFIX))

ALLOWED_LITERAL_INNER_TYPES class-attribute instance-attribute

ALLOWED_LITERAL_INNER_TYPES: tuple[type, ...] = cast(
    tuple[type, ...], all_type_variants(LiteralEnumInnerTypes)
)

Literals instance-attribute

Literals: ClassVar

A class variable that specifies the valid choices as a Literal type.

Each choice must also be defined as a separate class attribute with a Literal type.

is_random_choice_value classmethod

is_random_choice_value(value: object) -> bool

Checks whether the value is valid as random choice for this enum.

PARAMETER DESCRIPTION
value

The value to check.

TYPE: object

RETURNS DESCRIPTION
bool

True if the value is a valid random choice value for this enum, False otherwise.

Source code in src/omnipy/util/literal_enum.py
@classmethod
def is_random_choice_value(cls, value: object) -> bool:
    """Checks whether the value is valid as random choice for this enum.

    Parameters:
        value: The value to check.

    Returns:
        True if the value is a valid random choice value for this enum,
        False otherwise.
    """
    return (isinstance(value, str) and cast(LiteralInnerTypeT, value) in cls
            and value.startswith(RANDOM_PREFIX))

name_for_value classmethod

name_for_value(value: LiteralInnerTypeT) -> str

Get the name of the enum attribute for the given value.

PARAMETER DESCRIPTION
value

The value to look up in the enum

TYPE: LiteralInnerTypeT

RETURNS DESCRIPTION
str

The name of the enum attribute that corresponds to the value, or raise ValueError if the value is not found.

Source code in src/omnipy/util/literal_enum.py
@classmethod
def name_for_value(cls: 'type[LiteralEnum[LiteralInnerTypeT]]',
                   value: LiteralInnerTypeT) -> str:
    """Get the name of the enum attribute for the given value.

    Parameters:
        value: The value to look up in the enum

    Returns:
        The name of the enum attribute that corresponds to the value,
        or raise ValueError if the value is not found.
    """
    for attr_name, attr_value in cls._get_all_mro_dicts().items():
        if attr_value == value:
            return attr_name
    raise ValueError(f'Value {value!r} not found in {cls.__name__}')

names classmethod

names() -> Iterator[str]

Get an iterator of all attribute names defined in the enum.

RETURNS DESCRIPTION
Iterator[str]

An iterator of attribute names defined in the enum.

Source code in src/omnipy/util/literal_enum.py
@classmethod
def names(cls) -> Iterator[str]:
    """Get an iterator of all attribute names defined in the enum.

    Returns:
        An iterator of attribute names defined in the enum.
    """
    for attr_name in cls._get_all_mro_dicts().keys():
        if cls._is_public_attr(attr_name, getattr(cls, attr_name)):
            yield attr_name

random_choice classmethod

random_choice() -> LiteralInnerTypeT

Select a random choice from all available enum values.

Source code in src/omnipy/util/literal_enum.py
@classmethod
def random_choice(cls) -> LiteralInnerTypeT:
    """Select a random choice from all available enum values.
    """
    exclude_prefixes = [RANDOM_PREFIX, AUTO_VALUE]
    choice = ''
    while choice == '' or any(choice.startswith(_) for _ in exclude_prefixes):
        choice = random.choice(get_args(cls.Literals))

    return cast(LiteralInnerTypeT, choice)

LiteralEnumMeta

Bases: type

A metaclass for LiteralEnum that contains the logic for iteration.

Source code in src/omnipy/util/literal_enum.py
class LiteralEnumMeta(type):
    """A metaclass for LiteralEnum that contains the logic for iteration.
    """
    @overload
    def __iter__(  # type: ignore[misc]
        self: 'type[LiteralEnum[LiteralEnumInnerTypes]]'  # Match generic LiteralEnum subtypes
    ) -> Iterator[LiteralEnumInnerTypes]:
        ...

    @overload
    def __iter__(  # type: ignore[misc]
        self: 'type[LiteralEnum[LiteralInnerTypeT]]'  # Match specialized LiteralEnum subtypes
    ) -> Iterator[LiteralInnerTypeT]:
        ...

    def __iter__(self) -> Iterator[LiteralEnumInnerTypes]:
        """Iterate over the enum values.

        Narrows the type according to the specialization to specific Literal
        inner types

        Returns:
            An typed iterator over the enum values.
        """
        return iter(get_args(cast(LiteralEnum, self).Literals))