Repository: streamlit/streamlit

Issue:
`experimental_memo` fails to hash input arguments that are class instances
### Summary

The `experimental_memo` decorator raises `UnhashableParamError` for functions with arguments that are instances of a class or dataclass, even though the hashing code appears to try to handle it using the `__reduce__` method. This limits the usefulness of `experimental_memo`.

### Steps to reproduce

```python
import streamlit as st

class Foo:
    def __init__(self, a: int):
        self.a  = a

@st.experimental_memo
def my_func(foo: Foo) -> int:
    return foo.a

st.write(my_func(Foo(a=5)))
```

**Expected behavior:**

I would expect the argument to be hashed properly using its pickled string or data tuple. Based on the code block here: https://github.com/streamlit/streamlit/blob/d44b162909fb8adcae463172c78000029e5d2fef/lib/streamlit/caching/hashing.py#L374-L382

It looks to me like we attempt to call `to_bytes` on the return values of `obj.__reduce__()`. However, the [first return value](https://docs.python.org/3/library/pickle.html#object.__reduce__) is the callable object to reconstruct the instance. This is a function, and it fails to make it through `to_bytes`. I believe it's unnecessary to hash this function, and instead we can either hash the subsequent arguments to `__reduce__` (classes, data state), or hash the result of `pickle.dumps(obj)` directly.

As is, we would continue to use `st.cache` instead of `st.experimental_memo` because of this limitation. Notably `experimental_memo` can already output class types, just not input arguments.

**Actual behavior:**

`UnhashableParamError: Cannot hash argument 'foo' (of type __main__.Foo) in 'my_func'.`

### Is this a regression?

No, but the `cache` decorator does work for this case.

### Debug info

- Streamlit version: 1.5.1
- Python version: 3.8.11

### Related links
https://github.com/streamlit/streamlit/issues/661
https://github.com/streamlit/streamlit/pull/3791


Discussion:
Also related: #3944 
Closing this in favor of #3944 as they seem to have the same root cause and the other ticket is older
This issue was closed in favor of #3944 as it was suspected as having the same root cause. However, #3944 was fixed by directly detecting if objects to be memoized are instances of dataclasses, rather than addressing the bug in the "last resort" default case as @hayk-skydio has mentioned here. I just reproduced the bug by running the snippet in the original post on the following environment:
**Streamlit: streamlit-nightly 1.10.1.dev20220627**
**Python: 3.8.10**

Attaching a breakpoint to `hashing.py` to where the UnhashableTypeError is raised confirms @hayk-skydio 's analysis: The error is raised when obj is of `function` type, and therefore calling `to_bytes()` on the entire return value of `obj.__reduce__()` causes the error `TypeError: cannot pickle 'function' object`


Resolving patch:
diff --git a/lib/streamlit/runtime/caching/hashing.py b/lib/streamlit/runtime/caching/hashing.py
--- a/lib/streamlit/runtime/caching/hashing.py
+++ b/lib/streamlit/runtime/caching/hashing.py
@@ -15,6 +15,7 @@
 """Hashing for st.memo and st.singleton."""
 import collections
 import dataclasses
+from enum import Enum
 import functools
 import hashlib
 import inspect
@@ -265,6 +266,9 @@ def _to_bytes(self, obj: Any) -> bytes:
         elif dataclasses.is_dataclass(obj):
             return self.to_bytes(dataclasses.asdict(obj))
 
+        elif isinstance(obj, Enum):
+            return (str(obj._value_) + str(obj._name_)).encode()
+
         elif type_util.is_type(obj, "pandas.core.frame.DataFrame") or type_util.is_type(
             obj, "pandas.core.series.Series"
         ):


Test patch:
diff --git a/lib/tests/streamlit/runtime/caching/hashing_test.py b/lib/tests/streamlit/runtime/caching/hashing_test.py
--- a/lib/tests/streamlit/runtime/caching/hashing_test.py
+++ b/lib/tests/streamlit/runtime/caching/hashing_test.py
@@ -14,6 +14,7 @@
 
 """st.memo/singleton hashing tests."""
 
+from enum import Enum, auto
 import functools
 import hashlib
 import os
@@ -281,6 +282,15 @@ class Data:
 
         assert get_hash(bar)
 
+    def test_enum(self):
+        class EnumClass(Enum):
+            ENUM_1 = auto()
+            ENUM_2 = auto()
+
+        enum_1 = EnumClass.ENUM_1
+
+        assert get_hash(enum_1)
+
 
 class NotHashableTest(unittest.TestCase):
     """Tests for various unhashable types. Many of these types *are*