Repository: streamlit/streamlit
Issue: `@st.cache_data` cannot handle UUID objects (but `@st.cache` can)

### 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

It looks like the "new" `@st.cache_data` decorator chokes on `uuid.UUID` objects, while the deprecated `@st.cache` seems to handle them just fine; see minimal example.


### Reproducible Code Example

```Python
import streamlit as st
import uuid


@st.cache
def works(some_id: uuid.UUID) -> None:
    print("Works")


@st.cache_data
def doesnt_work(some_id: uuid.UUID) -> None:
    print("Nope")


some_id = uuid.uuid4()

works(some_id)
doesnt_work(some_id)
```


### Steps To Reproduce

_No response_

### Expected Behavior

_No response_

### Current Behavior

The minimal example above produces the following output:

```
2023-04-06 10:46:30.563 WARNING streamlit.runtime.caching.cache_data_api: No runtime found, using MemoryCacheStorageManager
2023-04-06 10:46:30.678 
  Warning: to view this Streamlit app on a browser, run it with the following
  command:

    streamlit run /Users/timothy/Library/Application Support/JetBrains/PyCharmCE2023.1/scratches/scratch_7.py [ARGUMENTS]
2023-04-06 10:46:30.680 `st.cache` is deprecated. Please use one of Streamlit's new caching commands,
`st.cache_data` or `st.cache_resource`.

More information [in our docs](https://docs.streamlit.io/library/advanced-features/caching).
2023-04-06 10:46:30.681 No runtime found, using MemoryCacheStorageManager
Traceback (most recent call last):
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 383, in _to_bytes
    reduce_data = obj.__reduce__()
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/copyreg.py", line 76, in _reduce_ex
    raise TypeError(f"cannot pickle {cls.__name__!r} object")
TypeError: cannot pickle 'function' object

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/cache_utils.py", line 375, in _make_value_key
    update_hash(
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 56, in update_hash
    ch.update(hasher, val)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 209, in update
    b = self.to_bytes(obj)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 191, in to_bytes
    b = b"%s:%s" % (tname, self._to_bytes(obj))
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 239, in _to_bytes
    self.update(h, item)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 209, in update
    b = self.to_bytes(obj)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 191, in to_bytes
    b = b"%s:%s" % (tname, self._to_bytes(obj))
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 388, in _to_bytes
    self.update(h, item)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 209, in update
    b = self.to_bytes(obj)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 191, in to_bytes
    b = b"%s:%s" % (tname, self._to_bytes(obj))
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 385, in _to_bytes
    raise UnhashableTypeError() from ex
streamlit.runtime.caching.cache_errors.UnhashableTypeError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/timothy/Library/Application Support/JetBrains/PyCharmCE2023.1/scratches/scratch_7.py", line 20, in <module>
    doesnt_work(some_id)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/cache_utils.py", line 194, in wrapper
    return cached_func(*args, **kwargs)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/cache_utils.py", line 223, in __call__
    return self._get_or_create_cached_value(args, kwargs)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/cache_utils.py", line 237, in _get_or_create_cached_value
    value_key = _make_value_key(
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/cache_utils.py", line 381, in _make_value_key
    raise UnhashableParamError(cache_type, func, arg_name, arg_value, exc)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/cache_utils.py", line 375, in _make_value_key
    update_hash(
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 56, in update_hash
    ch.update(hasher, val)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 209, in update
    b = self.to_bytes(obj)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 191, in to_bytes
    b = b"%s:%s" % (tname, self._to_bytes(obj))
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 239, in _to_bytes
    self.update(h, item)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 209, in update
    b = self.to_bytes(obj)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 191, in to_bytes
    b = b"%s:%s" % (tname, self._to_bytes(obj))
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 388, in _to_bytes
    self.update(h, item)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 209, in update
    b = self.to_bytes(obj)
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 191, in to_bytes
    b = b"%s:%s" % (tname, self._to_bytes(obj))
  File "/usr/local/anaconda3/envs/demo-project/lib/python3.10/site-packages/streamlit/runtime/caching/hashing.py", line 385, in _to_bytes
    raise UnhashableTypeError() from ex
streamlit.runtime.caching.cache_errors.UnhashableParamError: Cannot hash argument 'some_id' (of type `uuid.UUID`) in 'doesnt_work'.

To address this, you can tell Streamlit not to hash this argument by adding a
leading underscore to the argument's name in the function signature:

\\```
@st.cache_data
def doesnt_work(_some_id, ...):
    ...
\\```
            
Works
```

### Is this a regression?

