We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
UnhashableTypeError: Cannot hash object of type _json.Scanner...
### Summary

When attempting to use st.cache decorator (with streamlit 1.10.0) on a json data loader method, streamlit returns an unhashable error for a _json.Scanner. This behavior does not exist in the previous version (1.9.2).  

### Steps to reproduce

1. Update streamlit to version 1.10.0
2. Use st.cache on any method that loads data from a json file

For example:

```python
@st.cache(allow_output_mutation=True)
def fetch_data_json(filename: str) -> Dict[str, Any]:
    with open(filename, "r") as f:
        config = json.load(f)
    return config
```

Returns: 

```
UnhashableTypeError: Cannot hash object of type _json.Scanner, found in the body of fetch_data_json().

While caching the body of fetch_data_json(), Streamlit encountered an object of type _json.Scanner, which it does not know how to hash.

To address this, please try helping Streamlit understand how to hash that type by passing the hash_funcs argument into @st.cache. For example:


@st.cache(hash_funcs={_json.Scanner: my_hash_func})
def my_func(...):
  
If you don't know where the object of type _json.Scanner is coming from, try looking at the hash chain below for an object that you do recognize, then pass that to hash_funcs instead:


Object of type _json.Scanner: <_json.Scanner object at 0x7fa22470b160>
Object of type builtins.method: <bound method JSONDecoder.raw_decode of <json.decoder.JSONDecoder object at 0x7fa2247b0760>>
Object of type builtins.method: <bound method JSONDecoder.decode of <json.decoder.JSONDecoder object at 0x7fa2247b0760>>
Object of type builtins.function: <function loads at 0x7fa22470e3a0>
Object of type builtins.function: <function load at 0x7fa22470e310>
Object of type builtins.function: <function fetch_data_json at 0x7fa22128cdc0>
Please see the hash_funcs [documentation] (https://docs.streamlit.io/library/advanced-features/caching#the-hash_funcs-parameter) for more details.

Traceback:
File "app.py", line 23, in <module>
    json_file = fetch_data_json(TEST_CONFIG_FOLDER + "/test_config.json")
File "/usr/lib/python3.8/copyreg.py", line 66, in _reduce_ex
    raise TypeError(f"cannot pickle {cls.__name__!r} object")
```

**Expected behavior:**

In version 1.9.2 the same code would produce valid parsed json data without @st.cache attempting to has an unhashable object. This image below is the method outlined above implemented in 1.10.0 and 1.9.2:

