Repository: gradio-app/gradio

Issue:
Remove `typing-extensions` dependency
In #1805, we introduced an additional dependency in the `gradio` package: `typing-extensions` in order for `gradio` to work properly with Python 3.7.

However, this dependency is not strictly needed as we can wrap the relevant type checking code with a TYPE_CHECKING block so it does not get executed at runtime, as pointed out by @freddyaboulton.

It would be good to remove this dependency so we lighten the package.


Discussion:

Resolving patch:
diff --git a/gradio/components.py b/gradio/components.py
--- a/gradio/components.py
+++ b/gradio/components.py
@@ -12,17 +12,19 @@
 import os
 import pathlib
 import shutil
-import sys
 import tempfile
 import warnings
 from copy import deepcopy
 from types import ModuleType
-from typing import Any, Callable, Dict, List, Optional, Tuple
+from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple
 
-if sys.version_info[0] == 3 and sys.version_info[1] >= 8:
+if TYPE_CHECKING:
     from typing import TypedDict
-else:
-    from typing_extensions import TypedDict
+
+    class DataframeData(TypedDict):
+        headers: List[str]
+        data: List[List[str | int | bool]]
+
 
 import matplotlib.figure
 import numpy as np
@@ -2400,11 +2402,6 @@ def style(
         )
 
 