- [ ] Yes, this used to work in a previous version.

### Debug info

- Streamlit version: 1.20.0
- Python version: 3.10.10
- Operating System: macOS 13.2.1 (22D68)
- Browser: —
- Virtual environment: conda


### Additional Information

The root of the problems seems to be this:
```
Cannot hash argument 'some_id' (of type `uuid.UUID`) in 'doesnt_work'.
```
Which is a little strange because pickling and hashing `UUID` objects does not seem to be a problem in general:
```python
>>> import pickle
>>> import uuid
>>> pickle.dumps(uuid.uuid4())
b'\\x80\\x04\\x950\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\x04uuid\\x94\\x8c\\x04UUID\\x94\\x93\\x94)\\x81\\x94}\\x94\\x8c\\x03int\\x94\\x8a\\x10\\xa8\\x0eL\\x84\\x14\\xd9\\xcf\\x8d\\xae@M\\x9e\\x11\\xb0\\x9cvsb.'
>>> hash(uuid.uuid4())
1532964488252874118
```

A simple workaround for now is to cast `UUID` objects to strings (which are cacheable also with `@st.cache_data`), but of course it would be nicer if that were not necessary :)

### Are you willing to submit a PR?

- [ ] Yes, I am willing to submit a PR!

Changed files:
lib/tests/streamlit/runtime/caching/hashing_test.py
lib/streamlit/runtime/caching/hashing.py

diff --git a/lib/streamlit/runtime/caching/hashing.py b/lib/streamlit/runtime/caching/hashing.py
index 8d53db8a9..8c995675a 100644
--- a/lib/streamlit/runtime/caching/hashing.py
+++ b/lib/streamlit/runtime/caching/hashing.py
@@ -25,6 +25,7 @@ import sys
 import tempfile
 import threading
 import unittest.mock
+import uuid
 import weakref
 from enum import Enum
 from typing import Any, Dict, List, Optional, Pattern
@@ -130,6 +131,7 @@ def _key(obj: Optional[Any]) -> Any:
             or isinstance(obj, float)
             or isinstance(obj, int)
             or isinstance(obj, bool)
+            or isinstance(obj, uuid.UUID)
             or obj is None
         )
 
@@ -233,6 +235,9 @@ class _CacheFuncHasher:
         elif isinstance(obj, int):
             return _int_to_bytes(obj)
 
+        elif isinstance(obj, uuid.UUID):
+            return obj.bytes
+
         elif isinstance(obj, (list, tuple)):
             h = hashlib.new("md5")
             for item in obj:
diff --git a/lib/tests/streamlit/runtime/caching/hashing_test.py b/lib/tests/streamlit/runtime/caching/hashing_test.py
index dcdd09a7c..2e067fa68 100644
--- a/lib/tests/streamlit/runtime/caching/hashing_test.py
+++ b/lib/tests/streamlit/runtime/caching/hashing_test.py
@@ -21,6 +21,7 @@ import re
 import tempfile
 import types
 import unittest
+import uuid
 from dataclasses import dataclass
 from enum import Enum, auto
 from io import BytesIO, StringIO
@@ -74,6 +75,26 @@ class HashTest(unittest.TestCase):
         self.assertNotEqual(get_hash(2**7), get_hash(2**7 - 1))
         self.assertNotEqual(get_hash(2**7), get_hash(2**7 + 1))
 
+    def test_uuid(self):
+        uuid1 = uuid.uuid4()
+        uuid1_copy = uuid.UUID(uuid1.hex)
+        uuid2 = uuid.uuid4()
+
+        # Our hashing functionality should work with UUIDs
+        # regardless of UUID factory function.
+
+        uuid3 = uuid.uuid5(uuid.NAMESPACE_DNS, "streamlit.io")
+        uuid3_copy = uuid.UUID(uuid3.hex)
+        uuid4 = uuid.uuid5(uuid.NAMESPACE_DNS, "snowflake.com")
+
+        self.assertEqual(get_hash(uuid1), get_hash(uuid1_copy))
+        self.assertNotEqual(id(uuid1), id(uuid1_copy))
+        self.assertNotEqual(get_hash(uuid1), get_hash(uuid2))
+
+        self.assertEqual(get_hash(uuid3), get_hash(uuid3_copy))
+        self.assertNotEqual(id(uuid3), id(uuid3_copy))
+        self.assertNotEqual(get_hash(uuid3), get_hash(uuid4))
+
     def test_mocks_do_not_result_in_infinite_recursion(self):
         try:
             get_hash(Mock())