![bug](https://user-images.githubusercontent.com/73495852/174457986-a2912a56-2849-4ee2-a5f7-a00754298898.png)


### Debug info

- Streamlit version: 1.10.0
- Python version: 3.8.12
- Using Conda? PipEnv? PyEnv? Pex? Pip via Poetry
- OS version: Ubuntu 20.04 (via Windows 11)
- Browser version: Chrome 102.0.5005.115




--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `lib/streamlit/source_util.py`
Content:
```
1 # Copyright 2018-2022 Streamlit Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #    http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 import re
16 import threading
17 from pathlib import Path
18 from typing import Any, Callable, cast, Dict, List, Optional, Tuple
19 
20 from blinker import Signal
21 
22 from streamlit.logger import get_logger
23 from streamlit.util import calc_md5
24 
25 LOGGER = get_logger(__name__)
26 
27 
28 def open_python_file(filename):
29     """Open a read-only Python file taking proper care of its encoding.
30 
31     In Python 3, we would like all files to be opened with utf-8 encoding.
32     However, some author like to specify PEP263 headers in their source files
33     with their own encodings. In that case, we should respect the author's
34     encoding.
35     """
36     import tokenize
37 
38     if hasattr(tokenize, "open"):  # Added in Python 3.2
39         # Open file respecting PEP263 encoding. If no encoding header is
40         # found, opens as utf-8.
41         return tokenize.open(filename)
42     else:
43         return open(filename, "r", encoding="utf-8")
44 
45 
46 PAGE_FILENAME_REGEX = re.compile(r"([0-9]*)[_ -]*(.*)\.py")
47 # Regex pattern to extract emoji taken from https://gist.github.com/Alex-Just/e86110836f3f93fe7932290526529cd1#gistcomment-3208085
48 # We may eventually want to swap this out for https://pypi.org/project/emoji,
49 # but I want to avoid adding a dependency if possible.
50 PAGE_ICON_REGEX = re.compile(
51     "(^[\U0001F1E0-\U0001F1FF"
52     "\U0001F300-\U0001F5FF"
53     "\U0001F600-\U0001F64F"
54     "\U0001F680-\U0001F6FF"
55     "\U0001F700-\U0001F77F"
56     "\U0001F780-\U0001F7FF"
57     "\U0001F800-\U0001F8FF"
58     "\U0001F900-\U0001F9FF"
59     "\U0001FA00-\U0001FA6F"
60     "\U0001FA70-\U0001FAFF"
61     "\U00002702-\U000027B0"
62     "\U000024C2-\U0001F251])[_-]*"
63 )
64 
65 
66 def page_sort_key(script_path: Path) -> Tuple[float, str]:
67     matches = re.findall(PAGE_FILENAME_REGEX, script_path.name)
68 
69     # Failing this assert should only be possible if script_path isn't a Python
70     # file, which should never happen.
71     assert len(matches) > 0, f"{script_path} is not a Python file"
72 
73     [(number, label)] = matches
74     label = label.lower()
75 
76     if number == "":
77         return (float("inf"), label)
78 
79     return (float(number), label)
80 
81 
82 def page_name_and_icon(script_path: Path) -> Tuple[str, str]:
83     """Compute the name of a page from its script path.
84 
85     This is *almost* the page name displayed in the nav UI, but it has
86     underscores instead of spaces. The reason we do this is because having
87     spaces in URLs both looks bad and is hard to deal with due to the need to
88     URL-encode them. To solve this, we only swap the underscores for spaces
89     right before we render page names.
90     """
91     extraction = re.search(PAGE_FILENAME_REGEX, script_path.name)
92     if extraction is None:
93         return "", ""
94 
95     # This cast to Any+type annotation weirdness is done because
96     # cast(re.Match[str], ...) explodes at runtime since Python interprets it
97     # as an attempt to index into re.Match instead of as a type annotation.
98     extraction: re.Match[str] = cast(Any, extraction)
99 
100     name = re.sub(r"[_ ]+", "_", extraction.group(2)).strip()
101     if not name:
102         name = extraction.group(1)
103 
104     extracted_icon = re.search(PAGE_ICON_REGEX, name)
105     if extracted_icon is not None:
106         icon = str(extracted_icon.group(1))
107         name = re.sub(PAGE_ICON_REGEX, "", name)
108     else:
109         icon = ""
110 
111     return str(name), icon
112 
113 
114 _pages_cache_lock = threading.RLock()
115 _cached_pages: Optional[Dict[str, Dict[str, str]]] = None
116 _on_pages_changed = Signal(doc="Emitted when the pages directory is changed")
117 
118 
119 def invalidate_pages_cache():
120     global _cached_pages
121 
122     LOGGER.debug("Pages directory changed")
123     with _pages_cache_lock:
124         _cached_pages = None
125 
126     _on_pages_changed.send()
127 
128 
129 def get_pages(main_script_path_str: str) -> Dict[str, Dict[str, str]]:
130     global _cached_pages
131 
132     # Avoid taking the lock if the pages cache hasn't been invalidated.
133     pages = _cached_pages
134     if pages is not None:
135         return pages
136 
137     with _pages_cache_lock:
138         # The cache may have been repopulated while we were waiting to grab
139         # the lock.
140         if _cached_pages is not None:
141             return _cached_pages
142 
143         main_script_path = Path(main_script_path_str)
144         main_page_name, main_page_icon = page_name_and_icon(main_script_path)
145         main_page_script_hash = calc_md5(main_script_path_str)
146 
147         # NOTE: We include the page_script_hash in the dict even though it is
148         #       already used as the key because that occasionally makes things
149         #       easier for us when we need to iterate over pages.
150         pages = {
151             main_page_script_hash: {
152                 "page_script_hash": main_page_script_hash,
153                 "page_name": main_page_name,
154                 "icon": main_page_icon,
155                 "script_path": str(main_script_path),
156             }
157         }
158 
159         pages_dir = main_script_path.parent / "pages"
160         page_scripts = sorted(
161             [f for f in pages_dir.glob("*.py") if not f.name.startswith(".")],
162             key=page_sort_key,
163         )
164 
165         for script_path in page_scripts:
166             script_path_str = str(script_path)
167             pn, pi = page_name_and_icon(script_path)
168             psh = calc_md5(script_path_str)
169 
170             pages[psh] = {
171                 "page_script_hash": psh,
172                 "page_name": pn,
173                 "icon": pi,
174                 "script_path": script_path_str,
175             }
176 
177         _cached_pages = pages
178 
179         return pages
180 
181 
182 def register_pages_changed_callback(
183     callback: Callable[[str], None],
184 ):
185     def disconnect():
186         _on_pages_changed.disconnect(callback)
187 
188     # weak=False so that we have control of when the pages changed
189     # callback is deregistered.
190     _on_pages_changed.connect(callback, weak=False)
191 
192     return disconnect
193
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/lib/streamlit/source_util.py b/lib/streamlit/source_util.py
--- a/lib/streamlit/source_util.py
+++ b/lib/streamlit/source_util.py
@@ -152,7 +152,7 @@
                 "page_script_hash": main_page_script_hash,
                 "page_name": main_page_name,
                 "icon": main_page_icon,
-                "script_path": str(main_script_path),
+                "script_path": str(main_script_path.resolve()),
             }
         }
 
@@ -163,7 +163,7 @@
         )
 
         for script_path in page_scripts:
-            script_path_str = str(script_path)
+            script_path_str = str(script_path.resolve())
             pn, pi = page_name_and_icon(script_path)
             psh = calc_md5(script_path_str)

{"golden_diff": "diff --git a/lib/streamlit/source_util.py b/lib/streamlit/source_util.py\n--- a/lib/streamlit/source_util.py\n+++ b/lib/streamlit/source_util.py\n@@ -152,7 +152,7 @@\n                 \"page_script_hash\": main_page_script_hash,\n                 \"page_name\": main_page_name,\n                 \"icon\": main_page_icon,\n-                \"script_path\": str(main_script_path),\n+                \"script_path\": str(main_script_path.resolve()),\n             }\n         }\n \n@@ -163,7 +163,7 @@\n         )\n \n         for script_path in page_scripts:\n-            script_path_str = str(script_path)\n+            script_path_str = str(script_path.resolve())\n             pn, pi = page_name_and_icon(script_path)\n             psh = calc_md5(script_path_str)\n", "issue": "UnhashableTypeError: Cannot hash object of type _json.Scanner...\n### Summary\r\n\r\nWhen attempting to use st.cache decorator (with streamlit 1.10.0) on a json data loader method, streamlit returns an unhashable error for a _json.Scanner. This behavior does not exist in the previous version (1.9.2).  \r\n\r\n### Steps to reproduce\r\n\r\n1. Update streamlit to version 1.10.0\r\n2. Use st.cache on any method that loads data from a json file\r\n\r\nFor example:\r\n\r\n```python\r\n@st.cache(allow_output_mutation=True)\r\ndef fetch_data_json(filename: str) -> Dict[str, Any]:\r\n    with open(filename, \"r\") as f:\r\n        config = json.load(f)\r\n    return config\r\n```\r\n\r\nReturns: \r\n\r\n```\r\nUnhashableTypeError: Cannot hash object of type _json.Scanner, found in the body of fetch_data_json().\r\n\r\nWhile caching the body of fetch_data_json(), Streamlit encountered an object of type _json.Scanner, which it does not know how to hash.\r\n\r\nTo address this, please try helping Streamlit understand how to hash that type by passing the hash_funcs argument into @st.cache. For example:\r\n\r\n\r\n@st.cache(hash_funcs={_json.Scanner: my_hash_func})\r\ndef my_func(...):\r\n  \r\nIf you don't know where the object of type _json.Scanner is coming from, try looking at the hash chain below for an object that you do recognize, then pass that to hash_funcs instead:\r\n\r\n\r\nObject of type _json.Scanner: <_json.Scanner object at 0x7fa22470b160>\r\nObject of type builtins.method: <bound method JSONDecoder.raw_decode of <json.decoder.JSONDecoder object at 0x7fa2247b0760>>\r\nObject of type builtins.method: <bound method JSONDecoder.decode of <json.decoder.JSONDecoder object at 0x7fa2247b0760>>\r\nObject of type builtins.function: <function loads at 0x7fa22470e3a0>\r\nObject of type builtins.function: <function load at 0x7fa22470e310>\r\nObject of type builtins.function: <function fetch_data_json at 0x7fa22128cdc0>\r\nPlease see the hash_funcs [documentation] (https://docs.streamlit.io/library/advanced-features/caching#the-hash_funcs-parameter) for more details.\r\n\r\nTraceback:\r\nFile \"app.py\", line 23, in <module>\r\n    json_file = fetch_data_json(TEST_CONFIG_FOLDER + \"/test_config.json\")\r\nFile \"/usr/lib/python3.8/copyreg.py\", line 66, in _reduce_ex\r\n    raise TypeError(f\"cannot pickle {cls.__name__!r} object\")\r\n```\r\n\r\n**Expected behavior:**\r\n\r\nIn version 1.9.2 the same code would produce valid parsed json data without @st.cache attempting to has an unhashable object. This image below is the method outlined above implemented in 1.10.0 and 1.9.2:\r\n\r\n![bug](https://user-images.githubusercontent.com/73495852/174457986-a2912a56-2849-4ee2-a5f7-a00754298898.png)\r\n\r\n\r\n### Debug info\r\n\r\n- Streamlit version: 1.10.0\r\n- Python version: 3.8.12\r\n- Using Conda? PipEnv? PyEnv? Pex? Pip via Poetry\r\n- OS version: Ubuntu 20.04 (via Windows 11)\r\n- Browser version: Chrome 102.0.5005.115\r\n\r\n\r\n\n", "before_files": [{"content": "# Copyright 2018-2022 Streamlit Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport re\nimport threading\nfrom pathlib import Path\nfrom typing import Any, Callable, cast, Dict, List, Optional, Tuple\n\nfrom blinker import Signal\n\nfrom streamlit.logger import get_logger\nfrom streamlit.util import calc_md5\n\nLOGGER = get_logger(__name__)\n\n\ndef open_python_file(filename):\n    \"\"\"Open a read-only Python file taking proper care of its encoding.\n\n    In Python 3, we would like all files to be opened with utf-8 encoding.\n    However, some author like to specify PEP263 headers in their source files\n    with their own encodings. In that case, we should respect the author's\n    encoding.\n    \"\"\"\n    import tokenize\n\n    if hasattr(tokenize, \"open\"):  # Added in Python 3.2\n        # Open file respecting PEP263 encoding. If no encoding header is\n        # found, opens as utf-8.\n        return tokenize.open(filename)\n    else:\n        return open(filename, \"r\", encoding=\"utf-8\")\n\n\nPAGE_FILENAME_REGEX = re.compile(r\"([0-9]*)[_ -]*(.*)\\.py\")\n# Regex pattern to extract emoji taken from https://gist.github.com/Alex-Just/e86110836f3f93fe7932290526529cd1#gistcomment-3208085\n# We may eventually want to swap this out for https://pypi.org/project/emoji,\n# but I want to avoid adding a dependency if possible.\nPAGE_ICON_REGEX = re.compile(\n    \"(^[\\U0001F1E0-\\U0001F1FF\"\n    \"\\U0001F300-\\U0001F5FF\"\n    \"\\U0001F600-\\U0001F64F\"\n    \"\\U0001F680-\\U0001F6FF\"\n    \"\\U0001F700-\\U0001F77F\"\n    \"\\U0001F780-\\U0001F7FF\"\n    \"\\U0001F800-\\U0001F8FF\"\n    \"\\U0001F900-\\U0001F9FF\"\n    \"\\U0001FA00-\\U0001FA6F\"\n    \"\\U0001FA70-\\U0001FAFF\"\n    \"\\U00002702-\\U000027B0\"\n    \"\\U000024C2-\\U0001F251])[_-]*\"\n)\n\n\ndef page_sort_key(script_path: Path) -> Tuple[float, str]:\n    matches = re.findall(PAGE_FILENAME_REGEX, script_path.name)\n\n    # Failing this assert should only be possible if script_path isn't a Python\n    # file, which should never happen.\n    assert len(matches) > 0, f\"{script_path} is not a Python file\"\n\n    [(number, label)] = matches\n    label = label.lower()\n\n    if number == \"\":\n        return (float(\"inf\"), label)\n\n    return (float(number), label)\n\n\ndef page_name_and_icon(script_path: Path) -> Tuple[str, str]:\n    \"\"\"Compute the name of a page from its script path.\n\n    This is *almost* the page name displayed in the nav UI, but it has\n    underscores instead of spaces. The reason we do this is because having\n    spaces in URLs both looks bad and is hard to deal with due to the need to\n    URL-encode them. To solve this, we only swap the underscores for spaces\n    right before we render page names.\n    \"\"\"\n    extraction = re.search(PAGE_FILENAME_REGEX, script_path.name)\n    if extraction is None:\n        return \"\", \"\"\n\n    # This cast to Any+type annotation weirdness is done because\n    # cast(re.Match[str], ...) explodes at runtime since Python interprets it\n    # as an attempt to index into re.Match instead of as a type annotation.\n    extraction: re.Match[str] = cast(Any, extraction)\n\n    name = re.sub(r\"[_ ]+\", \"_\", extraction.group(2)).strip()\n    if not name:\n        name = extraction.group(1)\n\n    extracted_icon = re.search(PAGE_ICON_REGEX, name)\n    if extracted_icon is not None:\n        icon = str(extracted_icon.group(1))\n        name = re.sub(PAGE_ICON_REGEX, \"\", name)\n    else:\n        icon = \"\"\n\n    return str(name), icon\n\n\n_pages_cache_lock = threading.RLock()\n_cached_pages: Optional[Dict[str, Dict[str, str]]] = None\n_on_pages_changed = Signal(doc=\"Emitted when the pages directory is changed\")\n\n\ndef invalidate_pages_cache():\n    global _cached_pages\n\n    LOGGER.debug(\"Pages directory changed\")\n    with _pages_cache_lock:\n        _cached_pages = None\n\n    _on_pages_changed.send()\n\n\ndef get_pages(main_script_path_str: str) -> Dict[str, Dict[str, str]]:\n    global _cached_pages\n\n    # Avoid taking the lock if the pages cache hasn't been invalidated.\n    pages = _cached_pages\n    if pages is not None:\n        return pages\n\n    with _pages_cache_lock:\n        # The cache may have been repopulated while we were waiting to grab\n        # the lock.\n        if _cached_pages is not None:\n            return _cached_pages\n\n        main_script_path = Path(main_script_path_str)\n        main_page_name, main_page_icon = page_name_and_icon(main_script_path)\n        main_page_script_hash = calc_md5(main_script_path_str)\n\n        # NOTE: We include the page_script_hash in the dict even though it is\n        #       already used as the key because that occasionally makes things\n        #       easier for us when we need to iterate over pages.\n        pages = {\n            main_page_script_hash: {\n                \"page_script_hash\": main_page_script_hash,\n                \"page_name\": main_page_name,\n                \"icon\": main_page_icon,\n                \"script_path\": str(main_script_path),\n            }\n        }\n\n        pages_dir = main_script_path.parent / \"pages\"\n        page_scripts = sorted(\n            [f for f in pages_dir.glob(\"*.py\") if not f.name.startswith(\".\")],\n            key=page_sort_key,\n        )\n\n        for script_path in page_scripts:\n            script_path_str = str(script_path)\n            pn, pi = page_name_and_icon(script_path)\n            psh = calc_md5(script_path_str)\n\n            pages[psh] = {\n                \"page_script_hash\": psh,\n                \"page_name\": pn,\n                \"icon\": pi,\n                \"script_path\": script_path_str,\n            }\n\n        _cached_pages = pages\n\n        return pages\n\n\ndef register_pages_changed_callback(\n    callback: Callable[[str], None],\n):\n    def disconnect():\n        _on_pages_changed.disconnect(callback)\n\n    # weak=False so that we have control of when the pages changed\n    # callback is deregistered.\n    _on_pages_changed.connect(callback, weak=False)\n\n    return disconnect\n", "path": "lib/streamlit/source_util.py"}], "after_files": [{"content": "# Copyright 2018-2022 Streamlit Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport re\nimport threading\nfrom pathlib import Path\nfrom typing import Any, Callable, cast, Dict, List, Optional, Tuple\n\nfrom blinker import Signal\n\nfrom streamlit.logger import get_logger\nfrom streamlit.util import calc_md5\n\nLOGGER = get_logger(__name__)\n\n\ndef open_python_file(filename):\n    \"\"\"Open a read-only Python file taking proper care of its encoding.\n\n    In Python 3, we would like all files to be opened with utf-8 encoding.\n    However, some author like to specify PEP263 headers in their source files\n    with their own encodings. In that case, we should respect the author's\n    encoding.\n    \"\"\"\n    import tokenize\n\n    if hasattr(tokenize, \"open\"):  # Added in Python 3.2\n        # Open file respecting PEP263 encoding. If no encoding header is\n        # found, opens as utf-8.\n        return tokenize.open(filename)\n    else:\n        return open(filename, \"r\", encoding=\"utf-8\")\n\n\nPAGE_FILENAME_REGEX = re.compile(r\"([0-9]*)[_ -]*(.*)\\.py\")\n# Regex pattern to extract emoji taken from https://gist.github.com/Alex-Just/e86110836f3f93fe7932290526529cd1#gistcomment-3208085\n# We may eventually want to swap this out for https://pypi.org/project/emoji,\n# but I want to avoid adding a dependency if possible.\nPAGE_ICON_REGEX = re.compile(\n    \"(^[\\U0001F1E0-\\U0001F1FF\"\n    \"\\U0001F300-\\U0001F5FF\"\n    \"\\U0001F600-\\U0001F64F\"\n    \"\\U0001F680-\\U0001F6FF\"\n    \"\\U0001F700-\\U0001F77F\"\n    \"\\U0001F780-\\U0001F7FF\"\n    \"\\U0001F800-\\U0001F8FF\"\n    \"\\U0001F900-\\U0001F9FF\"\n    \"\\U0001FA00-\\U0001FA6F\"\n    \"\\U0001FA70-\\U0001FAFF\"\n    \"\\U00002702-\\U000027B0\"\n    \"\\U000024C2-\\U0001F251])[_-]*\"\n)\n\n\ndef page_sort_key(script_path: Path) -> Tuple[float, str]:\n    matches = re.findall(PAGE_FILENAME_REGEX, script_path.name)\n\n    # Failing this assert should only be possible if script_path isn't a Python\n    # file, which should never happen.\n    assert len(matches) > 0, f\"{script_path} is not a Python file\"\n\n    [(number, label)] = matches\n    label = label.lower()\n\n    if number == \"\":\n        return (float(\"inf\"), label)\n\n    return (float(number), label)\n\n\ndef page_name_and_icon(script_path: Path) -> Tuple[str, str]:\n    \"\"\"Compute the name of a page from its script path.\n\n    This is *almost* the page name displayed in the nav UI, but it has\n    underscores instead of spaces. The reason we do this is because having\n    spaces in URLs both looks bad and is hard to deal with due to the need to\n    URL-encode them. To solve this, we only swap the underscores for spaces\n    right before we render page names.\n    \"\"\"\n    extraction = re.search(PAGE_FILENAME_REGEX, script_path.name)\n    if extraction is None:\n        return \"\", \"\"\n\n    # This cast to Any+type annotation weirdness is done because\n    # cast(re.Match[str], ...) explodes at runtime since Python interprets it\n    # as an attempt to index into re.Match instead of as a type annotation.\n    extraction: re.Match[str] = cast(Any, extraction)\n\n    name = re.sub(r\"[_ ]+\", \"_\", extraction.group(2)).strip()\n    if not name:\n        name = extraction.group(1)\n\n    extracted_icon = re.search(PAGE_ICON_REGEX, name)\n    if extracted_icon is not None:\n        icon = str(extracted_icon.group(1))\n        name = re.sub(PAGE_ICON_REGEX, \"\", name)\n    else:\n        icon = \"\"\n\n    return str(name), icon\n\n\n_pages_cache_lock = threading.RLock()\n_cached_pages: Optional[Dict[str, Dict[str, str]]] = None\n_on_pages_changed = Signal(doc=\"Emitted when the pages directory is changed\")\n\n\ndef invalidate_pages_cache():\n    global _cached_pages\n\n    LOGGER.debug(\"Pages directory changed\")\n    with _pages_cache_lock:\n        _cached_pages = None\n\n    _on_pages_changed.send()\n\n\ndef get_pages(main_script_path_str: str) -> Dict[str, Dict[str, str]]:\n    global _cached_pages\n\n    # Avoid taking the lock if the pages cache hasn't been invalidated.\n    pages = _cached_pages\n    if pages is not None:\n        return pages\n\n    with _pages_cache_lock:\n        # The cache may have been repopulated while we were waiting to grab\n        # the lock.\n        if _cached_pages is not None:\n            return _cached_pages\n\n        main_script_path = Path(main_script_path_str)\n        main_page_name, main_page_icon = page_name_and_icon(main_script_path)\n        main_page_script_hash = calc_md5(main_script_path_str)\n\n        # NOTE: We include the page_script_hash in the dict even though it is\n        #       already used as the key because that occasionally makes things\n        #       easier for us when we need to iterate over pages.\n        pages = {\n            main_page_script_hash: {\n                \"page_script_hash\": main_page_script_hash,\n                \"page_name\": main_page_name,\n                \"icon\": main_page_icon,\n                \"script_path\": str(main_script_path.resolve()),\n            }\n        }\n\n        pages_dir = main_script_path.parent / \"pages\"\n        page_scripts = sorted(\n            [f for f in pages_dir.glob(\"*.py\") if not f.name.startswith(\".\")],\n            key=page_sort_key,\n        )\n\n        for script_path in page_scripts:\n            script_path_str = str(script_path.resolve())\n            pn, pi = page_name_and_icon(script_path)\n            psh = calc_md5(script_path_str)\n\n            pages[psh] = {\n                \"page_script_hash\": psh,\n                \"page_name\": pn,\n                \"icon\": pi,\n                \"script_path\": script_path_str,\n            }\n\n        _cached_pages = pages\n\n        return pages\n\n\ndef register_pages_changed_callback(\n    callback: Callable[[str], None],\n):\n    def disconnect():\n        _on_pages_changed.disconnect(callback)\n\n    # weak=False so that we have control of when the pages changed\n    # callback is deregistered.\n    _on_pages_changed.connect(callback, weak=False)\n\n    return disconnect\n", "path": "lib/streamlit/source_util.py"}]}