Deltares / Deltares/imod-python

"User friendly" enums

Open
#1,181 5 comments 0 reactions 1 assignee View on GitHub

@Manangka is already working on this.

Since Sep 3, 2024.

Dominant language
Python
Stars
41
Forks
12
Avg merge
21h 8m
Merged PRs (30d)
1

Description

Basically a follow up of #416:

We've briefly discussed this before: Enums are a great way to enumerate options, but a lot of our users aren't familiar with them. The issue with enums is also that you need to import the relevant enums from the right namespace. A pragmatic solution is to dynamically force inputs to enums, thereby checking them as well.

This gives decent errors:

from enum import Enum


class Color(Enum):
    RED = 0
    GREEN = 1
    BLUE = 2

color = Color("YELLOW")

ValueError: 'YELLOW' is not a valid Color

Ideally, it wouldn't tell you that it's wrong, but what the right entries are. This is especially helpful with typos.

from enum import Enum
from typing import Union, Type, TypeVar


E = TypeVar('E', bound='FlexibleEnum')


def _show_options(options: Enum) -> str:
    return "\n * ".join(map(str, options.__members__))


class FlexibleEnum(Enum):
    @classmethod
    def from_value(cls: Type[E], value: Union[E, str]) -> E:
        if isinstance(value, cls):
            return value
        elif isinstance(value, str):
            try:
                return cls.__members__[value]
            except KeyError:
                pass

        raise ValueError(
            # Use __repr__() so strings are shown with quotes.
            f"{value.__repr__()} is not a valid {cls.__name__}. "
            f"Valid options are:\n * {_show_options(cls)}"
        )
        

class Color(FlexibleEnum):
    RED = 1
    GREEN = 2
    BLUE = 3
    

Color.from_value("YELLOW")
    

Color.from_value("YELLOW")
ValueError: 'YELLOW' is not a valid Color. Valid options are:
 * RED
 * GREEN
 * BLUE

Another advantage is that regular enums accept integer values.

E.g. one of the current enums:

class ALLOCATION_OPTION(Enum):
    stage_to_riv_bot = 0
    first_active_to_elevation = -1
    stage_to_riv_bot_drn_above = 1
    at_elevation = 2
    at_first_active = 9  # Not an iMOD 5.6 option

In general, I don't think we want user facing functions to support something like .allocate(option=0). A default Enum will support ALLOCATION_OPTION(0).

But with the FlexibleEnum.from_value(), it won't (which is good):

Color.from_value(1)

ValueError: 1 is not a valid Color. Valid options are:
 * RED
 * GREEN
 * BLUE

So my suggestion is to replace the Enums with these FlexibleEnums (or a better name), and preferable the same for all strings literals that we support. Then inside of the function:

def f(arg, option: str | ThisOptionEnum):
     option = ThisOptionEnum.from_value(option)
     ...

This ensures the option is validated and that a clear error message listing the available valid options is printed.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.