Repository: gradio-app/gradio
Pull request: Fix for `typing.get_type_hints()` on Python 3.9 or lower
A fix for the issue pointed out by @hysts which was breaking Gradio demos running `Python<=3.9`

It's not a perfect fix, as it parses type annotations as strings, so for example for `gr.Request`, the type annotation has to be `gr.Request` or `gradio.Request` or `Request` for it to be recognized correctly. But I don't believe there is any other way to do it, and this should be strictly better than what we have right now since this alternative logic is only used if an exception would be have been thrown otherwise. 

Fixes: #4216

Problem statement:
On Python 3.9, `typing.get_type_hints()` throws an error if "|" is used in the type hint
### Describe the bug

The following code works with Python 3.10, but not with Python 3.9, as reported by @hysts 

`app.py`

```py
#!/usr/bin/env python

import gradio as gr

from utils import fn

with gr.Blocks() as demo:
    text = gr.Text()
    text2 = gr.Text()
    text.submit(fn=fn, inputs=text, outputs=text2)

demo.queue(api_open=False).launch()
```

`utils.py`

```py
from __future__ import annotations

def fn(a: str | int) -> str:
    return f'{a}'
```

### Is there an existing issue for this?

- [X] I have searched the existing issues

### Reproduction

See above

### Screenshot

_No response_

### Logs

```shell
Traceback (most recent call last):
  File "/home/hysts/.pyenv/versions/3.9.16/lib/python3.9/multiprocessing/process.py", line 315, in _bootstrap
    self.run()
  File "/home/hysts/.pyenv/versions/3.9.16/lib/python3.9/multiprocessing/process.py", line 108, in run
    self._target(*self._args, **self._kwargs)
  File "/home/hysts/.virtualenvs/py3916/lib/python3.9/site-packages/uvicorn/_subprocess.py", line 76, in subprocess_started
    target(sockets=sockets)
  File "/home/hysts/.virtualenvs/py3916/lib/python3.9/site-packages/uvicorn/server.py", line 60, in run
    return asyncio.run(self.serve(sockets=sockets))
  File "/home/hysts/.pyenv/versions/3.9.16/lib/python3.9/asyncio/runners.py", line 44, in run
    return loop.run_until_complete(main)
  File "/home/hysts/.pyenv/versions/3.9.16/lib/python3.9/asyncio/base_events.py", line 647, in run_until_complete
    return future.result()
  File "/home/hysts/.virtualenvs/py3916/lib/python3.9/site-packages/uvicorn/server.py", line 67, in serve
    config.load()
  File "/home/hysts/.virtualenvs/py3916/lib/python3.9/site-packages/uvicorn/config.py", line 477, in load
    self.loaded_app = import_from_string(self.app)
  File "/home/hysts/.virtualenvs/py3916/lib/python3.9/site-packages/uvicorn/importer.py", line 21, in import_from_string
    module = importlib.import_module(module_str)
  File "/home/hysts/.pyenv/versions/3.9.16/lib/python3.9/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1030, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1007, in _find_and_load
  File "<frozen importlib._bootstrap>", line 986, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 680, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 850, in exec_module
  File "<frozen importlib._bootstrap>", line 228, in _call_with_frames_removed
  File "/tmp/temp2/./app.py", line 10, in <module>
    text.submit(fn=fn, inputs=text, outputs=text2)
  File "/home/hysts/.virtualenvs/py3916/lib/python3.9/site-packages/gradio/events.py", line 132, in __call__
    dep, dep_index = self.trigger.set_event_trigger(
  File "/home/hysts/.virtualenvs/py3916/lib/python3.9/site-packages/gradio/blocks.py", line 218, in set_event_trigger
    check_function_inputs_match(fn, inputs, inputs_as_dict)
  File "/home/hysts/.virtualenvs/py3916/lib/python3.9/site-packages/gradio/utils.py", line 888, in check_function_inputs_match
    parameter_types = get_type_hints(fn)
  File "/home/hysts/.virtualenvs/py3916/lib/python3.9/site-packages/gradio/utils.py", line 862, in get_type_hints
    return typing.get_type_hints(fn)
  File "/home/hysts/.pyenv/versions/3.9.16/lib/python3.9/typing.py", line 1497, in get_type_hints
    value = _eval_type(value, globalns, localns)
  File "/home/hysts/.pyenv/versions/3.9.16/lib/python3.9/typing.py", line 292, in _eval_type
    return t._evaluate(globalns, localns, recursive_guard)
  File "/home/hysts/.pyenv/versions/3.9.16/lib/python3.9/typing.py", line 554, in _evaluate
    eval(self.__forward_code__, globalns, localns),
  File "<string>", line 1, in <module>
TypeError: unsupported operand type(s) for |: 'type' and 'type'
```


### System Info

```shell
latest
```


### Severity

serious, but I can work around it

Hints from issue discussion:
Adding to 3.x milestone as this could break a lot of gradio demos running on Python 3.9 
This is the reason I disabled new-style annotations in the Ruff config.

https://github.com/gradio-app/gradio/blob/8b72e9e1272e9468646d9d5c689ec60a8cac0da8/pyproject.toml#L83

Commit message: Update gradio/utils.py

Co-authored-by: Aarni Koskela <akx@iki.fi>

