Repository: streamlit/streamlit

Issue:
StrEnum doesn't work with st.selectbox, st.multiselect options
### Checklist

- [X] I have searched the [existing issues](https://github.com/streamlit/streamlit/issues) for similar issues.
- [X] I added a very descriptive title to this issue.
- [X] I have provided sufficient information below to help reproduce this issue.

### Summary

When plain Enum class is used to define options, everything works fine. As soon as we switch from Enum to StrEnum then it throws key error.

### Reproducible Code Example

```Python
import streamlit as st
from enum import Enum

class ModelStr(str, Enum):
  ZEPHYR = "hf/zephyr"
  MISTRAL_7b = "hf/mistral-7b"

class Model(Enum):
  ZEPHYR = "hf/zephyr"
  MISTRAL_7b = "hf/mistral-7b"

llm = st.selectbox(label="chose your llm", options=ModelStr) # this will throw error
llm = st.selectbox(label="chose your llm", options=Model) # this works
```


### Steps To Reproduce

As soon we go with str enum, key error will be thrown as soon as the code runs.


### Expected Behavior

It should work with String enums also

### Current Behavior

Error Message ```KeyError: 0```
![image](https://github.com/streamlit/streamlit/assets/21143936/e5f14d10-32fc-4bba-a37a-5686a6c4829c)


### Is this a regression?

- [ ] Yes, this used to work in a previous version.

### Debug info

- Streamlit version: 1.33.0
- Python version: 3.11
- Operating System: Debian GNU/Linux 12 (bookworm)
- Browser: Safari/Chrome


### Additional Information

I am building on docker, with python:3.11-slim


Discussion:
**If this issue affects you, please react with a 👍 (thumbs up emoji) to the initial post.**

Your feedback helps us prioritize which bugs to investigate and address first.

![Visits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2Fstreamlit%2Fstreamlit%2Fissues%2F8500&title=visits&edge_flat=false)

Hmmm this could be an issue with EnumCoercion not recognizing StrEnum. As a workaround please follow the guidelines here and move your enums into a separate module: https://docs.streamlit.io/develop/concepts/design/custom-classes
Actually I take that back, doesn't even appear to be reaching the enum coercion spot in the code. What happens if you try `list(ModelStr)` as your options value instead?
This works, am curious as in Why vanilla Enum worked without list? 🤔 
> This works, am curious as in Why vanilla Enum worked without list? 🤔

Great question. I suspect it's because the line is returning the enum itself, incorrectly assuming the enum is position-indexable when it is in fact not (the class inherits from "str" which has an "index" method). Turning it into a list first makes it position-indexable
https://github.com/streamlit/streamlit/blob/e3a1588a560ce8061a0e0bebdd8cb183c462470d/lib/streamlit/elements/widgets/selectbox.py#L237

I would classify this as a bug with "ensure_indexible". Thanks for reporting!
Yes, I would like to contribute and send a patch PR!
Hey @97k were you still interested in contributing a patch for this? 
Yes! I am interested @Asaurus1 . This is now on my top priority list.
I will send a patch soon.
> > This works, am curious as in Why vanilla Enum worked without list? 🤔
> 
> Great question. I suspect it's because the line is returning the enum itself, incorrectly assuming the enum is position-indexable when it is in fact not (the class inherits from "str" which has an "index" method). Turning it into a list first makes it position-indexable
> 
> https://github.com/streamlit/streamlit/blob/e3a1588a560ce8061a0e0bebdd8cb183c462470d/lib/streamlit/elements/widgets/selectbox.py#L237
> 
> I would classify this as a bug with "ensure_indexible". Thanks for reporting!

@Asaurus1 
This is exactly what is happening, the issue is with 'index' method in str and because of this, it is stepping inside the if block and index of string is returned.

A hardcoded, type check on Enum would fix it

`if callable(index_fn) and type(it) != EnumType`

this block of code will fix issues with Enum and will not break anything else.

I will create a PR, we can have discussions on it if you want.


Resolving patch:
diff --git a/lib/streamlit/type_util.py b/lib/streamlit/type_util.py
--- a/lib/streamlit/type_util.py
+++ b/lib/streamlit/type_util.py
@@ -741,7 +741,7 @@ def ensure_indexable(obj: OptionSequence[V_co]) -> Sequence[V_co]:
     # This is an imperfect check because there is no guarantee that an `index`
     # function actually does the thing we want.
     index_fn = getattr(it, "index", None)
-    if callable(index_fn):
+    if callable(index_fn) and type(it) != EnumMeta:
         # We return a shallow copy of the Sequence here because the return value of
         # this function is saved in a widget serde class instance to be used in later
         # script runs, and we don't want mutations to the options object passed to a


Test patch:
diff --git a/lib/tests/streamlit/type_util_test.py b/lib/tests/streamlit/type_util_test.py
--- a/lib/tests/streamlit/type_util_test.py
+++ b/lib/tests/streamlit/type_util_test.py
@@ -721,6 +721,23 @@ def test_ensure_indexable_object_not_indexable(self):
         self.assertIn("b", l)
         self.assertIn("c", l)
 
+    def test_ensure_indexable_enum_is_indexable(self):
+        """Test Enums are indexable"""
+
+        class Opt(enum.Enum):
+            OPT1 = 1
+            OPT2 = 2
+
+        class StrOpt(str, enum.Enum):
+            OPT1 = "a"
+            OPT2 = "b"
+
+        l = type_util.ensure_indexable(Opt)
+        self.assertEqual(list(Opt), l)
+
+        l = type_util.ensure_indexable(StrOpt)
+        self.assertEqual(list(StrOpt), l)
+
 
 class TestArrowTruncation(DeltaGeneratorTestCase):
     """Test class for the automatic arrow truncation feature."""