-class DataframeData(TypedDict):
-    headers: List[str]
-    data: List[List[str | int | bool]]
-
-
 @document()
 class Dataframe(Changeable, IOComponent):
     """


Test patch:
diff --git a/scripts/create_test_requirements.sh b/scripts/create_test_requirements.sh
--- a/scripts/create_test_requirements.sh
+++ b/scripts/create_test_requirements.sh
@@ -3,8 +3,8 @@ if [ -z "$(ls | grep CONTRIBUTING.md)" ]; then
   echo "Please run the script from repo directory"
   exit -1
 else
-  echo "Creating requirements under test/requirements.txt using requirements.in. Please run this script from unix or wsl!"
+  echo "Creating requirements under test/requirements.txt using requirements.in. Please run this script from unix or wsl in a python3.7 env!"
   cd test
   pip install --upgrade pip-tools
-  pip-compile
+  pip-compile --output-file requirements.txt
 fi
diff --git a/test/requirements.in b/test/requirements.in
--- a/test/requirements.in
+++ b/test/requirements.in
@@ -17,4 +17,5 @@ black
 isort
 flake8
 httpx
-pydantic
\ No newline at end of file
+pydantic
+respx
\ No newline at end of file
diff --git a/test/requirements.txt b/test/requirements.txt
--- a/test/requirements.txt
+++ b/test/requirements.txt
@@ -1,76 +1,52 @@
 #
-# This file is autogenerated by pip-compile with python 3.9
+# This file is autogenerated by pip-compile with python 3.7
 # To update, run:
 #
-#    pip-compile
+#    pip-compile --output-file=requirements.txt
 #
-absl-py==1.0.0
-    # via
-    #   tensorboard
-    #   tensorflow
-alembic==1.7.6
+alembic==1.8.1
     # via mlflow
-asttokens==2.0.5
-    # via stack-data
-astunparse==1.6.3
-    # via tensorflow
+anyio==3.6.1
+    # via httpcore
 asyncio==3.4.3
     # via -r requirements.in
-atomicwrites==1.4.1
-    # via pytest
 attrs==21.4.0
     # via
     #   jsonschema
     #   pytest
 backcall==0.2.0
     # via ipython
-black==22.1.0
-    # via
-    #   -r requirements.in
-    #   ipython
-cachetools==5.0.0
-    # via google-auth
-certifi==2021.10.8
+black==22.6.0
+    # via -r requirements.in
+certifi==2022.6.15
     # via
     #   dulwich
+    #   httpcore
+    #   httpx
     #   requests
     #   sentry-sdk
-    #   urllib3
-cffi==1.15.0
-    # via cryptography
-charset-normalizer==2.0.11
+charset-normalizer==2.1.0
     # via requests
-click==8.0.3
+click==8.1.3
     # via
     #   black
     #   databricks-cli
     #   flask
     #   mlflow
-    #   sacremoses
     #   wandb
-cloudpickle==2.0.0
+cloudpickle==2.1.0
     # via
     #   mlflow
     #   shap
-colorama==0.4.4
-    # via
-    #   click
-    #   ipython
-    #   pytest
-    #   tqdm
-comet-ml==3.25.0
+comet-ml==3.31.6
     # via -r requirements.in
 configobj==5.0.6
     # via everett
-coverage[toml]==6.3.1
+coverage[toml]==6.4.2
     # via
     #   -r requirements.in
     #   pytest-cov
-cryptography==36.0.1
-    # via
-    #   pyopenssl
-    #   urllib3
-databricks-cli==0.16.4
+databricks-cli==0.17.0
     # via mlflow
 decorator==5.1.1
     # via ipython
@@ -78,95 +54,90 @@ docker==5.0.3
     # via mlflow
 docker-pycreds==0.4.0
     # via wandb
-dulwich==0.20.32
+dulwich==0.20.45
     # via comet-ml
 entrypoints==0.4
     # via mlflow
 everett[ini]==3.0.0
     # via comet-ml
-executing==0.8.2
-    # via stack-data
-filelock==3.4.2
+filelock==3.7.1
     # via
     #   huggingface-hub
     #   transformers
 flake8==4.0.1
     # via -r requirements.in
-flask==2.0.2
+flask==2.1.3
     # via
     #   mlflow
     #   prometheus-flask-exporter
-flatbuffers==2.0
-    # via tensorflow
-gast==0.5.3
-    # via tensorflow
 gitdb==4.0.9
     # via gitpython
-gitpython==3.1.26
+gitpython==3.1.27
     # via
     #   mlflow
     #   wandb
-google-auth==2.6.0
-    # via
-    #   google-auth-oauthlib
-    #   tensorboard
-google-auth-oauthlib==0.4.6
-    # via tensorboard
-google-pasta==0.2.0
-    # via tensorflow
 greenlet==1.1.2
     # via sqlalchemy
-grpcio==1.43.0
+gunicorn==20.1.0
+    # via mlflow
+h11==0.12.0
+    # via httpcore
+httpcore==0.15.0
+    # via httpx
+httpx==0.23.0
     # via
-    #   tensorboard
-    #   tensorflow
-h5py==3.6.0
-    # via tensorflow
-huggingface-hub==0.4.0
+    #   -r requirements.in
+    #   respx
+huggingface-hub==0.8.1
     # via
     #   -r requirements.in
     #   transformers
 idna==3.3
     # via
+    #   anyio
     #   requests
-    #   urllib3
-imageio==2.14.1
+    #   rfc3986
+imageio==2.19.5
     # via scikit-image
-importlib-metadata==4.10.1
+importlib-metadata==4.2.0
     # via
-    #   markdown
+    #   alembic
+    #   click
+    #   flake8
+    #   flask
+    #   huggingface-hub
+    #   jsonschema
+    #   mako
     #   mlflow
+    #   pluggy
+    #   pytest
+    #   sqlalchemy
+    #   transformers
+importlib-resources==5.8.0
+    # via
+    #   alembic
+    #   jsonschema
 iniconfig==1.1.1
     # via pytest
-ipython==8.0.1
+ipython==7.34.0
     # via -r requirements.in
 isort==5.10.1
     # via -r requirements.in
-itsdangerous==2.0.1
+itsdangerous==2.1.2
     # via flask
 jedi==0.18.1
     # via ipython
-jinja2==3.0.3
+jinja2==3.1.2
     # via flask
 joblib==1.1.0
-    # via
-    #   sacremoses
-    #   scikit-learn
-jsonschema==4.4.0
+    # via scikit-learn
+jsonschema==4.7.2
     # via comet-ml
-keras==2.8.0
-    # via tensorflow
-keras-preprocessing==1.1.2
-    # via tensorflow
-libclang==13.0.0
-    # via tensorflow
-llvmlite==0.38.0
+llvmlite==0.38.1
     # via numba
-mako==1.1.6
+mako==1.2.1
     # via alembic
-markdown==3.3.6
-    # via tensorboard
-markupsafe==2.0.1
+markupsafe==2.1.1
     # via
     #   jinja2
     #   mako
@@ -174,38 +145,31 @@ matplotlib-inline==0.1.3
     # via ipython
 mccabe==0.6.1
     # via flake8
-mlflow==1.23.1
+mlflow==1.27.0
     # via -r requirements.in
 mypy-extensions==0.4.3
     # via black
 networkx==2.6.3
     # via scikit-image
-numba==0.55.1
+numba==0.55.2
     # via shap
-numpy==1.21.5
+numpy==1.21.6
     # via
-    #   h5py
     #   imageio
-    #   keras-preprocessing
     #   mlflow
     #   numba
-    #   opt-einsum
     #   pandas
     #   pywavelets
     #   scikit-image
     #   scikit-learn
     #   scipy
     #   shap
-    #   tensorboard
-    #   tensorflow
     #   tifffile
     #   transformers
 nvidia-ml-py3==7.352.0
     # via comet-ml
 oauthlib==3.2.0
-    # via requests-oauthlib
-opt-einsum==3.3.0
-    # via tensorflow
+    # via databricks-cli
 packaging==21.3
     # via
     #   huggingface-hub
@@ -214,7 +178,7 @@ packaging==21.3
     #   scikit-image
     #   shap
     #   transformers
-pandas==1.4.0
+pandas==1.3.5
     # via
     #   mlflow
     #   shap
@@ -224,74 +188,66 @@ pathspec==0.9.0
     # via black
 pathtools==0.1.2
     # via wandb
+pexpect==4.8.0
+    # via ipython
 pickleshare==0.7.5
     # via ipython
-pillow==9.0.1
+pillow==9.2.0
     # via
     #   imageio
     #   scikit-image
-platformdirs==2.4.1
+platformdirs==2.5.2
     # via black
 pluggy==1.0.0
     # via pytest
-prometheus-client==0.13.1
+prometheus-client==0.14.1
     # via prometheus-flask-exporter
-prometheus-flask-exporter==0.18.7
+prometheus-flask-exporter==0.20.2
     # via mlflow
 promise==2.3
     # via wandb
-prompt-toolkit==3.0.26
+prompt-toolkit==3.0.30
     # via ipython
-protobuf==3.19.4
+protobuf==3.20.1
     # via
     #   mlflow
-    #   tensorboard
-    #   tensorflow
     #   wandb
-psutil==5.9.0
+psutil==5.9.1
     # via wandb
-pure-eval==0.2.2
-    # via stack-data
+ptyprocess==0.7.0
+    # via pexpect
 py==1.11.0
     # via pytest
-pyasn1==0.4.8
-    # via
-    #   pyasn1-modules
-    #   rsa
-pyasn1-modules==0.2.8
-    # via google-auth
 pycodestyle==2.8.0
     # via flake8
-pycparser==2.21
-    # via cffi
+pydantic==1.9.1
+    # via -r requirements.in
 pyflakes==2.4.0
     # via flake8
-pygments==2.11.2
+pygments==2.12.0
     # via ipython
-pyopenssl==22.0.0
-    # via urllib3
-pyparsing==3.0.7
+pyjwt==2.4.0
+    # via databricks-cli
+pyparsing==3.0.9
     # via packaging
 pyrsistent==0.18.1
     # via jsonschema
-pytest==7.0.0
+pytest==7.1.2
     # via
     #   -r requirements.in
     #   pytest-asyncio
     #   pytest-cov
-pytest-asyncio==0.18.3
+pytest-asyncio==0.19.0
     # via -r requirements.in
 pytest-cov==3.0.0
     # via -r requirements.in
 python-dateutil==2.8.2
-    # via
-    #   pandas
-    #   wandb
-pytz==2021.3
+    # via pandas
+pytz==2022.1
     # via
     #   mlflow
     #   pandas
-pywavelets==1.2.0
+pywavelets==1.3.0
     # via scikit-image
 pyyaml==6.0
     # via
@@ -301,164 +257,133 @@ pyyaml==6.0
     #   wandb
 querystring-parser==1.2.4
     # via mlflow
-regex==2022.1.18
-    # via
-    #   sacremoses
-    #   transformers
-requests==2.27.1
+regex==2022.7.9
+    # via transformers
+requests==2.28.1
     # via
     #   comet-ml
     #   databricks-cli
     #   docker
     #   huggingface-hub
     #   mlflow
-    #   requests-oauthlib
     #   requests-toolbelt
-    #   tensorboard
     #   transformers
     #   wandb
-requests-oauthlib==1.3.1
-    # via google-auth-oauthlib
 requests-toolbelt==0.9.1
     # via comet-ml
-rsa==4.8
-    # via google-auth
-sacremoses==0.0.47
-    # via transformers
-scikit-image==0.19.1
+respx==0.19.2
+    # via -r requirements.in
+rfc3986[idna2008]==1.5.0
+    # via httpx
+scikit-image==0.19.3
     # via -r requirements.in
 scikit-learn==1.0.2
     # via shap
-scipy==1.8.0
+scipy==1.7.3
     # via
     #   mlflow
     #   scikit-image
     #   scikit-learn
     #   shap
-selenium==4.0.0a6.post2
-    # via -r requirements.in
-semantic-version==2.9.0
+semantic-version==2.10.0
     # via comet-ml
-sentry-sdk==1.5.4
+sentry-sdk==1.7.2
+    # via
+    #   comet-ml
+    #   wandb
+setproctitle==1.2.3
     # via wandb
-shap==0.40.0
+shap==0.41.0
     # via -r requirements.in
-shortuuid==1.0.8
+shortuuid==1.0.9
     # via wandb
 six==1.16.0
     # via
-    #   absl-py
-    #   asttokens
-    #   astunparse
     #   comet-ml
     #   configobj
     #   databricks-cli
     #   docker-pycreds
-    #   google-auth
-    #   google-pasta
-    #   grpcio
-    #   keras-preprocessing
     #   promise
     #   python-dateutil
     #   querystring-parser
-    #   sacremoses
-    #   tensorflow
     #   wandb
 slicer==0.0.7
     # via shap
 smmap==5.0.0
     # via gitdb
-sqlalchemy==1.4.31
+sniffio==1.2.0
+    # via
+    #   anyio
+    #   httpcore
+    #   httpx
+sqlalchemy==1.4.39
     # via
     #   alembic
     #   mlflow
 sqlparse==0.4.2
     # via mlflow
-stack-data==0.1.4
-    # via ipython
-tabulate==0.8.9
+tabulate==0.8.10
     # via databricks-cli
-tensorboard==2.8.0
-    # via tensorflow
-tensorboard-data-server==0.6.1
-    # via tensorboard
-tensorboard-plugin-wit==1.8.1
-    # via tensorboard
-tensorflow==2.8.0
-    # via -r requirements.in
-tensorflow-io-gcs-filesystem==0.24.0
-    # via tensorflow
-termcolor==1.1.0
-    # via
-    #   tensorflow
-    #   yaspin
-tf-estimator-nightly==2.8.0.dev2021122109
-    # via tensorflow
 threadpoolctl==3.1.0
     # via scikit-learn
-tifffile==2022.2.2
+tifffile==2021.11.2
     # via scikit-image
-tokenizers==0.11.4
+tokenizers==0.12.1
     # via transformers
-tomli==2.0.0
+tomli==2.0.1
     # via
     #   black
     #   coverage
     #   pytest
-torch==1.10.2
+torch==1.12.0
     # via -r requirements.in
-tqdm==4.62.3
+tqdm==4.64.0
     # via
     #   huggingface-hub
-    #   sacremoses
     #   shap
     #   transformers
-traitlets==5.1.1
+traitlets==5.3.0
     # via
     #   ipython
     #   matplotlib-inline
-transformers==4.16.2
+transformers==4.20.1
     # via -r requirements.in
-typing-extensions==4.0.1
+typed-ast==1.5.4
+    # via black
+typing-extensions==4.3.0
     # via
+    #   anyio
     #   black
+    #   gitpython
     #   huggingface-hub
-    #   tensorflow
+    #   importlib-metadata
+    #   jsonschema
+    #   pydantic
+    #   pytest-asyncio
     #   torch
-urllib3[secure]==1.26.8
+urllib3==1.26.10
     # via
     #   dulwich
     #   requests
-    #   selenium
     #   sentry-sdk
-waitress==2.1.1
-    # via mlflow
-wandb==0.12.10
+wandb==0.12.21
     # via -r requirements.in
 wcwidth==0.2.5
     # via prompt-toolkit
-websocket-client==1.2.3
+websocket-client==1.3.3
     # via
     #   comet-ml
     #   docker
-werkzeug==2.0.2
-    # via
-    #   flask
-    #   tensorboard
-wheel==0.37.1
-    # via
-    #   astunparse
-    #   tensorboard
-wrapt==1.13.3
-    # via
-    #   comet-ml
-    #   tensorflow
+werkzeug==2.1.2
+    # via flask
+wrapt==1.14.1
+    # via comet-ml
 wurlitzer==3.0.2
     # via comet-ml
-yaspin==2.1.0
-    # via wandb
-zipp==3.7.0
-    # via importlib-metadata
+zipp==3.8.1
+    # via
+    #   importlib-metadata
+    #   importlib-resources
 
 # The following packages are considered to be unsafe in a requirements file:
 # setuptools
diff --git a/test/test_documentation.py b/test/test_documentation.py
--- a/test/test_documentation.py
+++ b/test/test_documentation.py
@@ -1,9 +1,16 @@
+import sys
 import unittest
 
+import pytest
+
 import gradio as gr
 
 
-class TestDocumentatino(unittest.TestCase):
+class TestDocumentation(unittest.TestCase):
+    @pytest.mark.skipif(
+        sys.version_info < (3, 8),
+        reason="Docs use features in inspect module not available in py 3.7",
+    )
     def test_documentation(self):
         documentation = gr.documentation.generate_documentation()
         assert len(documentation) > 0
diff --git a/test/test_utils.py b/test/test_utils.py
--- a/test/test_utils.py
+++ b/test/test_utils.py
@@ -3,13 +3,13 @@
 import unittest
 import unittest.mock as mock
 import warnings
-from typing import Literal
 
 import pytest
 import pytest_asyncio
 import requests
-from httpx import AsyncClient
+from httpx import AsyncClient, Response
 from pydantic import BaseModel
+from typing_extensions import Literal
 
 from gradio.test_data.blocks_configs import (
     XRAY_CONFIG,
@@ -259,143 +259,155 @@ async def client():
 
 
 def make_mock_response(return_value):
-    response = mock.MagicMock(name="mock_response")
-    response.status_code = 201
-    response.json.return_value = return_value
-    return response
+    return Response(201, json=return_value)
 
 
-@mock.patch("gradio.utils.client.send")
-class TestRequest:
-    @pytest.mark.asyncio
-    async def test_get(self, mock_send):
+MOCK_REQUEST_URL = "https://very_real_url.com"
 
-        mock_send.return_value = make_mock_response({"Host": "headers.jsontest.com"})
 
-        client_response: Request = await Request(
-            method=Request.Method.GET,
-            url="very_real_url.com",
-        )
-        validated_data = client_response.get_validated_data()
-        assert client_response.is_valid() is True
-        assert validated_data["Host"] == "headers.jsontest.com"
+@pytest.mark.asyncio
+async def test_get(respx_mock):
+    respx_mock.get(MOCK_REQUEST_URL).mock(
+        make_mock_response({"Host": "headers.jsontest.com"})
+    )
 
-    @pytest.mark.asyncio
-    async def test_post(self, mock_send):
+    client_response: Request = await Request(
+        method=Request.Method.GET,
+        url=MOCK_REQUEST_URL,
+    )
+    validated_data = client_response.get_validated_data()
+    assert client_response.is_valid() is True
+    assert validated_data["Host"] == "headers.jsontest.com"
 
-        payload = {"name": "morpheus", "job": "leader"}
-        mock_send.return_value = make_mock_response(payload)
 
-        client_response: Request = await Request(
-            method=Request.Method.POST,
-            url="very_real_url.com",
-            json=payload,
-        )
-        validated_data = client_response.get_validated_data()
-        assert client_response.status == 201
-        assert validated_data["job"] == "leader"
-        assert validated_data["name"] == "morpheus"
-
-    @pytest.mark.asyncio
-    async def test_validate_with_model(self, mock_send):
-        mock_send.return_value = make_mock_response(
-            {
-                "name": "morpheus",
-                "id": "1",
-                "job": "leader",
-                "createdAt": "2",
-            }
-        )
+@pytest.mark.asyncio
+async def test_post(respx_mock):
 
-        class TestModel(BaseModel):
-            name: str
-            job: str
-            id: str
-            createdAt: str
-
-        client_response: Request = await Request(
-            method=Request.Method.POST,
-            url="very_real_url.com",
-            json={"name": "morpheus", "job": "leader"},
-            validation_model=TestModel,
-        )
-        assert isinstance(client_response.get_validated_data(), TestModel)
-
-    @pytest.mark.asyncio
-    async def test_validate_and_fail_with_model(self, mock_send):
-        class TestModel(BaseModel):
-            name: Literal[str] = "John"
-            job: str
-
-        payload = {"name": "morpheus", "job": "leader"}
-        mock_send.return_value = make_mock_response(payload)
-
-        client_response: Request = await Request(
-            method=Request.Method.POST,
-            url="very_real_url.com",
-            json=payload,
-            validation_model=TestModel,
-        )
-        with pytest.raises(Exception):
-            client_response.is_valid(raise_exceptions=True)
-        assert client_response.has_exception is True
-        assert isinstance(client_response.exception, Exception)
-
-    @mock.patch("gradio.utils.Request._validate_response_data")
-    @pytest.mark.asyncio
-    async def test_exception_type(self, validate_response_data, mock_send):
-        class ResponseValidationException(Exception):
-            message = "Response object is not valid."
-
-        validate_response_data.side_effect = Exception()
-
-        client_response: Request = await Request(
-            method=Request.Method.GET,
-            url="very_real_url.com",
-            exception_type=ResponseValidationException,
-        )
-        assert isinstance(client_response.exception, ResponseValidationException)
+    payload = {"name": "morpheus", "job": "leader"}
+    respx_mock.post(MOCK_REQUEST_URL).mock(make_mock_response(payload))
 
-    @pytest.mark.asyncio
-    async def test_validate_with_function(self, mock_send):
-        mock_send.return_value = make_mock_response({"name": "morpheus", "id": 1})
+    client_response: Request = await Request(
+        method=Request.Method.POST,
+        url=MOCK_REQUEST_URL,
+        json=payload,
+    )
+    validated_data = client_response.get_validated_data()
+    assert client_response.status == 201
+    assert validated_data["job"] == "leader"
+    assert validated_data["name"] == "morpheus"
 
-        def has_name(response):
-            if response["name"] is not None:
-                return response
-            raise Exception
 
-        client_response: Request = await Request(
-            method=Request.Method.POST,
-            url="very_real_url.com",
-            json={"name": "morpheus", "job": "leader"},
-            validation_function=has_name,
-        )
-        validated_data = client_response.get_validated_data()
-        assert client_response.is_valid() is True
-        assert validated_data["id"] is not None
-        assert client_response.exception is None
-
-    @pytest.mark.asyncio
-    async def test_validate_and_fail_with_function(self, mock_send):
-        def has_name(response):
-            if response["name"] is not None:
-                if response["name"] == "Alex":
-                    return response
-            raise Exception
-
-        mock_send.return_value = make_mock_response({"name": "morpheus"})
-
-        client_response: Request = await Request(
-            method=Request.Method.POST,
-            url="very_real_url.com",
-            json={"name": "morpheus", "job": "leader"},
-            validation_function=has_name,
-        )
-        assert client_response.is_valid() is False
-        with pytest.raises(Exception):
-            client_response.is_valid(raise_exceptions=True)
-        assert client_response.exception is not None
+@pytest.mark.asyncio
+async def test_validate_with_model(respx_mock):
+
+    response = make_mock_response(
+        {
+            "name": "morpheus",
+            "id": "1",
+            "job": "leader",
+            "createdAt": "2",
+        }
+    )
+    respx_mock.post(MOCK_REQUEST_URL).mock(response)
+
+    class TestModel(BaseModel):
+        name: str
+        job: str
+        id: str
+        createdAt: str
+
+    client_response: Request = await Request(
+        method=Request.Method.POST,
+        url=MOCK_REQUEST_URL,
+        json={"name": "morpheus", "job": "leader"},
+        validation_model=TestModel,
+    )
+    assert isinstance(client_response.get_validated_data(), TestModel)
+
+
+@pytest.mark.asyncio
+async def test_validate_and_fail_with_model(respx_mock):
+    class TestModel(BaseModel):
+        name: Literal[str] = "John"
+        job: str
+
+    payload = {"name": "morpheus", "job": "leader"}
+    respx_mock.post(MOCK_REQUEST_URL).mock(make_mock_response(payload))
+
+    client_response: Request = await Request(
+        method=Request.Method.POST,
+        url=MOCK_REQUEST_URL,
+        json=payload,
+        validation_model=TestModel,
+    )
+    with pytest.raises(Exception):
+        client_response.is_valid(raise_exceptions=True)
+    assert client_response.has_exception is True
+    assert isinstance(client_response.exception, Exception)
+
+
+@mock.patch("gradio.utils.Request._validate_response_data")
+@pytest.mark.asyncio
+async def test_exception_type(validate_response_data, respx_mock):
+    class ResponseValidationException(Exception):
+        message = "Response object is not valid."
+
+    validate_response_data.side_effect = Exception()
+
+    respx_mock.get(MOCK_REQUEST_URL).mock(Response(201))
+
+    client_response: Request = await Request(
+        method=Request.Method.GET,
+        url=MOCK_REQUEST_URL,
+        exception_type=ResponseValidationException,
+    )
+    assert isinstance(client_response.exception, ResponseValidationException)
+
+
+@pytest.mark.asyncio
+async def test_validate_with_function(respx_mock):
+
+    respx_mock.post(MOCK_REQUEST_URL).mock(
+        make_mock_response({"name": "morpheus", "id": 1})
+    )
+
+    def has_name(response):
+        if response["name"] is not None:
+            return response
+        raise Exception
+
+    client_response: Request = await Request(
+        method=Request.Method.POST,
+        url=MOCK_REQUEST_URL,
+        json={"name": "morpheus", "job": "leader"},
+        validation_function=has_name,
+    )
+    validated_data = client_response.get_validated_data()
+    assert client_response.is_valid() is True
+    assert validated_data["id"] is not None
+    assert client_response.exception is None
+
+
+@pytest.mark.asyncio
+async def test_validate_and_fail_with_function(respx_mock):
+    def has_name(response):
+        if response["name"] is not None:
+            if response["name"] == "Alex":
+                return response
+        raise Exception
+
+    respx_mock.post(MOCK_REQUEST_URL).mock(make_mock_response({"name": "morpheus"}))
+
+    client_response: Request = await Request(
+        method=Request.Method.POST,
+        url=MOCK_REQUEST_URL,
+        json={"name": "morpheus", "job": "leader"},
+        validation_function=has_name,
+    )
+    assert client_response.is_valid() is False
+    with pytest.raises(Exception):
+        client_response.is_valid(raise_exceptions=True)
+    assert client_response.exception is not None
 
 
 if __name__ == "__main__":