Patch to review:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4175c2b85e..29f55c4fbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,12 +6,14 @@
 - Added `format` argument to `Audio` component by [@freddyaboulton](https://github.com/freddyaboulton) in [PR 4178](https://github.com/gradio-app/gradio/pull/4178)
 - Add JS client code snippets to use via api page by [@aliabd](https://github.com/aliabd) in [PR 3927](https://github.com/gradio-app/gradio/pull/3927).
 
+
 ## Bug Fixes:
 
 - Fix "TypeError: issubclass() arg 1 must be a class" When use Optional[Types] by [@lingfengchencn](https://github.com/lingfengchencn) in [PR 4200](https://github.com/gradio-app/gradio/pull/4200). 
 - Gradio will no longer send any analytics if analytics are disabled with the GRADIO_ANALYTICS_ENABLED environment variable. By [@akx](https://github.com/akx) in [PR 4194](https://github.com/gradio-app/gradio/pull/4194)
 - The deprecation warnings for kwargs now show the actual stack level for the invocation, by [@akx](https://github.com/akx) in [PR 4203](https://github.com/gradio-app/gradio/pull/4203).
 - Fix "TypeError: issubclass() arg 1 must be a class" When use Optional[Types] by [@lingfengchencn](https://github.com/lingfengchencn) in [PR 4200](https://github.com/gradio-app/gradio/pull/4200). 
+- Fixes a bug with typing.get_type_hints() on Python 3.9 by [@abidlabs](https://github.com/abidlabs) in [PR 4228](https://github.com/gradio-app/gradio/pull/4228).
 
 ## Other Changes:
 
diff --git a/gradio/utils.py b/gradio/utils.py
index 5a0abd39b5..5a7def6e9c 100644
--- a/gradio/utils.py
+++ b/gradio/utils.py
@@ -683,11 +683,41 @@ async def cancel(session_hash: str) -> None:
 
 
 def get_type_hints(fn):
+    # Importing gradio with the canonical abbreviation. Used in typing._eval_type.
+    import gradio as gr  # noqa: F401
+    from gradio import Request  # noqa: F401
+
     if inspect.isfunction(fn) or inspect.ismethod(fn):
-        return typing.get_type_hints(fn)
+        pass
     elif callable(fn):
-        return typing.get_type_hints(fn.__call__)
-    return {}
+        fn = fn.__call__
+    else:
+        return {}
+
+    try:
+        return typing.get_type_hints(fn)
+    except TypeError:
+        # On Python 3.9 or earlier, get_type_hints throws a TypeError if the function
+        # has a type annotation that include "|". We resort to parsing the signature
+        # manually using inspect.signature.
+        type_hints = {}
+        sig = inspect.signature(fn)
+        for name, param in sig.parameters.items():
+            if param.annotation is inspect.Parameter.empty:
+                continue
+            if "|" in str(param.annotation):
+                continue
+                # To convert the string annotation to a class, we use the
+                # internal typing._eval_type function. This is not ideal, but
+                # it's the only way to do it without eval-ing the string.
+                # Since the API is internal, it may change in the future.
+                try:
+                    type_hints[name] = typing._eval_type(  # type: ignore
+                        typing.ForwardRef(param.annotation), globals(), locals()
+                    )
+                except (NameError, TypeError):
+                    pass
+        return type_hints
 
 
 def is_special_typed_parameter(name, parameter_types):
diff --git a/test/test_utils.py b/test/test_utils.py
index 046c23ba75..911b851aef 100644
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -1,9 +1,10 @@
+from __future__ import annotations
+
 import copy
 import os
 import sys
 import unittest.mock as mock
 import warnings
-from typing import List
 from unittest.mock import MagicMock
 
 import pytest
@@ -570,7 +571,7 @@ class GenericObject:
         assert len(get_type_hints(GenericObject())) == 0
 
     def test_is_special_typed_parameter(self):
-        def func(a: List[str], b: Literal["a", "b"], c, d: Request):
+        def func(a: list[str], b: Literal["a", "b"], c, d: Request):
             pass
 
         hints = get_type_hints(func)
@@ -579,6 +580,15 @@ def func(a: List[str], b: Literal["a", "b"], c, d: Request):
         assert not is_special_typed_parameter("c", hints)
         assert is_special_typed_parameter("d", hints)
 
+    def test_is_special_typed_parameter_with_pipe(self):
+        def func(a: Request, b: str | int, c: list[str]):
+            pass
+
+        hints = get_type_hints(func)
+        assert is_special_typed_parameter("a", hints)
+        assert not is_special_typed_parameter("b", hints)
+        assert not is_special_typed_parameter("c", hints)
+
 
 class TestCheckFunctionInputsMatch:
     def test_check_function_inputs_match(self):


Code review comments:
diff_hunk: @@ -683,11 +683,41 @@ async def cancel(session_hash: str) -> None:
 
 
 def get_type_hints(fn):
+    # Importing gradio with the canonical abbreviation. Used in typing._eval_type.
+    import gradio as gr  # noqa: F401
+    from gradio import Request  # noqa: F401
+
     if inspect.isfunction(fn) or inspect.ismethod(fn):
-        return typing.get_type_hints(fn)
+        pass
     elif callable(fn):
-        return typing.get_type_hints(fn.__call__)
-    return {}
+        fn = fn.__call__
+    else:
+        return {}
+
+    try:
+        return typing.get_type_hints(fn)
+    except TypeError:
+        # On Python 3.9 or earlier, get_type_hints throws a TypeError if the function
+        # has a type annotation that include "|". We resort to parsing the signature
+        # manually using inspect.signature.
+        type_hints = {}
+        sig = inspect.signature(fn)
+        for name, param in sig.parameters.items():
+            if param.annotation is inspect.Parameter.empty:
+                continue
+            if "|" in str(param.annotation):
+                continue
+                # To convert the string annotation to a class, we use the
+                # internal typing._eval_type function. This is not ideal, but
+                # it's the only way to do it without eval-ing the string.
+                # Since the API is internal, it may change in the future.
+                try:
path: gradio/utils.py
text: @user1:
This would never run because it's after the continue. Am I missing something?

@author:
Whoops indentation mistake. Let me fix

@author:
Fixed