Repository: streamlit/streamlit

Issue:
[v1.21.0] "TypeError: Unicode-objects must be encoded before hashing" in `util.calc_md5` due to inadequate type check
### 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

After upgrading to version 1.21.0, [`util.calc_md5`](https://github.com/streamlit/streamlit/blob/316cef3426c4f3c81e99414446b0e9a131dbdb57/lib/streamlit/util.py#L180) throws an error when loading a page: `TypeError: Unicode-objects must be encoded before hashing`.

This is due to the check `if type(s) is str` in `b = cast(bytes, s.encode("utf-8") if type(s) is str else s)` not being broad enough to catch all `str`-like objects.

### Reproducible Code Example

```Python
from streamlit import util

class MyString(str):
    pass

s = MyString("foobar")

util.calc_md5(s)
```


### Steps To Reproduce

Run the provided code example.

### Expected Behavior

No error.

### Current Behavior

TypeError: Unicode-objects must be encoded before hashing

### Is this a regression?

- [X] Yes, this used to work in a previous version.

### Debug info

- Streamlit version: 1.21.0
- Python version: 3.8.14
- Operating System: Ubuntu 18.04.6
- Browser: Chrome Version 107.0.5304.87 (Official Build) (64-bit)
- Virtual environment:


### Additional Information

In my case, the [`main_script_path_str` passed to `get_pages` here](https://github.com/streamlit/streamlit/blob/316cef3426c4f3c81e99414446b0e9a131dbdb57/lib/streamlit/source_util.py#L106) ends up being a subclass of `str`.

A good enough fix for my specific case is using `isinstance` instead of `type`:

```
b = cast(bytes, s.encode("utf-8") if isinstance(s, str) else s)
```

Or something like this could maybe catch more potential issues:

```
b = cast(bytes, str(s).encode("utf-8") if type(s) is not bytes else s)
```

### Are you willing to submit a PR?

- [X] Yes, I am willing to submit a PR!


Discussion:
Hi @rachel-skydio,

Thanks for reporting this! Are you using the `calc_md5` in your own code? Note that we consider this function an internal one so don't provide any guarantees around their stability.

In this case, though, the fix is small enough that we'd be okay with accepting a PR to use `isinstance` instead of `type`.
> Are you using the `calc_md5` in your own code? Note that we consider this function an internal one so don't provide any guarantees around their stability.

No, I'm not using `calc_md5` directly. The maybe unexpected behavior is due to my not running streamlit on the command line with `streamlit run your_script.py`. Instead, I run it through a wrapper that modifies `sys.argv` and then calls `streamlit.web.cli.main()`, which leads to `streamlit.web.cli.main_run` being called with a `target` that's a subclass of `str`.

Here's the trace:
```
Traceback (most recent call last):
  File "[snip]/site-packages/tornado/http1connection.py", line 276, in _read_message
    delegate.finish()
  File "[snip]/site-packages/tornado/routing.py", line 268, in finish
    self.delegate.finish()
  File "[snip]/site-packages/tornado/web.py", line 2322, in finish
    self.execute()
  File "[snip]/site-packages/tornado/web.py", line 2344, in execute
    self.handler = self.handler_class(
  File "[snip]/site-packages/tornado/web.py", line 239, in __init__
    self.initialize(**kwargs)  # type: ignore
  File "[snip]/site-packages/streamlit/web/server/routes.py", line 42, in initialize
    self._pages = get_pages()
  File "[snip]/site-packages/streamlit/web/server/server.py", line 369, in <lambda>
    for page_info in source_util.get_pages(
  File "[snip]/site-packages/streamlit/source_util.py", line 122, in get_pages
    main_page_script_hash = calc_md5(main_script_path_str)
  File "[snip]/site-packages/streamlit/util.py", line 180, in calc_md5
    h.update(b)
TypeError: Unicode-objects must be encoded before hashing
```

> In this case, though, the fix is small enough that we'd be okay with accepting a PR to use `isinstance` instead of `type`.

Great, I'll submit a PR. Thanks for the fast response!

Resolving patch:
diff --git a/lib/streamlit/util.py b/lib/streamlit/util.py
--- a/lib/streamlit/util.py
+++ b/lib/streamlit/util.py
@@ -21,7 +21,7 @@
 import hashlib
 import os
 import subprocess
-from typing import Any, Dict, Iterable, List, Mapping, Set, TypeVar, Union, cast
+from typing import Any, Dict, Iterable, List, Mapping, Set, TypeVar, Union
 
 from typing_extensions import Final
 
@@ -173,9 +173,7 @@ def calc_md5(s: Union[bytes, str]) -> str:
     """Return the md5 hash of the given string."""
     h = hashlib.new("md5")
 
-    # mypy seems to have trouble inferring that the type of the if/else expression is
-    # always bytes.
-    b = cast(bytes, s.encode("utf-8") if type(s) is str else s)
+    b = s.encode("utf-8") if isinstance(s, str) else s
 
     h.update(b)
     return h.hexdigest()


Test patch: