Repository: gradio-app/gradio
Pull request: Support custom components in gr.load
## Description

Closes: #8124

## 🎯 PRs Should Target Issues

Before your create a PR, please check to see if there is [an existing issue](https://github.com/gradio-app/gradio/issues) for this change. If not, please create an issue before you create this PR, unless the fix is very small. 

Not adhering to this guideline will result in the PR being closed. 

## Tests

1. PRs will only be merged if tests pass on CI. To run the tests locally, please set up [your Gradio environment locally](https://github.com/gradio-app/gradio/blob/main/CONTRIBUTING.md) and run the tests: `bash scripts/run_all_tests.sh`

2. You may need to run the linters: `bash scripts/format_backend.sh` and `bash scripts/format_frontend.sh`
  


Problem statement:
[Custom Components] support custom components in gr.load
- [x] I have searched to see if a similar issue already exists.

**Is your feature request related to a problem? Please describe.**  
I have a space with custom components, but `gr.load` is not working properly.

**Describe the solution you'd like**  
Support custom components in `gr.load`.

**Additional context**  
Add any other context or screenshots about the feature request here.
```
File "/Users/coloring/anaconda3/envs/gradio_4/lib/python3.12/site-packages/gradio/utils.py", line 525, in component_or_layout_class
    raise ValueError(f"No such component or layout: {cls_name}")
ValueError: No such component or layout: i18neditor
```
This error is caused by `gradio.utils.component_or_layout_class`, maybe `gradio` should expose some context or parameters of this method.

Hints from issue discussion:
Makes sense — could you link to the Space you are trying to load or provide us a quick repro?
> Makes sense — could you link to the Space you are trying to load or provide us a quick repro?

Reproduce repo: https://huggingface.co/spaces/Coloring/gradio_load_test

Commit message: Fix tests

Patch to review:
diff --git a/.changeset/fuzzy-mirrors-scream.md b/.changeset/fuzzy-mirrors-scream.md
new file mode 100644
index 0000000000..e38fd74e6f
--- /dev/null
+++ b/.changeset/fuzzy-mirrors-scream.md
@@ -0,0 +1,23 @@
+---
+"gradio": feat
+"gradio_client": patch
+---
+
+#### Support custom components in gr.load
+
+It is now possible to load a demo with a custom component with `gr.load`.
+
+The custom component must be installed in your system and imported in your python session.
+
+```python
+import gradio as gr
+import gradio_pdf
+
+demo = gr.load("freddyaboulton/gradiopdf", src="spaces")
+
+if __name__ == "__main__":
+    demo.launch()
+```
+
+<img width="1284" alt="image" src="https://github.com/gradio-app/gradio/assets/41651716/9c3e846b-f3f2-4c1c-8cb6-53a6d186aaa0">
+
diff --git a/client/python/gradio_client/client.py b/client/python/gradio_client/client.py
index 2f9843565f..d2905d6cb7 100644
--- a/client/python/gradio_client/client.py
+++ b/client/python/gradio_client/client.py
@@ -1171,7 +1171,11 @@ def process_input_files(self, *data) -> tuple:
                     or utils.is_http_url_like(f),
                 )
             elif not self.client.upload_files:
-                d = utils.traverse(d, self._upload_file, utils.is_file_obj_with_meta)
+                d = utils.traverse(
+                    d,
+                    partial(self._upload_file, data_index=i),
+                    utils.is_file_obj_with_meta,
+                )
             data_.append(d)
         return tuple(data_)
 
diff --git a/gradio/utils.py b/gradio/utils.py
index 22576fa740..8d287da2f7 100644
--- a/gradio/utils.py
+++ b/gradio/utils.py
@@ -518,6 +518,32 @@ def resolve_singleton(_list: list[Any] | Any) -> Any:
         return _list
 
 
+def get_all_components() -> list[type[Component] | type[BlockContext]]:
+    import gradio as gr
+
+    classes_to_check = (
+        gr.components.Component.__subclasses__()
+        + gr.blocks.BlockContext.__subclasses__()  # type: ignore
+    )
+    subclasses = []
+
+    while classes_to_check:
+        subclass = classes_to_check.pop()
+        children = subclass.__subclasses__()
+        if children:
+            classes_to_check.extend(children)
+        subclasses.append(subclass)
+    return subclasses
+
+
+def core_gradio_components():
+    return [
+        class_
+        for class_ in get_all_components()
+        if class_.__module__.startswith("gradio.")
+    ]
+
+
 def component_or_layout_class(cls_name: str) -> type[Component] | type[BlockContext]:
     """
     Returns the component, template, or layout class with the given class name, or
@@ -528,33 +554,23 @@ def component_or_layout_class(cls_name: str) -> type[Component] | type[BlockCont
     Returns:
     cls: the component class
     """
-    import gradio.blocks
-    import gradio.components
-    import gradio.layouts
-    import gradio.templates
-
-    components = [
-        (name, cls)
-        for name, cls in gradio.components.__dict__.items()
-        if isinstance(cls, type)
-    ]
-    templates = [
-        (name, cls)
-        for name, cls in gradio.templates.__dict__.items()
-        if isinstance(cls, type)
-    ]
-    layouts = [
-        (name, cls)
-        for name, cls in gradio.layouts.__dict__.items()
-        if isinstance(cls, type)
-    ]
-    for name, cls in components + templates + layouts:
-        if name.lower() == cls_name.replace("_", "") and (
-            issubclass(cls, gradio.components.Component)
-            or issubclass(cls, gradio.blocks.BlockContext)
-        ):
-            return cls
-    raise ValueError(f"No such component or layout: {cls_name}")
+    import gradio.components as components_module
+    from gradio.components import Component
+
+    components = {c.__name__.lower(): c for c in get_all_components()}
+    # add aliases such as 'text'
+    for name, cls in components_module.__dict__.items():
+        if isinstance(cls, type) and issubclass(cls, Component):
+            components[name.lower()] = cls
+
+    if cls_name.replace("_", "") in components:
+        return components[cls_name.replace("_", "")]
+
+    raise ValueError(
+        f"No such component or layout: {cls_name}. "
+        "It is possible it is a custom component, "
+        "in which case make sure it is installed and imported in your python session."
+    )
 
 
 def run_coro_in_background(func: Callable, *args, **kwargs):
diff --git a/test/conftest.py b/test/conftest.py
index 7832df08c7..ccb17fe5a4 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -6,6 +6,7 @@
 from gradio_client import Client
 
 import gradio as gr
+import gradio.utils
 
 
 def pytest_configure(config):
@@ -21,11 +22,9 @@ def test_file_dir():
 
 @pytest.fixture
 def io_components():
-    classes_to_check = gr.components.Component.__subclasses__()
+    all_subclasses = gradio.utils.core_gradio_components()
     subclasses = []
-
-    while classes_to_check:
-        subclass = classes_to_check.pop()
+    for subclass in all_subclasses:
         if subclass in [
             gr.components.FormComponent,
             gr.State,
@@ -33,10 +32,9 @@ def io_components():
             gr.LogoutButton,
         ]:
             continue
-        children = subclass.__subclasses__()
+        if subclass in gr.components.FormComponent.__subclasses__():
+            continue
 
-        if children:
-            classes_to_check.extend(children)
         if "value" in inspect.signature(subclass.__init__).parameters:
             subclasses.append(subclass)
 
diff --git a/test/test_external.py b/test/test_external.py
index 307e2b7c45..39b6b3c3d5 100644
--- a/test/test_external.py
+++ b/test/test_external.py
@@ -514,3 +514,11 @@ def test_use_api_name_in_call_method():
     # app = gr.load(name="spaces/gradio/multiple-api-name-test")
     # assert app(15, api_name="minus_one") == 14
     # assert app(4, api_name="double") == 8
+
+
+def test_load_custom_component():
+    demo = gr.load("spaces/freddyaboulton/gradiopdf")
+    output = demo(
+        "test/test_files/sample_file.pdf", "What does this say?", api_name="predict"
+    )
+    assert isinstance(output, str)


Code review comments:
diff_hunk: @@ -0,0 +1,23 @@
+---
+"gradio": feat
path: .changeset/fuzzy-mirrors-scream.md
text: @user1:
is this supposed to be patch?
```suggestion
"gradio": patch
```

diff_hunk: @@ -518,6 +518,32 @@ def resolve_singleton(_list: list[Any] | Any) -> Any:
         return _list
 
 
+def get_all_components() -> list[type[Component] | type[BlockContext]]:
+    import gradio as gr
+
+    classes_to_check = (
+        gr.components.Component.__subclasses__()
+        + gr.blocks.BlockContext.__subclasses__()  # type: ignore
+    )
+    subclasses = []
+
+    while classes_to_check:
+        subclass = classes_to_check.pop()
+        children = subclass.__subclasses__()
+        if children:
+            classes_to_check.extend(children)
path: gradio/utils.py
text: @user1:
nit: slightly more concise (since if the subclass doesn't have any subclasses, children will be an empty list and extending by an empty list leaves `classes_to_check` unchanged) 
```suggestion
        classes_to_check.extend(subclass.__subclasses__())
```